Laravel & Braintree: Middleware and Other Advanced Concepts

Christopher Vundi
Christopher Vundi
Share

This article was peer reviewed by Viraj Khatavkar. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


Previously, we saw how to configure a Laravel app to handle Braintree subscriptions.

Braintree Logo

This time, we’ll talk about how to:

  • Prevent users from signing up to the same plan twice
  • Add basic flash messaging to our application
  • Add the ability to swap plans
  • Create middleware to protect some routes based on the subscription status
  • Restrict users with basic subscriptions from accessing premium content
  • Cancel and resume subscriptions
  • Add Braintree notifications to the application’s events via webhooks

Double Subscriptions

As it stands, if we visit the plans index page, we can still see the Choose Plan button for the plan we are currently subscribed to, and this shouldn’t be the case. In the plans index view, let’s add an if conditional to hide the button based on the user’s subscription status:

[...]
@if (!Auth::user()->subscribedToPlan($plan->braintree_plan, 'main'))
    <a href="{{ url('/plan', $plan->slug) }}" class="btn btn-default pull-right">Choose Plan</a>
@endif
[...]

But that’s not to say users can’t access the plan by typing in the URL pointing to the same plan in the address bar. To counter this, let’s update the code in the show action of the PlansController to this:

[...]
public function show(Request $request, Plan $plan)
{
    if ($request->user()->subscribedToPlan($plan->braintree_plan, 'main')) {
        return redirect('home')->with('error', 'Unauthorised operation');
    }

    return view('plans.show')->with(['plan' => $plan]);
}
[...]

Here, we are getting the user from the request object; remember all our routes fall under the auth middleware and thus it’s possible to get the authenticated user. Once we get the user, we check if they are already subscribed to the plan. If that’s the case, we redirect them to the homepage and display a notification. We will implement basic flash messaging later.

One last precaution is preventing users from submitting the payment form with a different plan ID value. It’s possible to inspect the DOM elements and change the value for the hidden input. In our SubscriptionsController, let’s update the store method to this:

[...]
public function store(Request $request)
{
    $plan = Plan::findOrFail($request->plan);

    if ($request->user()->subscribedToPlan($plan->braintree_plan, 'main')) {
        return redirect('home')->with('error', 'Unauthorised operation');
    }

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

    // redirect to home after a successful subscription
    return redirect('home')->with('success', 'Subscribed to '.$plan->braintree_plan.' successfully');
}
[...]

Flash Messaging

Let’s now implement some basic flash messaging to display notifications in the app in response to certain operations. In the resources/views/layouts/app.blade.php file, let’s insert this block right above our content:

[...]
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            @include ('layouts.partials._notifications')
        </div>
    </div>
</div>
@yield('content')
[...]

Then, we create the notifications partial:

resources/views/layouts/partials/_notifications.blade.php

@if (session('success'))
    <div class="alert alert-success">
        {{ session('success') }}
    </div>
@endif

@if (session('error'))
    <div class="alert alert-danger">
        {{ session('error') }}
    </div>
@endif

In the partial, we have used the session helper to help us come up with different notification colors based on the session status i.e. success or error.

Swapping Plans

After a user has subscribed to our application, they may occasionally want to change to a new subscription plan.

To accomplish this, we’ll first have to check if the user is subscribed to any plan inside the store method of the SubscriptionsController. If not, we subscribe them to a new plan. To swap a user to a new subscription, we pass the plan’s identifier to the swap method.

Let’s open our SubscriptionsController and update the store method:

public function store(Request $request)
{
    [...]
    if (!$request->user()->subscribed('main')) {
        $request->user()->newSubscription('main', $plan->braintree_plan)->create($request->payment_method_nonce);
    } else {
        $request->user()->subscription('main')->swap($plan->braintree_plan);
    }

    return redirect('home')->with('success', 'Subscribed to '.$plan->braintree_plan.' successfully');
}

Let’s see if things are working as intended by choosing a different plan, filling in fake card details (same details used when subscribing to a new plan in the previous post) then submitting the form. If we look inside the subscriptions table, we should notice the plan for the authenticated user has changed. This change should also be reflected in Braintree under transactions. Next, we want to protect routes based on subscription status.

Protecting Routes

For this application, we want to introduce lessons. Only subscribed users should have access to lessons. We’ll generate a LessonsController:

php artisan make:controller LessonsController

… then head over to the controller and create an index action. We won’t be creating views for this, just displaying some text:

[...]
public function index()
{
    return 'Normal Lessons';
}
[...]

Then, we create the route pointing to this index action inside the route group with the auth middleware:

Route::group(['middleware' => 'auth'], function () {
    Route::get('/lessons', 'LessonsController@index');
});

Let’s also update our navbar to include a link to these ‘lessons’:

resources/views/layouts/app.blade.php

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

If we now click on Lessons from the navbar, we’ll see the text “Normal Lessons” regardless of the subscription status. This shouldn’t be the case. Only subscribed users should be able to access lessons. To enable this behavior, we’ll have to create some middleware:

php artisan make:middleware Subscribed

Let’s open the file in app/Http/Middleware/Subscribed.php and update the handle method to this:

[...]
public function handle($request, Closure $next)
{
    if (!$request->user()->subscribed('main')) {
        if ($request->ajax() || $request->wantsJson()) {
            return response('Unauthorized.', 401);
        }
        return redirect('/plans');
    }

    return $next($request);
}
[...]

In the code block above, we are checking for users that are not subscribed to any plan. If that’s the case, whether using ajax or not, they will get the Unauthorized response or else be redirected to the plans’ index page. If a user is already subscribed to a plan, we’ll just proceed with the next request.

With this defined, let’s head over to app/Http/Kernel.php and register our middleware to the $routeMiddleware so we can call the middleware within our routes:

app/Http/Kernel.php

protected $routeMiddleware = [
    [...]
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'subscribed' => \App\Http\Middleware\Subscribed::class,
];

We can now create another route group inside the group with the auth middleware, but this time around, we pass in subscribed as the middleware. It’s inside this new route group where we will define the route to access lessons:

Route::group(['middleware' => 'auth'], function () {
    [...]
    Route::group(['middleware' => 'subscribed'], function () {
        Route::get('/lessons', 'LessonsController@index');
    });
});

If we try signing up a new user, then clicking on Lessons from the navbar, we will be redirected to /plans. If we subscribe to a plan with the new account, we now have access to lessons.

Protecting Premium Content from Basic Users

For our app, we want to provide basic and premium content. Users with basic plans will have access to normal lessons but only users with premium subscriptions will have access to premium lessons. For demonstration purposes, let’s create a premium method inside LessonsController:

[...]
public function premium()
{
    return 'Premium Lessons';
}
[...]

Let’s also update the navbar to include a link pointing to Premium Lessons:

[...]
<ul class="nav navbar-nav navbar-left">
    <li><a href="{{ url('/plans') }}">Plans</a></li>
    <li><a href="{{ url('/lessons') }}">Lessons</a></li>
    <li><a href="{{ url('/prolessons') }}">Premium Lessons</a></li>
</ul>
[...]

We could have placed the route to access the premium lessons inside the route group with the subscribed middleware but that will let anyone access premium content regardless of whether they are subscribed or not. We will have to create another middleware to prevent basic users from accessing premium content:

php artisan make:middleware PremiumSubscription

The code going into the PremiumSubscription middleware won’t be so different from the one we had in the Subscribed middleware. The only difference is that we’ll have to be plan specific when checking the user’s subscription status. If the user is not subscribed to the premium plan, they’ll be redirected to the plans’ index page:

app/Http/Middleware/Premium.php

[...]
public function handle($request, Closure $next)
{
    if (!$request->user()->subscribed('premium', 'main')) {
        if ($request->ajax() || $request->wantsJson()) {
            return response('Unauthorized.', 401);
        }
        return redirect('/plans');
    }

    return $next($request);
}
[...]

Let’s register this new middleware in the Kernel before creating a new route group for premium users:

app/Http/Kernel.php

protected $routeMiddleware = [
    [...]
    'subscribed' => \App\Http\Middleware\Subscribed::class,
    'premium-subscribed' => \App\Http\Middleware\PremiumSubscription::class,
];

Then, we create the new route group just below the group with the subscribed middleware:

Route::group(['middleware' => 'auth'], function () {
    [...]
    Route::group(['middleware' => 'premium-subscribed'], function () {
        Route::get('/prolessons', 'LessonsController@premium');
    });
});

And that’s it. Anyone with a basic subscription cannot access premium content, but premium users can access both premium and basic content. Next, we’ll look at how to cancel and resume subscriptions.

Cancelling Subscriptions

To enable cancelling and resuming subscriptions, we’ll have to create a new page. Let’s open our SubscriptionsController and add an index action. It is inside the index action where we will return the view to manage subscriptions:

[...]
class SubscriptionController extends Controller
{
    public function index()
    {
        return view('subscriptions.index');
    }
    [...]
}

Let’s first create a subscriptions folder inside views before creating the index view. Then, we paste the snippet below inside the view:

resources/views/subscriptions/index.blade.html

@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">Manage Subscriptions</div>

                <div class="panel-body">
                    ...
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

We should then define a route pointing to this view. This route should be inside the route group with the subscribed middleware:

Route::group(['middleware' => 'subscribed'], function () {
    Route::get('/lessons', 'LessonsController@index');
    Route::get('/subscriptions', 'SubscriptionController@index');
});

