Creating a Subscription-Based Website with Laravel and Recurly, Part 1

Lukas White
Share

Scheduling and processing payments is easy, but don’t think recurring payments are trivial. Things can get complicated very quickly. You need to decide how to handle failures (how many failed payment attempts does someone get?), billing details must be kept up to date, and upgrading and downgrading plans can introduce all sorts of issues. Then there’s perhaps the most significant issue with recurring payments – you need to keep your visitors’ payment details on file. This introduces security, compliance, and legal issues.

Luckily, Recurly is a service that handles most of the issues around taking payments, as well as processing recurring payments from there on out. Using its JavaScript libraries, you can create secure forms for submitting and updating billing information with Recurly – including the all-important credit card details – which submit the information securely, independent from your application.

Once you’ve set up the amount and frequency of payments, the service takes care of scheduling and taking payments at regular intervals until the customer decides to cancel. Recurly also handles plan changes, calculating and making additional charges or refunds.

In this two-part series I’ll show you step-by-step how to create a paid subscription-based membership website using Laravel, a PHP5-based framework, and the Recurly payment processing service. First we’ll start by creating a simple site with basic registration, authentication, and user roles and permissions. Then we’ll add payment processing and tie it into the registration process, allowing people to purchase different membership tiers.

Setting Up the Application

Start by creating a new project folder and run the following command:

composer create-project laravel/laravel recurly --prefer-dist

This creates a new Laravel project using Composer, which downloads the framework and its dependencies.

We also need some additional libraries for later, so add the following in the require section of composer.json and run composer.phar update.

"machuga/authority": "dev-develop",
"machuga/authority-l4" : "dev-master",
"recurly/recurly-client": "2.1.*@dev"

This downloads the Authority library, which we’ll use for user roles and permissions, and the Recurly client library.

Next, configure the database connection by specifying the appropriate schema name, username, password and hostname/address in app/config/database.php. Laravel works out-of-the-box with a number of databases, such as MySQL, Postgres, SQLite, and SQL Server.

We also need a users table. Rather than set that up by hand, Laravel provides migrations, a means to create and modify database tables programmatically. Run the following command:

php artisan migrate:make create_users_table

This creates the migration file in the app/database/migrations folder. The file is created with a basic outline, but let’s flesh it out with our table definition:

public function up()
{
    Schema::create('users', function($table) {
        $table->increments('id');
        $table->string('email')->unique();
        $table->string('name');
        $table->string('password');
        $table->timestamps();
    });
}

public function down()
{
    Schema::drop('users');
}

Go back to the command line and run:

php artisan migrate

Look in the database and you should find that Laravel has created the users table for you.

Setting up Authority for Roles and Permissions

To determine the type of account a user has and privileges it grants them, we’ll assign users to a role. We’ve already downloaded Authority via Composer to help with this; we just need to carry out a few more steps to configure it fully.

In app/config/app.php, add the following line to the providers:

'Authority\AuthorityL4\AuthorityL4ServiceProvider',

Add the following to the aliases:

'Authority' => 'Authority\AuthorityL4\Facades\Authority',

And publish the Authority configuration file:

php artisan config:publish machuga/authority-l4

We’ll return to the configuration file later. For now, we need to create some additional database tables. Fortunately, the package contains its own migrations for this. Run them with the following command:

php artisan migrate --package="machuga/authority-l4"

You’ll find you have three additional tables: permissions, roles and role_user.

We also need to create models to represent roles and permissions. We’ll keep them simple for now. In app/models/Role.php:

<?php
class Role extends Eloquent
{
}

And in app/models/Permission.php:

<?php
class Permission extends Eloquent 
{
}

Now we need to modify the User model class – which has already been created – to associate a user with roles and permissions. Add the following lines to app/models/User.php:

public function roles() {
    return $this->belongsToMany('Role');
}

public function permissions() {
    return $this->hasMany('Permission');
}

public function hasRole($key) {
    foreach($this->roles as $role){
        if ($role->name === $key) {
            return true;
        }
    }
    return false;
}

