PHP
Article

Laravel and Braintree, Sitting in a Tree…

By Christopher Vundi

Working with PHP 7.1? Download our FREE PHP 7.1 Cheat Sheet!

Building a Subscription-based Courses Website

This article was peer reviewed by Younes Rafie and Wern Ancheta. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


Subscriptions to services online are something extremely common – from subscribing to music streaming services to tutorial sites to access premium content.

With Laravel 5, we saw the introduction of Laravel Cashier, an official Laravel package to help developers manage Stripe’s and Braintree’s subscription billing services without writing most of the boilerplate subscription billing code.

Stripe and Braintree are payment platforms that make it easy to accept payments in your app or website.

In this tutorial, we will be building a dummy Courses site with Braintree subscriptions. In the process, we will learn how to use the various methods offered by Cashier.

Braintree Logo

In the first part of this extensive two-part series, we are going to:

  • Set up Laravel Cashier
  • Sign up to the Braintree sandbox (For production apps we use the main Braintree service)
  • Create plans on Braintree
  • Create an artisan command to sync the online plans with our database
  • Allow users to subscribe to a plan

In part two, we will:

  • Add the ability to swap plans
  • Create middleware to protect some routes based on the subscription status
  • Protect premium courses from users with basic subscription
  • Learn how to cancel and resume subscriptions
  • Add Braintree notifications to a variety of the application’s events via webhooks

The complete code for part one can be found here.

Creating the Application

We will start with a fresh Laravel installation.

composer create-project laravel/laravel lara-billable

Preparing the Database

Next, we have to set up the database before running any migrations. Let’s use MySQL – it’s easy to use, easy to configure, and included in a well-built development environment like Homestead Improved. After setting up the database, I have my .env file looking like this:

DB_HOST=localhost
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

Scaffolding Auth

The next step is adding authentication to our application.

Laravel introduced a built in Authentication module in version 5.2 which made this extremely easy.

php artisan make:auth

By running the command, everything related to authentication will be generated for us i.e. the views, controllers and the routes mapping to the controller actions.

To be able to sign up and log in, we need to have tables to hold the user data. If we look inside database/migrations, we’ll notice that the generated Laravel app came with migrations for creating the users and password_resets tables. Let’s run these migrations:

php artisan migrate

If we navigate to /register, we can now sign up new users. Links for signing up and logging in are also present in the navbar.

Setting up Cashier

With the users table in place, we can now add Cashier. Since we’ll be using Braintree for this tutorial, let’s require the braintree-cashier package:

composer require laravel/cashier-braintree

Then, we register the Laravel\Cashier\CashierServiceProvider in our config/app.php:

'providers' => [
    // Other service providers...
    Laravel\Cashier\CashierServiceProvider::class,
],

Next, we have to pull in the Billable trait in the User model so as to be able to call the various cashier methods on a user:

[...]
use Laravel\Cashier\Billable;
[...]
class User extends Authenticatable
{
    use Billable;
    [...]
}

Then we add extra columns to the users table for billing purposes. We will also create a subscriptions table to handle all of our subscriptions:

php artisan make:migration add_billable_columns_to_users_table --table=users

Open the newly generated migration and change the up method to this:

public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('braintree_id')->nullable();
            $table->string('paypal_email')->nullable();
            $table->string('card_brand')->nullable();
            $table->string('card_last_four')->nullable();
            $table->timestamp('trial_ends_at')->nullable();
        });
    }

Let’s now create the subscription model and migration:

php artisan make:model Subscription -m

Open the migration and tweak the up method to this:

public function up()
{
    Schema::table('subscriptions', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id'); // A subscription belongs to a user
        $table->string('name');  // The name of the subscription
        $table->string('braintree_id'); //id for the subscription
        $table->string('braintree_plan'); // The name of the plan
        $table->integer('quantity');
        $table->timestamp('trial_ends_at')->nullable(); 
        $table->timestamp('ends_at')->nullable();
        $table->timestamps();
  });
}

With this set up, run the migrate Artisan command to create the subscriptions table and add the extra columns to the users table:

php artisan migrate

At this point, we have our Laravel side set up. Time to wire things up on the Braintree end. We will be using the Braintree Sandbox since this is not a production app. For those who don’t have a Sandbox account, sign up here, then log in.

