Memberships with Laravel Cashier

Younes Rafie
Tweet

In this tutorial we’re going to see how easy it is to create a basic paid membership website using the Laravel Cashier package. You can see a demo of the app here and download the source code here.

Setting up a working environment

We need to create a boilreplate project to get started, and we can do that in two different ways:

  • we can clone the Github repo to our project folder.
  • Assuming that you have composer installed, ( check installation guide for more details).
    we run composer create-project laravel/laravel laravel_membership --prefer-dist, this will create a laravel boilerplate project in our laravel_membership folder.

Now we need to require the Laravel Cashier package to the project by adding "laravel/cashier": "~1.0" to our composer.json on the require section and run composer update to update our dependencies.

After that we need to tell our app to load the Cashier service provider class. We can do that by adding "Laravel\Cashier\CashierServiceProvider" to the providers array on the config/app.php file.

Note: You must run composer dump-autoload to update the classMap package.

Creating the database using migrations

If you are new to Laravel Migrations be sure to check the documentation.
We are going to use two tables:

posts table:
INT id
STRING title
LONG_TEXT content
BOOL is_premium

users table:
INT id
VARCHAR(60) email
VARCHAR(60) password

The Laravel artisan command line tool makes it easy to create and manage migration classes.

php artisan migrate:make create_posts_table --create="posts"

php artisan migrate:make create_users_table --create="users"

and then we fill the Schema::create callback function argument with the necessary code that looks like this

Schema::create('posts', function(Blueprint $table)
{
    $table->increments('id');
    $table->string('title');
    $table->longText('content');
    $table->boolean("is_premium");
    $table->timestamps();
});

Schema::create('users', function(Blueprint $table)
{
    $table->increments('id');
    $table->string('email', 100)->unique();
    $table->string('password', 60);
    $table->timestamps();
});

To let Laravel Cashier know about our billable table we need to create a specific migration for that. Laravel Cashier has a built in command for that purpose.

php artisan cashier:table users

Now we are ready to migrate our database

php artisan migrate

if you open your users table you will see a bunch of fields added when the package migration is executed.
stripe_active if you have an active subscription.
stripe_id user id on Stripe server.
stripe_plan Stripe subscription plan.
last_four credit card last four digits.
trial_ends_at an end date is stored if you specify a trial period.
subscription_ends_at subscription end date.

Now we will seed the database with some dummy data to get started; check the final result on GitHub.

Stripe billing process

Dealing with payment can be a pain in the neck, and Stripe can help you with that, they use tokens instead of card numbers etc.., and that’s how you can make sure that your customers stay secure while paying for your service.

NOTE: Check if Stripe is supported in your country, but you can still use it for testing if not.

To get started we need to get an account first. Stripe doesn’t have a monthly fee for the subscription, you only pay when you get paid.

Now, after getting an account you need to create Plans for your application (Monthly, Yearly, Silver, Gold…).

new_plan.png

Every field is self explanatory, so lets create a Gold membership that will cost $40 and a Basic membership for $10. They will be billed every month.

We have already added the necessary columns to our users table, now we need to let Laravel Cashier know that we will use the User class as our billing class.

use Laravel\Cashier\BillableInterface;
use Laravel\Cashier\BillableTrait;

class User extends Eloquent implements BillableInterface {

