Memberships with Laravel Cashier
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 runcomposer create-project laravel/laravel laravel_membership --prefer-dist
, this will create a laravel boilerplate project in ourlaravel_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…).
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
.
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:
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:
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, thecvc
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 thedata-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:
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:
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 isonGracePeriod
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.