Now let’s pre-populate the database with some data. Open app/database/seeds/DatabaseSeeder.php and paste in the following:

<?php
class DatabaseSeeder extends Seeder
{
    public function run() {
        Eloquent::unguard();
        $this->call('UserTableSeeder');
        $this->command->info('User table seeded!');
        $this->call('RoleTableSeeder');
        $this->command->info('Role table seeded!');
    }
}

class UserTableSeeder extends Seeder
{
    public function run() {
        DB::table('users')->delete();
        User::create(array(
            'email'    => 'joe.bloggs@example.com',
            'name'     => 'Joe Bloggs',
            'password' => Hash::make('password')
        ));
    }

}

class RoleTableSeeder extends Seeder
{
    public function run() {
        DB::table('roles')->delete();
        Role::create(array('name' => 'admin'));
        Role::create(array('name' => 'pending'));
        Role::create(array('name' => 'member'));
        Role::create(array('name' => 'bronze'));
        Role::create(array('name' => 'silver'));
        Role::create(array('name' => 'gold'));
    }
}

Then seed the database by running:

php artisan db:seed

Creating a Layout

Now we’ll create the overall page layout. Download Twitter Bootstrap and put the source files in the public folder – moving the js files to public/js/libs.

Create the file app/views/layouts/default.blade.php with the following contents:

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Subscription Site Tutorial</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="">
  <meta name="author" content="">

  <!-- Le styles -->
  <link href="/css/bootstrap.css" rel="stylesheet">  
  <link href="/css/style.css" rel="stylesheet"> 
 </head>

 <body>
  <div class="navbar navbar-inverse navbar-fixed-top">
   <div class="navbar-inner">
    <div class="container">
     <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
     </button>
     <a class="brand" href="#">Subscription Site Tutorial</a>
     <div class="nav-collapse collapse">
      <ul class="nav">
       <li class="active"><a href="#">Home</a></li>
       <li><a href="#about">About</a></li>
       <li><a href="#contact">Contact</a></li>
      </ul>
     </div><!--/.nav-collapse -->
    </div>
   </div>
  </div>

  <div class="container">

    @if(Session::has('success'))
    <div class="alert alert-success">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      {{ Session::get('success') }}
    </div>    
    @endif

    @if(Session::has('error'))
    <div class="alert alert-error">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      {{ Session::get('error') }}
    </div>    
    @endif

    @yield('content')

  </div> <!-- /container -->

  <!-- Le javascript
  ================================================== -->
  <!-- Placed at the end of the document so the pages load faster -->
  <script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
  <script src="/js/libs/bootstrap.min.js"></script>
 </body>
</html>

It’s pretty basic stuff – our page content is outputted in the line that reads @yield('content').

Next, create the file public/css/style.css:

body {
    padding-top: 50px;
}

p.logged-in {
    color: white;
    margin-top: 0.5em;
}

And finally, let’s create a basic home page for the application. Create the file app/views/home/index.blade.php:

@extends('layouts.default')
@section('content')
  <h1>Subscription Site Tutorial</h1>
@stop

The @extends declaration tells Laravel to use the default layout we just created with the content we’re injecting into the content area via the @yield command wrapped in the @section declaration.

Don’t forget to change the default route accordingly in app/routes.php:

Route::get('/', function() {
	return View::make('home/index');
});

Building the Login Mechanism

We’ve got our users – and an account to start off with – so now we need the ability to log in. In app/routes.php, add a route to the login page:

Route::get('/auth/login', function() {
    return View::make('auth/login');
});

Now create the view, app/views/auth/login.blade.php. The .blade.php extension indicates that we’re going to use the Blade templating library which ships with Laravel, which is cleaner than straight PHP. Behind the scenes, these template files are compiled into PHP as required.