Let’s update the dropdown in the navbar to include a link pointing to the index view we just created (place the link above the logout link). This link should only be visible to subscribed users:

resources/views/layouts/app.blade.html

<ul class="dropdown-menu" role="menu">
    [...]
    <li>
        @if (Auth::user()->subscribed('main'))
            <a href="{{ url('/subscriptions') }}">
                Manage subscriptions
            </a>
        @endif
    </li>
    [...]
</ul>

The next step is creating a link to enable users to cancel their subscriptions. For this, we’ll need a form. The form is important since we need to use CSRF protection to make sure the user who is cancelling the subscription is indeed the correct user. Let’s create the form inside the div with the class panel-body:

resources/views/subscriptions/index.blade.html

[...]
<div class="panel-body">
    @if (Auth::user()->subscription('main')->cancelled())
        <!-- Will create the form to resume a subscription later -->
    @else
        <p>You are currently subscribed to {{ Auth::user()->subscription('main')->braintree_plan }} plan</p>
        <form action="{{ url('subscription/cancel') }}" method="post">
            <button type="submit" class="btn btn-default">Cancel subscription</button>
            {{ csrf_field() }}
        </form>
    @endif
</div>
[...]

Notice how we have a conditional to check if the user has a cancelled subscription, and if that’s the case, we’ll leave them with the option to resume it. If the user still has an active subscription, that’s when the form to cancel will show up.

When a subscription is cancelled, Cashier will automatically set the ends_at column in our database. This column is used to know when the subscribed method should begin returning false. For example, if a customer cancels a subscription on March 1st, but the subscription was not scheduled to end until March 5th, the subscribed method will continue to return true until March 5th.

With the form to cancel subscriptions in place, we can now define the various controller actions and routes to handle the cancellation process.

In the SubscriptionsController, let’s add a cancel method:

[...]
class SubscriptionController extends Controller
{
    public function cancel(Request $request)
    {
        $request->user()->subscription('main')->cancel();

        return redirect()->back()->with('success', 'You have successfully cancelled your subscription');
    }
    [...]
}

Here, we are grabbing the user from the request object, getting their subscription, then calling Cashier’s cancel method on this subscription. After that, we redirect the user back to the same page but with a notification that the cancellation was successful.

We also need a route to handle the posting action of the form:

Route::group(['middleware' => 'subscribed'], function () {
    [...]
    Route::post('/subscription/cancel', 'SubscriptionController@cancel');
});

Resuming Subscriptions

The user must still be on their “grace period” in order to resume a subscription. If the user cancels a subscription and then resumes that subscription before the subscription has fully expired, they will not be billed immediately. Instead, their subscription will simply be re-activated, and they will be billed on the original billing cycle.

The procedure to resume a subscription is similar to what we did when cancelling a subscription. The only difference is we call the resume method on a subscription instead of the cancel method. We will also notify the user when the grace period is supposed to end on top of the form.

Let’s update our resources/views/subscriptions/index.blade.html view to include the form which lets users resume subscriptions:

<div class="panel-body">
    @if (Auth::user()->subscription('main')->cancelled())
        <p>Your subscription ends on {{ Auth::user()->subscription('main')->ends_at->format('dS M Y') }}</p>
        <form action="{{ url('subscription/resume') }}" method="post">
            <button type="submit" class="btn btn-default">Resume subscription</button>
            {{ csrf_field() }}
        </form>
    @else
    [...]

Then, we create the controller action and routes to handle the form’s posting action:

app/Http/controllers/SubscriptionsController.php

[...]
class SubscriptionController extends Controller
{
    [...]
    public function resume(Request $request)
    {
        $request->user()->subscription('main')->resume();

        return redirect()->back()->with('success', 'You have successfully resumed your subscription');
    }
    [...]
}

Let’s update our routes to accommodate the form’s posting action:

Route::group(['middleware' => 'subscribed'], function () {
    [...]
    Route::post('/subscription/cancel', 'SubscriptionController@cancel');
    Route::post('/subscription/resume', 'SubscriptionController@resume');
});

As you can see, Cashier makes it extremely easy to manage subscriptions. Next, we’ll look at how to set up webhooks for our application.

Webhooks

Both Stripe and Braintree can notify our application of a variety of events via webhooks. A webhook is a part of your application that can be called when an action happens on a third party service or some other server. In simpler terms, how does our application know if someone’s card was declined when we try to charge them for membership, or their card expires, or something else goes wrong? When that happens on Braintree, we want our application to know about it so we can cancel the subscription.

To handle Braintree webhooks, we define a route that points to Cashier’s webhook controller. This controller will handle all incoming webhook requests and dispatch them to the proper controller method:

Route::post(
    'braintree/webhooks',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);