Once inside the dashboard, generate a new API key in order to be able to use Braintree’s API in our app:

Generating a new API Key

After generating the key, we also get the Public Key, Environment Key and Merchant ID. With these in place, we need to set up configuration in our Laravel App so we can communicate with the Braintree API. Open your .env file and set the keys alongside their corresponding values. I have my .env file looking like this:

BRAINTREE_ENV=sandbox
BRAINTREE_MERCHANT_ID=xxxxxxxxxxxxxx
BRAINTREE_PUBLIC_KEY=xxxxxxxxxxxxxx
BRAINTREE_PRIVATE_KEY=xxxxxxxxxxxxxx

Then we add the Braintree configuration to our config/services.php file:

'braintree' => [
        'model' => App\User::class, //model used to processs subscriptions
        'environment' => env('BRAINTREE_ENV'),
        'merchant_id' => env('BRAINTREE_MERCHANT_ID'),
        'public_key' => env('BRAINTREE_PUBLIC_KEY'),
        'private_key' => env('BRAINTREE_PRIVATE_KEY'),
],

As the final step before communicating with Braintree’s API, let’s add the following Braintree SDK calls to our AppServiceProvider service provider’s boot method. We will use the env helper to pull in the values we set in our .env file. Note that we also have to import the Braintree_Configuration class into our AppServiceProvider, else we won’t be able to call the various methods from the Braintree_Configuration class:

[...]
use Braintree_Configuration;
[...]
public function boot()
{
    Braintree_Configuration::environment(env('BRAINTREE_ENV'));
    Braintree_Configuration::merchantId(env('BRAINTREE_MERCHANT_ID'));
    Braintree_Configuration::publicKey(env('BRAINTREE_PUBLIC_KEY'));
    Braintree_Configuration::privateKey(env('BRAINTREE_PRIVATE_KEY'));
    // Cashier::useCurrency('eur', '€');
}

The default Cashier currency is United States Dollars (USD). We can change the default currency by calling the Cashier::useCurrency method from within the boot method of one of the service providers. The useCurrency method accepts two string parameters: the currency and the currency’s symbol. In this tutorial, we will just stick to USD. I have a commented out line of code illustrating how to change to Euros for example.

I must admit that setting up Cashier took a while but once done with this, we find ourselves in a position to start processing payments and manage subscriptions.

Creating Plans and Syncing Them to Our Database

Our next step is creating the plans. For this tutorial, we will be creating two: a Basic and a Premium plan. Let’s head over to the Braintree dashboard and do that. Note the trial period is optional but I set mine to 14 days:

Plan creation

Continuation:

Plan creation

Repeat the same process to create a premium plan but set the amount to 20 USD for a subscription.

When done creating the plans on Braintree, we can then create a plans table to store the plans locally. Let’s generate a Plan model and a migration:

php artisan make:model Plan -m

Update the up method in your migration to this:

[...]
public function up()
    {
        Schema::create('plans', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('slug')->unique(); //name used to identify plan in the URL
            $table->string('braintree_plan');
            $table->float('cost');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }
[...]

Create the plans table by running the migration:

php artisan migrate

Syncing Plans

Next, we want to populate the Plans table with the data we set on Braintree. Hard coding the plans into our table is allowed but I find it a bit tedious, especially when the plan information online keeps changing. To simplify the process, we will create an artisan command to sync with the plans online and update our database:

php artisan make:command SyncPlans

For those not on Laravel 5.3+ run:

php artisan make:console SyncPlans

In app/Console/Commands/SyncPlans.php, we need to change the signature value and also add a description for the command:

[...]
protected $signature = 'braintree:sync-plans';
protected $description = 'Sync with online plans on Braintree';
[...]

We then register the command to the kernel so as to be able to run it from the terminal:

class Kernel extends ConsoleKernel
{
    [...]
    protected $commands = [
        Commands\SyncPlans::class,
    ];
    [...]
}

If we now run php artisan, the plan-syncing command will be visible from the list of available commands:

Command from terminal

Then the big question, what should happen after running this command? The command is supposed to clear the data in the plans table, then populate the table with the Plan data available online. This logic should be placed in the handle method inside app/Console/Commands/SyncPlans.php:

[...]
use Braintree_Plan;
use App\Plan;
[...]
class SyncBraintreePlans extends Command
{
    [...]
    public function handle()
    {
        // Empty table
        Plan::truncate();

        // Get plans from Braintree
        $braintreePlans = Braintree_Plan::all();

        // uncomment the line below to dump the plans when running the command
        // var_dump($braintreePlans);

        // Iterate through the plans while populating our table with the plan data
        foreach ($braintreePlans as $braintreePlan) {
            Plan::create([
                'name' => $braintreePlan->name,
                'slug' => str_slug($braintreePlan->name),
                'braintree_plan' => $braintreePlan->id,
                'cost' => $braintreePlan->price,
                'description' => $braintreePlan->description,
            ]);
        }
    }
}

Note we have to pull in the Braintree_Plan and App\Plan namespaces so as to be able to call methods statically from these classes.

We should then navigate to the Plan model and add the name, slug, braintree_plan, cost and description to the list of mass assignable attributes. If we don’t do this, we will get a MassAssignmentException when trying to update the attributes:

class Plan extends Model
{
    [...]
    protected $fillable = ['name', 'slug', 'braintree_plan', 'cost', 'description'];
    [...]
}

We can then run our command to see if everything is working as expected:

php artisan braintree:sync-plans

For those planning to push the app to production, it’s always a good idea to set the site to maintenance mode before syncing plans then bring the site up once the syncing process is over. In this case I would have done something like this:

php artisan down
php artisan braintree:sync-plans
php artisan up

Displaying Plans

Let’s now display the plans on a page. We will start by creating a route:

routes/web.php

[...]
Route::get('/plans', 'PlansController@index');

Then, we create a PlansController and add an index action:

php artisan make:controller PlansController

The index action should return a view listing all the plans:

app/Http/Controllers/PlansController.php

[...]
use App\Plan;
[...]
class PlansController extends Controller
{
    public function index()
    {
        return view('plans.index')->with(['plans' => Plan::get()]);
    }
}

Now let’s set up the view. Create a plans folder before creating the index view:

mkdir resources/views/plans
touch resources/views/plans/index.blade.php

In the view, paste the following code:

resources/views/plans/index.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Choose your plan</div>

                <div class="panel-body">
                    <ul class="list-group">
                        @foreach ($plans as $plan)
                            <li class="list-group-item clearfix">
                                <div class="pull-left">
                                    <h4>{{ $plan->name }}</h4>
                                    <h4>${{ number_format($plan->cost, 2) }} monthly</h4>
                                    @if ($plan->description)
                                        <p>{{ $plan->description }}</p>
                                    @endif
                                </div>

                                <a href="#" class="btn btn-default pull-right">Choose Plan</a>

                            </li>
                        @endforeach
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

We’ve used the number_format helper to help us display the amount to two decimal places. As it stands, the Choose Plan button does not lead anywhere, but we’ll fix that in a moment. If we now visit /plans, all the plans will be displayed. Update the navbar to include a link pointing to the plans:

resoursces/views/layouts/app.blade.php

[...]
<ul class="nav navbar-nav navbar-left">
    <li><a href="{{ url('/plans') }}">Plans</a></li>
</ul>
<div class="collapse navbar-collapse" id="app-navbar-collapse">
[...]

Your view should now look like this:
Plans in nav

Our next step is creating the payment form where users will fill in their credit card details before subscribing to a plan.

Card Payment Form

We are going to use Braintree’s Drop-in UI for this. More documentation on how to customize the Drop-in UI can be found here.

First things first, we create a route pointing to the view. We also want to make it such that only authenticated users can perform operations involving subscriptions. Let’s create a route group and add the auth middleware. It is in this group where all routes pointing to subscription related activities will fall:

routes/web.php

[...]
Route::group(['middleware' => 'auth'], function () {
    Route::get('/plan/{plan}', 'PlansController@show');
});

Then create the show action inside our PlansController:

class PlansController extends Controller
{
    [...]
    public function show(Plan $plan)
    {
        return view('plans.show')->with(['plan' => $plan]);
    }
}

Since we want to pass in the slug value in the URL instead of the Plan ID, we have to override the getRouteKeyName method on the Eloquent model. By default, when using Route Model Binding in Laravel, it’s the id that is returned. Let’s change that so we can have the slug value returned instead:

app/Plan.php

public function getRouteKeyName()
{
  return 'slug';
}

The “show” URL should now be in this format: /plan/{slug}. Time we created the view holding the payment form:

resources/views/plans/show.blade.php

@extends('layouts.app')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col-md-8 col-md-offset-2">
        <div class="panel panel-default">
          <div class="panel-heading">{{ $plan->name }}</div>
          <div class="panel-body">
            ....
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

We can test things out by visiting /plan/premium or /plan/basic, depending on the plan name. Let’s also make the button in the plans index page point to the show view:

resources/views/plans/index.blade.php

[...]
<a href="{{ url('/plan', $plan->slug) }}" class="btn btn-default pull-right">Choose Plan</a>
[...]

At this point, the view is very basic and does not allow customers to enter credit card details. To load Braintree’s Drop-in UI, let’s require braintree.js after the content section:

resources/views/plans/show.blade.php

@section('braintree')
    <script src="https://js.braintreegateway.com/js/braintree-2.30.0.min.js"></script>
@endsection

Then make sure the script is loaded in the app:

resources/views/plans/layouts/app.blade.php

[...]
<!-- Scripts -->
<script src="/js/app.js"></script>
@yield('braintree')
[...]

Update the show view to include a form. The CLIENT-TOKEN-FROM-SERVER is required for this to work but we’ll handle that later:

resources/views/plans/show.blade.php

@extends('layouts.app')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col-md-8 col-md-offset-2">
        <div class="panel panel-default">
          <div class="panel-heading">{{ $plan->name }}</div>
          <div class="panel-body">
            <form>
              <div id="dropin-container"></div>
              <hr>

              <button id="payment-button" class="btn btn-primary btn-flat" type="submit">Pay now</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

@section('braintree')
    <script src="https://js.braintreegateway.com/js/braintree-2.30.0.min.js"></script>

    <script>
  braintree.setup('CLIENT-TOKEN-FROM-SERVER', 'dropin', {
    container: 'dropin-container'
  });
    </script>
@endsection

If we visit /plans/{slug-value} or click on the Choose Plan button in the plans listing page, we should be taken to a view that looks like this:

Basic or premium plan listing

We don’t, however, want the Pay Now button to be present when the payment form has not yet loaded. To achieve this, we are going to add a hidden class to the button, then remove the hidden class once the payment form shows up:

resources/views/plans/show.blade.php

<form>
     [...]
     <button id="payment-button" class="btn btn-primary btn-flat hidden" type="submit">Pay now</button>
     [...]
</form>

Let’s now create a new controller responsible for generating the token. We will then use ajax to set the generated token as the CLIENT-TOKEN-FROM-SERVER value. Note that braintree.js needs a client token generated by the Braintree server SDK for the payment form to be loaded:

php artisan make:controller BraintreeTokenController

Then, inside the controller, we create a token action which returns a JSON response containing the token:

app/Http/Controllers/BraintreeTokenController.php

[...]
use Braintree_ClientToken;
[...]
class BraintreeTokenController extends Controller
{
    public function token()
    {
        return response()->json([
            'data' => [
                'token' => Braintree_ClientToken::generate(),
            ]
        ]);
    }
}

Don’t forget to pull the Braintree_ClientToken namespace into this controller; otherwise, the generate method responsible for creating the token will error.

We then update our routes making it such that we can access a URL containing the JSON response from the token method:

routes/web.php

Route::group(['middleware' => 'auth'], function () {
    Route::get('/plan/{plan}', 'PlansController@show');
    Route::get('/braintree/token', 'BraintreeTokenController@token');
});

Try visiting /braintree/token and a token value will be displayed on the page. Our next step is pulling in this token value using AJAX into the show view. Update the code in the braintree section:

resources/views/plans/show.blade.php

@section('braintree')
    <script src="https://js.braintreegateway.com/js/braintree-2.30.0.min.js"></script>

    <script>
        $.ajax({
            url: '{{ url('braintree/token') }}'
        }).done(function (response) {
            braintree.setup(response.data.token, 'dropin', {
                container: 'dropin-container',
                onReady: function () {
                    $('#payment-button').removeClass('hidden');
                }
            });
        });
    </script>
@endsection

In the code block above, we are making a request to the braintree/token URL in order to obtain a JSON response containing the token. This token is what we then set as the CLIENT-TOKEN-VALUE. Once the payment form loads, we remove the hidden class from our Pay Now button making it visible to our customers.

With this in place, we can try reloading the page and we should see a payment form that looks like this:

Payment form

It’s time we made the form come to life so users can subscribe to a plan. Let’s update our form:

app/resources/views/plans/show.blade.php

[...]
<form action="{{ url('/subscribe') }}" method="post">
  <div id="dropin-container"></div>
  <input type="hidden" name="plan" value="{{ $plan->id }}">
  {{ csrf_field() }}
  <hr>
  <button id="payment-button" class="btn btn-primary btn-flat hidden" type="submit">Pay now</button>
</form>
[...]

The form will make a POST request to the /subscribe URL – we are yet to create a route and a controller action to handle that. We also added a hidden input to our form. This will help the SubscriptionsController know the plan we are subscribing to. Then, to protect users against Cross-Site Request Forgery when submitting the form, we’ve used Laravel’s csrf_field helper to generate a CSRF token.

Let’s create the route and controller responsible for subscribing users:

Route::group(['middleware' => 'auth'], function () {
    [...]
    Route::post('/subscribe', 'SubscriptionsController@create');
});
php artisan make:controller SubscriptionsController

Inside the controller, add a store action. This is the action responsible for creating and adding new subscriptions to the database:

app/Http/Controllers/SubscriptionsController.php

[...]
use App\Plan;
[...]
class SubscriptionController extends Controller
{
    public function store(Request $request)
    {
          // get the plan after submitting the form
          $plan = Plan::findOrFail($request->plan);

          // subscribe the user
          $request->user()->newSubscription('main', $plan->braintree_plan)->create($request->payment_method_nonce);

          // redirect to home after a successful subscription
          return redirect('home');
    }
}

What we are doing inside the method is getting the plan from the value we passed in the hidden input. Once we get the plan, we call the newSubscription method on the currently logged in user. This method came with the Billable trait that we required in the User model. The first argument passed to the newSubscription method should be the name of the subscription. For this app, we only offer monthly subscriptions and thus called our subscription main. The second argument is the specific Braintree plan the user is subscribing to.

The create method accepts the payment_method_nonce that we generate from Braintree as its argument. It’s the create method that will begin the subscription as well as update our database with the customer ID and other relevant billing information.

After subscribing the user, we redirect them to the homepage. We will implement basic flash messaging for notifications later.

We can now confirm that things are working by filling in the form with a test card, then navigating to the Braintree dashboard to see if the card was billed. For the card number, use 4242 4242 4242 4242 and set the date to a future date. Something like this:

Filling in the form

If everything went as expected, a new payment should be reflected in your braintree dashboard. A new record will also be added to the subscriptions table.

Conclusion

We’ve come a long way to get to this point but users now have the ability to subscribe to a plan. We covered basic Braintree integration with Laravel, plan creation, building of commands for syncing and fetching the token, and more – everything you need to get started with Braintree subscriptions.

In the next part, we’ll prevent users from signing up to the same plan twice, amongst other things. Stay tuned.

  • eusonlito

    Braintree currently support PSR-4 namespacing.

    I thinkt that you should update your examples with the new namespacing :)

  • Will you be covering Invoice generation?

    • Christopher Vundi

      Unfortunately no… might update the article later though

  • Mustafa Neguib

    I thank you for writing this amazing tutorial on setting up brain tree with laravel. This surely has helped me in setting this up for my project.

  • Vic Tor

    Hi. Thanks for this tutorial, but I’m having a problem with the step in pulling thetoken value using AJAX into the show view. “Uncaught ReferenceError: $ is not defined” error occurs so I added a call to a Jquery CDN. There’s no error but the form does not show, what could be theproblem? Thank you!

  • Vic Tor

    You referenced the create method in the route file but actually used store method in the SubscriptionController. Please edit. Thanks!

    • Christopher Vundi

      Thanks for pointing that out. I will update the article

Recommended
Sponsors
Get the latest in PHP, once a week, for free.