    use BillableTrait;
    protected $dates = ['trial_ends_at', 'subscription_ends_at'];

Note: we’re using BillableTrait and traits require PHP 5.4 or greater.

Now we have to set our Stripe API access key, which you can get from Your account > Account settings > API Keys and copy your Test Secret Key.

Settings API Keys

By using the BillableTrait we get access to the User::setStripeKey(key) method which can be called anywhere in our code, but the preferred way is to create a services.php file under your config directory and return an array like this:

return [
    'stripe' => [
            'secret'    => 'Your key'
        ]
];

When getStripeKey tries to load your key it will look for a property called stripeKey. If not found, it will automatically load your services file.

Creating our pages

To keep things simple we will create only a few pages:
– Signup: where user can signup with a membership plan ( Basic, Gold ).
– Login: members login page.
– Upgrade: upgrade from basic to gold membership.
– Post: display a single post page.

To speed up the process we will use bootsnipp. You can get the final code from the GitHub repo.

Login page:

login page

The login page has a basic email and password field, with a LoginController page that looks like this:

public function index(){
    return View::make('login');
}
public function store(){
    if( Auth::attempt( Input::only( ['email', 'password'] ), true)){
        return Redirect::to('/');
    }
    else{
        return Redirect::back()->withInput()->with( 'message', 'Email or password incorrect' );
    }
}
public function destroy(){
    Auth::logout();
    return Redirect::route("login");
}

Signup page:

signup page

The signup page has a Subscription plan field used to assign a user to plan.
We have also a Credit card number, Expiration date, CVC.

As we said earlier, we will never have to deal with any payment or verification process, we pass those values to the Stripe server to take care of the charging and verification process.
The return value is a token in case of success otherwise we get an error message that we can show to the user.

Let’s see what the front-end code looks like:

<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script>
    Stripe.setPublishableKey('Your public key');
    jQuery(function($) {
        $('#subscription-form').submit(function(event) {
            var $form = $(this);
            $form.find('button').prop('disabled', true);
            Stripe.card.createToken($form, stripeResponseHandler);

            return false;
        });
    });

    var stripeResponseHandler = function(status, response) {
        var $form = $('#subscription-form');

        if (response.error) {
            $form.find('.payment-errors').text(response.error.message);
            $form.find('button').prop('disabled', false);
        } else {
            var token = response.id;
            $form.append($('<input type="hidden" name="stripeToken" />').val(token));
            $form.get(0).submit();
        }
    };
</script>

First we include the JavaScript API file, then we set our public key that we grabbed from our Stripe dashboard settings.

Next we attach a callback function to our submit form (be sure that your form ID matches the one used on the event handler), to prevent a double submission we disable our submit button.
The Stripe.card.createToken accepts two arguments, the first one is a JSON object that has some required and optional values.

Required values:

  • number: card number as a string without any separators.
  • exp_month: two digit number representing the card’s expiration month.
  • exp_year: two or four digit number representing the card’s expiration year.

Optional values:

  • cvc: card security code as a string, the cvc number is optional but recommended to help prevent fraud.
  • name: cardholder name.
  • address_line1: billing address line 1.
  • address_line2: billing address line 2.
  • address_city: billing address city.
  • address_state: billing address state.
  • address_zip: billing zip as a string.
  • address_country: billing address country.
    You can notice that we’re passing a form object instead of a JSON object, you can choose to grab the values manually or use the data-stripe html5 attribute on your inputs and Stripe will use some helper methods to grab those values automatically for you. Ex:
    <input data-stripe="number" type="text">

The second argument passed to Stripe.card.createToken method is a callback function to handle the response.

In case of failure the stripeResponseHandler will try to find an element with a class of payment_errors to display some descriptive errors to the user.
In case of success a stripeToken hidden input will be appended to the form and it will be available on submit.

Additional options

  • Trial periods: as we stated before, when you create a new plan you have a choice to specify a trial period for users to test your product, and they won’t be charged until the specified period has elapsed.
  • Coupons: you create coupons via your dashboard menu where you can specify a fixed amount or by percentage,with some other useful options.

Now let’s move to our SignupController to see how we will handle this.

public function store(){
    $user = new User;
    $user->email = Input::get( 'email' );
    $user->username = Input::get( 'username' );
    $user->password = Hash::make( Input::get( 'password' ) );
    $user->save();
    $user->subscription(Input::get( 'subscription' ))->create( Input::get( 'stripeToken' ) );

    return 'you are now registred';
}

We will skip the validation process to keep things simple.

After creating a new User and saving it, we now have the option to subscribe the user to a new membership plan. The subscription method accepts an already registered plan as an argument, that can be either a PlanInterface or a String and return a StripeGateway.
The create method accepts a token as a parameter; we pass the new hidden input value with the name stripeToken.

Upgrade page:

upgrade page

The upgrade page will submit to the UpgradeController that looks like this:

public function store(){
    if( !Auth::check() )
        return Redirect::route("login");

    Auth::user()->subscription('gold')->swap();

    return 'You are now a GOLD member';
}

We check if the user is logged in first, then we create a new subscription with the new plan and we call the swap method, obviously in a real project you will have some fees adjustments and a downgrade option, but it should work the same way.

Post page:

Post page

The PostController checks if the post is_premium, and if so, we test if the user is a gold member who can see the post, else we return a simple error message.

public function show( $id ){
    $post = Post::find( $id );

    if( $post->is_premium && Auth::user()->stripe_plan != 'gold' )
        return View::make('error', [ 'message' => 'Only GOLD members can read this post, <a href="/upgrade">upgrade</a> your membership to get access' ] );

    return View::make('post', [ 'post' => $post ] );
}//show

Of course in our routes.php file we need to add an auth filter to prevent unauthenticated users from accessing the page.
Our routes file will look like this:

Route::get('/', function()
{
    $posts = Post::all();

    return View::make('index', [ 'posts' => $posts ]);
})->before('auth');


Route::get('/post/{id}', [ 'as' => 'post', 'uses' => 'PostsController@show' ])->before('auth');
Route::resource('login', 'LoginController', [ 'only' => [ 'index', 'store', 'destroy' ] ]);
Route::resource('signup', 'SignupController', [ 'only' => [ 'index', 'store' ] ]);
Route::resource('upgrade', 'UpgradeController', [ 'only' => [ 'index', 'store' ] ]);

Other useful methods

