I recently created my first subscription-based SaaS service, a form to PDF service for users of my plugin Caldera Forms. I built the application using the PHP framework Laravel, making use of their Cashier package for the subscription billing.
In this article, I want to show you how I did this on a technical level. SaaS options are becoming more and more prominent in the WordPress community, and the only tutorial I could find out there was using Stripe, not BrainTree. Those were useful, but I wanted to use Braintree as it offers both credit card and PayPal. I figured it out and want to share what I learned. This will be useful to those trying this package for the first time in Laravel and for WordPress developers to compare.
Next week I will follow up with a look at the difference between how an eCommerce package for Laravel compares to one for WordPress, something that has provided me with a new perspective on WordPress.
But first, let’s dive into the basics of Laravel Cashier.
Why Laravel?
I’ve spent a lot of time learning Laravel recently and I’ve built a few things with it. The Caldera Forms PDF app is the first project I finished and felt like was worth releasing. As someone who understands object-oriented PHP really well, I find Laravel a lot easier to develop PHP web applications with than WordPress.
I would not recommend Laravel to a beginner. But if, like me, you’ve worked a lot with PHP, but get frustrated with trying to make WordPress act like an object-oriented application or use the MVC pattern, which it’s not, try Laravel. It’s a lot of fun to work with.
This article is not an introduction to Laravel. It’s a well-documented framework, and the Laracasts site is a great resource for learning it. If you’re not familiar with Laravel, just keep in mind as you read this that it is and MVC framework, and creating routes, controllers and views should make conceptual sense. You can dig into the documentation to understand more before trying it for yourself.
Making It Work
Before You Begin
Before we work on implementing Cashier, you will need to install and configure Cashier for Braintree, as well as create a Braintree account. The documentation is pretty good. Make sure to update your environment variable, User model, and the AppServiceProvider, and other configurations.
Once everything is setup, according to the docs. You can start on adding the routes, controller, and view, I will walk through that as I found that part a lot less clear.
Setting Up Your Controller
Next, you will need a controller that can display the subscription form and process requests for new subscriptions. I also added a method to my controller for listing subscriptions.
To make a new controller, create a class in your controllers directory that extends the controller called “Subscription.” You can do this using artisan, by typing this command in your terminal:
php artisan make:controller Subscription
Now, add a method to this controller called “form.” We will use this to show the subscription form. It will be pretty simple:
<?php use Braintree\ClientToken; class Subscription extends Controller { public function form() { $clientToken = ClientToken::generate(); return view('subscription.join', ['clientToken' => $clientToken ]); } }
All this does is generate a token using the Braintree API we can use for the payment and then return a view, while passing that token to the view. If you’re not familiar with Laravel, just keep in mind that view( ‘subscription,join’ ) will return the blade file in the resources/views/subscriptions directory called “join.blade.php”. The second argument passes data to that view.
So, the next step is to create that view. Add “join.blade.php” to the “subscriptions” sub directory, which you will need to make, in your views directory. Open that file. In there we will need to make the form for creating subscriptions.
I used the form package from the Laravel collective for some parts of my forms in this app. But that is optional. You could just use standard HTML tags. Just keep in mind that, instead of inputs for the credit card fields, you should use standard elements, BrainTree will replace those with their hosted fields, which is more secure.
Here is how I generated my signup form:
<?php use \Illuminate\Support\Facades\Input; ?> @extends('layouts.app') <style> a#braintree-paypal-button {margin-top: 48px;} </style> @section('content') <div class="container"> @if( Input::get( 'message' ) ) @if( Input::get( 'success', false ) ) <div class="alert alert-success"> @else <div class="alert alert-danger"> @endif <?php echo urldecode( Input::get( 'message' ) ); ?> </div> @endif {!! Form::open(['url' => 'subscription', 'method' => 'post', 'id' => 'checkout'])!!} <div class="row"> <label for="plan"> Choose Plan </label> </div> <div class="row"> <div class="col-md-9 col-sm-12"> <select name="plan" id="plan" class="form-control"> <option value="1" data-price="5.00"> Option 1 </option> <option value="2" date-price="10.00" <?php if( 'two' == Input::get( 'plan', false ) ) : echo 'selected'; endif; ?> > Option 2 </option> </select> </div> </div> <div class="row"> <div id="paypal" class="col-sm-12 col-md-6" aria-live="assertive" style=""> </div> <a id="cc" class="col-sm-12 col-md-6 btn btn-secondary btn-green" href="" title="Click to pay by Credit Card"> Pay By Credit Card </a> </div> <div id="cc-info" style="display: none;visibility: hidden" aria-hidden="true"> <div class="form-group"> <label for="number"> Credit Card Number </label> <div id="number" class="form-control"></div> </div> <div class="row"> <div class="col-md-3 col-sm-12"> <div class="form-group"> <label for="expiration-date"> Expiration Date </label> <div id="expiration-date" class="form-control"></div> </div> </div> <div class="col-md-3 col-sm-12"> <div class="form-group"> <label for="cvv"> Secret Code (CVV) </label> <div id="cvv" class="form-control"></div> </div> </div> <div class="col-md-3 col-sm-12"> <div class="form-group"> <label> Postal Code </label> <div id="postal-code" class="form-control"></div> </div> </div> </div> <div class="row"> <input type="submit" value="Sign Up" class="btn-primary btn-orange col-sm-12" /> </div> </div> {!! Form::close() !!} </div> <script> //We will add braintreeJS here </script> @endsection
Notice that I created an empty element for the pay by PayPal button next to my pay by credit card button. I also made my credit form hidden, for two reasons. The first is that is that it takes a second or so for Braintree’s JavaScript to set up the form properly. The other is that there is no need to show it until someone clicks that “pay with credit card” button.
Now, we need to setup the JavaScript for Braintree, including showing the credit card section of the form. In the code I showed above, there was a script tag with nothing in it. Here is what to put there:
<script src="https://js.braintreegateway.com/v2/braintree.js"></script> <script> $( '#cc' ).on( 'click', function(e) { $( '#cc-info' ).show().attr( 'aria-hidden', true ).css( 'visibility', 'visible' ); }); var url = "<?php echo url('subscription' ); ?>"; var paypalEl = document.getElementById( 'paypal' ); braintree.setup("{{ $clientToken }}", "custom", { paypal: { container: paypalEl, singleUse: false, billingAgreementDescription: 'Your agreement description', locale: 'en_us', enableShippingAddress: false, button: { type: 'checkout' } }, dataCollector: { paypal: true }, onPaymentMethodReceived: function (obj) { $.ajax({ method: 'post', url: url, data: { payment_method_nonce: obj.nonce, plan: $( '#plan').val() }, headers: { 'X-CSRF-Token': $('input[name="_token"]').val() } }).success( function( r ){ window.location = url; }).error( function( r ){ alert( 'Error' ); } ); } } ); braintree.setup("{{ $clientToken }}", "custom", { id: "checkout", hostedFields: { number: { selector: "#number" }, postalCode: { selector: '#postal-code' }, expirationDate: { selector: "#expiration-date", placeholder: "00/00" }, cvv: { selector: "#cvv" } } } ); </script> Raw subscription-simplified.php
After handling showing and hiding the credit card details, this JavaScript sets up Braintree twice. Once for credit card and once for Paypal. In both cases, I use the PHP variable $clientToken, which was passed into the view using the view() function.
The first usage of Braintree.setup creates the PayPal system. This adds the Pay with PayPal button to the DOM, in the empty element I added to the page. When that button is clicked, a popup to complete the transaction will be opened. I used an AJAX call after the payment is complete. That goes to the same URL that the credit card form goes to.
The second usage of Braintree.setup() sets up the credit card form. This is pretty simple, just note that I am passing the IDs of the div elements I created instead of input elements. Braintree replaces those with proper inputs, using their secure system.
Handling Payments
In both cases, all that is getting POSTed back to the server is the payment nonce and the plan ID. On the server-side, we will need to verify the payment nonce and then create the subscription.
So, let’s add a method to the controller for handling POST requests to join. This will need to take the payment nonce and plan info, verify it with Braintree and then save the subscription for the current user. Note that the subscription logic is on the main User model for the app. This works if you’ve added the billable trait to the User model as shown in the setup.
Here is the update Subscription controller with the join method:
<?php use App\User; use Braintree\ClientToken; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; class Subscription extends Controller { public function form() { $clientToken = ClientToken::generate(); return view('subscription-join', ['clientToken' => $clientToken ]); } public function join() { //check that we have nonce and plan in the incoming HTTP request if( empty( Input::get( 'payment_method_nonce' ) ) || empty( Input::get( 'plan' ) ) ){ return redirect( '/subscription?success=false&message=' . urlencode( 'Invalid request' ), 400 ); } //set user $user = Auth::user(); try { //Try to create subscription $subscription = $user->newSubscription( 'main', Input::get( 'plan' ) )->create( Input::get( 'payment_method_nonce' ), [ 'email' => $user->email ] ); } catch ( \ Exception $e ) { //get message from caught error $message = $e->getMessage(); //send back error message to view return redirect( '/subscription/join?success=false&message=' . urlencode( $message ) ); } //Go to subscription manage view beacuse all is well return redirect( '/subscription/manage?success=true' ); } }
Most of this is validation and error handling. The actual plan creation, looks like this:
<?php $subscription = $user->newSubscription( 'main', Input::get( 'plan' ) )->create( Input::get( 'payment_method_nonce' ), [ 'email' => $user->email ] );
That’s it. Very simple.
Routing
You might have noticed I just created a controller for two types of requests without creating any routes. Let’s go ahead and add a route group for 4 routes. The first two, for the form and processing the form we already have methods for. We’ll also add routes for a manage view and a processing cancellations.
Go to app/routes/web.php and add a route group like this:
<?php Route::group( [ 'middleware' => 'auth' ], function () { //create subscription Route::get( '/subscription/join', 'Subscription@index' ); //process join form Route::post( '/subscription', 'Subscription@join' ); //View subscriptions Route::get( '/subscription/manage', 'Subscription@manage' ); //cancel subscription Route::get( '/subscription/cancel', 'Subscription@cancel' ); });
Canceling Subscriptions
The logic for canceling subscriptions is pretty simple. In the last step, we added a route that you can create links to. The controller just needs a cancel method to handle those:
<?php public function cancel() { $user = Auth::user(); $subscription = $useruser->subscription('main')->cancel(); return redirect( '/subscription/manage?success=true' ); }
This uses the cancel() method of the User model — really the cancel method of the billable trait to cancel the subscription using the Braintree SDK.
Displaying Subscriptions
The last route to add is the manage screen. This will take a new method in the controller called manage. Here is how you can get the current user’s subscription and return a view called manage from the subscriptions view directory:
<?php public function manage() { $user = Auth::user(); $subscriptions = $user->getSubscription(); return view('subscription-manage', ['subscriptions' => $subscriptions, ]); }
That collects all subscriptions and returns them to the view. In the view, we can check if the $subscriptions variable is empty. If so, simply display a “You have no subscriptions” message. If not, we can loop through them. Here is what the view looks like, complete with cancellation link:
<?php use App\User; use Braintree\ClientToken; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; class Subscription extends Controller { public function form() { $clientToken = ClientToken::generate(); return view('subscription-join', ['clientToken' => $clientToken ]); } public function join() { //check that we have nonce and plan in the incoming HTTP request if( empty( Input::get( 'payment_method_nonce' ) ) || empty( Input::get( 'plan' ) ) ){ return redirect( '/subscription?success=false&message=' . urlencode( 'Invalid request' ), 400 ); } //set user $user = Auth::user(); try { //Try to create subscription $subscription = $user->newSubscription( 'main', Input::get( 'plan' ) )->create( Input::get( 'payment_method_nonce' ), [ 'email' => $user->email ] ); } catch ( \ Exception $e ) { //get message from caught error $message = $e->getMessage(); //send back error message to view return redirect( '/subscription/join?success=false&message=' . urlencode( $message ) ); } //Go to subscription manage view beacuse all is well return redirect( '/subscription/manage?success=true' ); } public function cancel() { $user = Auth::user(); $subscription = $useruser->subscription('main')->cancel(); return redirect( '/subscription/manage?success=true' ); } public function manage() { $user = Auth::user(); $subscriptions = $user->getSubscription(); return view('subscription-manage', ['subscriptions' => $subscriptions, ]); } } Raw
Note that I am using the Laravel Collective HTML package, which needs to be installed separately, but is a shortcut and not necessary.
That’s The Basics
With this system, you can create recurring subscriptions to a site using PayPal or credit card. Cashier also has good support for consuming Braintree (or Stripe) webhooks and displaying invoices.
It’s a cool system that I will be playing with more. Using Laravel has gotten me thinking in new ways about building PHP web apps and also about WordPress eCommerce in general. Next week, I will share some of those thoughts, so please subscribe to my Torque posts using the form below or come back next week.
1 Comment