@extends('layouts.default')
@section('content')
  <h1>Please Log in</h1>
  {{ Form::open(array('url' => 'auth/login')) }}
  {{ Form::label('email', 'E-Mail Address') }}
  {{ Form::text('email') }}
  {{ Form::label('password', 'Password') }}
  {{ Form::password('password') }}
  <div class="form-actions">
  {{ Form::submit('Login', array('class' => 'btn btn-primary')) }}
  </div>
  {{ Form::close() }}
  <p>Not a member?  <a href="/user/register">Register here</a>.</p>
@stop

If you browse to /auth/login in your browser you should see a simple login form.

To process the login, we’ll need to build a POST route. Authentication in Laravel is a breeze; we simply do this:

Route::post('/auth/login', function() {
    $email = Input::get('email');
    $password = Input::get('password');

    if (Auth::attempt(array('email' => $email, 'password' => $password))) {
        return Redirect::to('/')->with('success', 'You have been logged in');
    }
    else {
        return Redirect::to('auth/login')->with('error', 'Login Failed');
    }

    return View::make('auth/login');
});

All the magic happens in Auth::attempt(); if the login is successful, a session is created and a populated instance of the User object is accessible via the static method Auth::user().

The logout method is equally straightforward:

Route::get('/auth/logout', function() {
    Auth::logout();
    return Redirect::to('/')->with('success', 'You have successfully logged out');
});

Basic Registration

Our last task in this part is to set up a basic registration process. Create a registration route in app/routes.php:

Route::get('/user/register', function() {
    return View::make('user/register/index');
});

Now create the view app/views/user/register/index.blade.php:

@extends('layouts.default')
@section('content')
  {{ Form::open(array('url' => 'user/register')) }}
        
  {{ Form::label('email', 'E-Mail Address') }}
  {{ Form::text('email') }}
  {{ $errors->first('email') }}

  {{ Form::label('name', 'Your name') }}
  {{ Form::text('name') }}
  {{ $errors->first('name') }}

  {{ Form::label('password', 'Password') }}
  {{ Form::password('password') }}
  {{ $errors->first('password') }}

  {{ Form::label('password_confirmation', 'Repeat') }}
  {{ Form::password('password_confirmation') }}

  <div class="form-actions">
  {{ Form::submit('Register', array('class' => 'btn btn-primary')) }}
  </div>
 
  {{ Form::close() }}
@stop

There are a couple of things to note here:

  • The $errors object is automatically passed to the view, regardless of whether there are any errors in the submission. If the field’s okay, it’ll just print nothing.
  • By appending _confirmation to the name of a field, it’s simple to validate the two fields together; i.e. confirm that the user has re-entered the chosen password correctly.

Now let’s implement the POST action:

Route::post('/user/register', function() {
    $validator = Validator::make(
        Input::all(),
        array(
            'name' => array('required', 'min:5'),
            'email' => array('required', 'email', 'unique:users'),
            'password' => array('required', 'confirmed')
        )
    );

    if ($validator->passes()) {
        $user = new User();
        $user->name     = Input::get('name');
        $user->email    = Input::get('email');
        $user->password = Hash::make(Input::get('password'));
        $user->save();

        $role_pending = Role::where('name', '=', 'pending')->first();
        $user->roles()->attach($role_pending);

        Auth::login($user);
        return Redirect::to('/')->with(
            'success',
            'Welcome to the site, . Auth::user()->name . '!'
        );
    }
    else {
        return Redirect::to('user/register')->with(
            'error',
            'Please correct the following errors:'
        )->withErrors($validator);
    }
});

This is all fairly basic stuff – we create a validator, passing it the POST variables with Input::all() and a set of validation rules. If validation passes, we create a new user, assign them to the pending role, log them in, and then redirect them to the front page.

If validation fails, we redirect back to the form, create an error flash message, and pass the error messages from the validator – they’ll then be available in the view in the $errors variable.

Conclusion

In this part we’ve gone step-by-step through building the bare bones of a subscription site. It has the ability to register a basic account, login, and logout. In the next part we’ll integrate Recurly for paid subscription plans.