  • withCoupon: we said before that we have the possibility to create discount coupons, in our example we can do that like so:
$user->subscription(Input::get( 'subscription' ))->withCoupon('coupon code')->create( Input::get( 'stripeToken' ) );
  • cancel: you can easily cancel a subscription using this method, but you have to check whether the user is onGracePeriod to be sure you don’t block them immediately:
User::find(1)->onGracePeriod();
  • onPlan: see if a user is on a certain plan.
  • onTrial: see if a user still on trial period.
  • canceled: if the user has canceled their subscription.
  • getLastFourCardDigits: get the user card last four digits.
  • getSubscriptionEndDate: get the subscription end date.
  • getTrialEndDate: get the trial end date.
  • invoices: get the list of user invoices.
  • findInvoice: find an invoice by id.
  • downloadInvoice: generate a downloadable invoice by id.

Conclusion

In this tutorial we explored how Laravel Cashier can ease the billing process and help manage your customers more easily.

We certainly didn’t cover everything here, but it’s a start for you to dig into the source code to explore what else you can do. If you’d like to see a Nitrous-hosted demo of this app, see here.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • maxsurguy

    Great tutorial. I love Laravel and cashier seems like a blessing from above for e-commerce projects. Thanks for using my Bootsnipp as well :)

  • younesrafie

    Sorry about that, can you use the link on the conclusion. the broken link should be fixed in a moment :)

  • younesrafie

    can you use the link on the conclusion. the broken link should be fixed in a moment :)

  • http://www.bitfalls.com/ Bruno Skvorc

    Fixed, thanks for the heads up!

  • http://www.bitfalls.com/ Bruno Skvorc

    Thanks for the heads up, fixed!

  • http://julien.tant.me Julien Tant

    Can we use Cashier for classic payment, or is it only for subscriptions ?

    • younesrafie

      charges are covered by Stripe, you can check https://stripe.com/docs/tutorials/charges for more infos

      • http://julien.tant.me Julien Tant

        My question was more related to cashier than to Stripe :) But i’ll dig in to see if it’s possible. Thanks anyway

        • younesrafie

          No, i don’t think that Cashier support that

  • Andrew Del Prete

    Nice post. I’ve been messing with Cashier for a few weeks now and I’m running into an issue with creating a user and THEN subscribing them (as shown in your example).

    I’ve output the DB Queries and for some reason the query to subscribe isn’t being run after the save().

    $user = new User;
    $user->fill($input);
    $user->save();

    $user->subscription(‘monthly’)->create($input['creditCardToken']);

    If I were to do a User::find({an existing user id}) then run the subscribe it works fine. It’s just after I create a user and try to subscribe.

    Any ideas?

    • younesrafie

      i think you did something wrong, Stripe can’t know about your customer if the subscription query is not executed after the save method!!

      • Andrew Del Prete

        I figured out the problem! I was using Jeffrey Ways Model Validator package. This was keeping it from persisting to the DB for some reason. I extracted my validation into some custom classes and doing that manually now. All is well! Thank you!