Note that the route is not inside any route group as we don’t need to be authenticated for this.

By default, the WebhookController will automatically handle cancelling subscriptions that have too many failed charges (as defined by your Braintree settings); however, you can extend this controller to handle any webhook event you like. You can read more on how to extend the WebhookController here.

Once we have registered this route, let’s configure the webhook URL in the Braintree control panel settings. There are a variety of events we can choose to be notified about, but for this tutorial, we will just stick to cancelled:

Webhooks and CSRF Protection

Since Braintree webhooks need to bypass Laravel’s CSRF protection, we have to list the URI as an exception in our VerifyCsrfToken middleware or list the route outside of the web middleware group:

app/Http/Middleware/VerifyCsrfToken.php

protected $except = [
    'braintree/*',
];

Testing Webhooks

So far, we’ve been developing our application locally, and its not possible to access the application from another computer.

To test if the webhooks are working as intended, we’ll have to use something like Ngrok to serve the application.

Ngrok is a handy tool and service that allows us to tunnel requests from the wide open Internet to our local machine when it’s behind a NAT or firewall. Serving the app via Ngrok will also provide us with a URL which lets anyone access our app from another machine. Let’s update Braintree’s webhook URL to use this new URL:

We can test things out by manually cancelling a subscription for a user from Braintree’s control panel. Once we cancel the subscription, Braintree will send this data to the webhook we just created, then Cashier will handle the rest.

If we now look at the data in the subscriptions table, we notice the ends_at column for the user whose subscription we just cancelled has been updated.

Conclusion

We’ve come a long way to get to this point. Our app now supports subscription features found in most websites. We might not have exhausted all the methods offered by Cashier, but as you might have noticed, Cashier makes it extremely easy to manage Braintree subscriptions thereby saving developers a lot of time.

Cashier is not limited to Braintree as it also supports Stripe. If you think we’ve failed to cover some of the more important features, or have your own implementations to demonstrate, please let us know in the comments!

Frequently Asked Questions on Laravel and Braintree Middleware

What is the role of Middleware in Laravel?

Middleware in Laravel acts as a bridge between a request and a response. It is a type of filtering mechanism that is used for things like authentication, logging, session renewal, CSRF protection, and more. Middleware is executed before the HTTP request is passed onto the app (before the request hits the controller).

How does Braintree work with Laravel?

Braintree is a full-stack payment platform that makes it easy to accept payments in your app or website. When integrated with Laravel, it provides a seamless and secure payment process. Laravel provides a simple, expressive API to Braintree’s SDK, allowing you to handle complex billing flows with ease.

How can I implement advanced techniques in Laravel?

Implementing advanced techniques in Laravel involves understanding and using features like Middleware, Service Container, Service Providers, Contracts, and Facades. These features allow you to write clean, maintainable code and build scalable applications. You can also use Laravel’s robust ecosystem of packages to add functionality to your application.

What are some advanced Laravel topics I should learn?

Some advanced Laravel topics you should learn include Middleware, Service Container, Service Providers, Contracts, Facades, Queues, Events, and Broadcasting. Understanding these topics will help you build robust and scalable applications with Laravel.

What are some advanced features of Laravel I probably never knew about?

Laravel is packed with advanced features that you might not be aware of. These include its powerful ORM (Eloquent), task scheduling capabilities, real-time event broadcasting, and more. Laravel also has a robust ecosystem of packages that can add a wide range of functionality to your application.

How can I take my Laravel development skills to the next level?

To take your Laravel development skills to the next level, you should focus on understanding and using Laravel’s advanced features. This includes Middleware, Service Container, Service Providers, Contracts, Facades, Queues, Events, and Broadcasting. You should also familiarize yourself with Laravel’s ecosystem of packages.

How can I master advanced Laravel techniques?

Mastering advanced Laravel techniques involves a deep understanding of Laravel’s core concepts and features. This includes Middleware, Service Container, Service Providers, Contracts, Facades, Queues, Events, and Broadcasting. Practice is key – the more you use these features in your projects, the more comfortable you’ll become with them.

What are some resources for learning advanced Laravel topics?

There are many resources available for learning advanced Laravel topics. The official Laravel documentation is a great place to start. There are also many tutorials, blog posts, and online courses available that cover advanced Laravel topics in depth.

How can I integrate Braintree with Laravel?

Integrating Braintree with Laravel involves using Laravel’s expressive API to interact with Braintree’s SDK. This allows you to handle complex billing flows with ease. You’ll need to set up a Braintree account, install the Braintree SDK, and configure your environment variables.

What are some best practices for using Middleware in Laravel?

Some best practices for using Middleware in Laravel include keeping your middleware focused and small, using middleware for things like authentication and logging, and avoiding business logic in your middleware. It’s also important to understand the order in which middleware is executed.