cult3

Creating a basic Invitation system with Laravel 4

Apr 21, 2014

Table of contents:

  1. How I want my invitation process to work
  2. Creating the database migration
  3. The Invite model
  4. The Invite Repository
  5. Defining the routes
  6. The signup form
  7. The Invite Controller
  8. The Registration Process
  9. Conclusion

A common requirement of new consumer web applications is the ability to restrict new registration requests behind an invitation process. This could be for the “velvet rope” experience or simply because you want to limit the initial users for testing.

Building an invitation process is not really that difficult because all of the individual components are basically already made available to us.

However, I think if you are approaching building something like this for the first time it can be useful to see how someone else would implement it.

With that in mind, here is how I’m going to implement an invitation system for Cribbb.

How I want my invitation process to work

Whenever I’m creating a feature such as this invitation process, I find it really beneficial to write out exactly how I want the functionality to work in prose before I actually write a single line of code. By getting the actual process clear in my head first I find that I save a lot of back and forth when I’m implementing it.

I want my invitation process to work in the following ways:

New users can sign up via email I want to have an input box on the homepage to allow interested traffic to submit their email address and be placed into a queue to receive an invite.

Existing users can invite new users I want existing users to be assigned invitations that they can send out to their friends to invite them into the application.

Queue jump When a user signs up for an invitation they should be able to jump the queue by sharing their unique link to invite more people, therefore giving an incentive to share on Twitter and Facebook.

A user family tree I want to be able to see how new users were added by existing users through invitations.

This is quite a lot to build and so I’ll be breaking up the processes over a couple of weeks. This week I will be looking at establishing the foundation of my invitation system.

Creating the database migration

So the first thing to do is to create the database migration for the invites table:

php artisan migrate:make create_invites_table

If you are new to migrations, take a look at this tutorial.

To start with I’m going to be keeping my invites table pretty simple:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateInvitesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create("invites", function (Blueprint $table) {
            $table->increments("id");
            $table->string("code");
            $table->string("email");
            $table->timestamp("claimed_at")->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop("invites");
    }
}

As you can see, I will require string fields for the invite code and to record the user’s email. I will also set a timestamp column to recored when the invitation was claimed. By default this field is nullable so I will be easily able to query if a invitation is unclaimed.

The Invite model

Next I will need to create an Invite model for the table:

class Invite extends Eloquent
{
    /**
     * Properties that can be mass assigned
     *
     * @var array
     */
    protected $fillable = ["code", "email"];
}

As you can see, this is really very simple to begin with. So far all I’m doing is defining that the code and the email properties can be mass assigned.

In the coming weeks I will remove the code property from the $fillable array and have the invitation automatically generate the code on creation. However, for now, this will get the job done.

The Invite Repository

Like all of the other models in Cribbb, the invite model will have an associated repository to aid testing. The first thing to do is to create the InviteRepository interface that will define the public api of this repository:

<?php namespace Cribbb\Repositories\Invite;

interface InviteRepository
{
    /**
     * Find a valid invite by a code
     *
     * @param string $code
     * @return Illuminate\Database\Eloquent\Model
     */
    public function getValidInviteByCode($code);
}

At the minute I will only require a single method to return valid invites by their code. It’s always better to start simple rather than try to cover every eventuality so this will do for now.

Next I will create the Eloquent implementation of this repository:

<?php namespace Cribbb\Repositories\Invite;

use Cribbb\Repositories\Crudable;
use Illuminate\Support\MessageBag;
use Cribbb\Repositories\Repository;
use Illuminate\Database\Eloquent\Model;
use Cribbb\Repositories\AbstractRepository;

class EloquentInviteRepository extends AbstractRepository implements
    Repository,
    Crudable,
    InviteRepository
{
    /**
     * @var Illuminate\Database\Eloquent\Model
     */
    protected $model;

    /**
     * Construct
     *
     * @param Illuminate\Database\Eloquent\Model $user
     */
    public function __construct(Model $model)
    {
        parent::__construct(new MessageBag());

        $this->model = $model;
    }

    /**
     * Find a valid invite by a code
     *
     * @param string $code
     * @return Illuminate\Database\Eloquent\Model
     */
    public function getValidInviteByCode($code)
    {
        return $this->model
            ->where("code", "=", $code)
            ->where("claimed_at", "=", null)
            ->first();
    }

    /**
     * Create
     *
     * @param array $data
     * @return Illuminate\Database\Eloquent\Model
     */
    public function create(array $data)
    {
        $data["code"] = bin2hex(openssl_random_pseudo_bytes(16));

        return $this->model->create($data);
    }

    /**
     * Update
     *
     * @param array $data
     * @return Illuminate\Database\Eloquent\Model
     */
    public function update(array $data)
    {
    }

    /**
     * Delete
     *
     * @param int $id
     * @return boolean
     */
    public function delete($id)
    {
    }
}

This repository extends my AbstractRepository and implements a couple of interfaces such as Repository and Crudable. Here I’m basically just inheriting some common methods that appear on many different repositories. Take a look at Eloquent tricks for better Repositories for more on this pattern.

I also need to satisfy the InviteRepository interface and so I will implement the getValidInviteByCode() method like this:

/**
 * Find a valid invite by a code
 *
 * @param string $code
 * @return Illuminate\Database\Eloquent\Model
 */
public function getValidInviteByCode($code)
{
    return $this->model->where('code', '=', $code)
        ->where('claimed_at', '=', null)
        ->first();
}

This method will simply return an invitation by it’s code but only when it is still valid.

Finally I need to implement how an invitation should be created:

/**
 * Create
 *
 * @param array $data
 * @return Illuminate\Database\Eloquent\Model
 */
public function create(array $data)
{
    $data['code'] = bin2hex(openssl_random_pseudo_bytes(16));

    return $this->model->create($data);
}

In this method I’m using the openssl_random_pseudo_bytes function to generate a string of pseudo-random bytes and the bin2hex function to convert the string into it’s hexadecimal representation.

This seems like a good solution to generating invitation codes, but I’m open to suggestions if you have any? Leave a comment with your thoughts.

For now I’m just going to leave the update() and delete() methods as they’re not very important at this stage.

Now that I’ve got the interface and the implementation for the Invite repository, I need to bind them together in the Service Provider:

$this->app->bind("Cribbb\Repositories\Invite\InviteRepository", function (
    $app
) {
    return new EloquentInviteRepository(new Invite());
});

Defining the routes

For this initial implementation, I will need to define a home route that will hold the invitation signup form, and a POST route that will accept the new invitation request when the user submits the signup form:

/**
 * Home
 *
 * The root of the application. This will either be
 * the index page or the user's dashboard
 */
Route::get("/", [
    "uses" => "HomeController@index",
    "as" => "home.index",
]);

/**
 * Invite
 *
 * The route to create a new Invite
 */
Route::post("invite", [
    "uses" => "InviteController@store",
    "as" => "invite.store",
]);

I’m creating the invite.store route as a method on a dedicated InviteController. I think it is much better to create an invite controller to manage create, finding, updating and deleting invitations, rather than having those methods as part of an already established controller.

The signup form

On the home page of Cribbb I’m going to need a form that the user can submit in order to register for an invitation. Here is a really simple version of a form to lay the foundation:

{{ Form::open(array('route' => 'invite.store')) }}

<div>
{{ Form::label('email', 'Email Address') }}
{{ Form::text('email') }}
</div>

{{ Form::submit('Submit') }}

{{ Form::close() }}

All this form really needs to do is accept an email address and POST to the correct method.

The Invite Controller

Next I need to create the InviteController class that will hold the method to create new invitations:

use Cribbb\Repositories\Invite\InviteRepository;

class InviteController extends BaseController
{
    /**
     * InviteRepository
     *
     * @var Cribbb\Repositories\Invite\InviteRepository
     */
    protected $repository;

    /**
     * Create a new instance of the InviteController
     *
     * @param Cribbb\Repositories\Invite\InviteRepository
     */
    public function __construct(InviteRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * Create a new invite
     *
     * @return Response
     */
    public function store()
    {
        $invite = $this->repository->create(Input::all());
    }
}

This might seem a little bit redundant to have a controller with a single method, but I think it is much better to have a lot of very simple controllers, rather than really monolithic controllers that are difficult to understand.

This controller is a really basic example of the type of controllers that I’ve covered a couple of times in this series. First I inject the repository into the controller through the __construct() method.

Next, In the store() method I simply pass the POST input to the create() method on the repository. At the minute there is no validation so a user could sign up multiple times. I’ll not worry about validation right now, as I’ll be looking at that next week.

The Registration Process

Finally I need to create the registration process that new users will follow to sign up to Cribbb.

First I will define the initial route:

/**
 * Register
 *
 * Sign up as a new user
 */
Route::get("register", [
    "uses" => "RegisterController@index",
    "as" => "register.index",
]);

Next I will create the controller:

use Cribbb\Repositories\Invite\InviteRepository;

class RegisterController extends BaseController {

    public function __construct(InviteRepository $invite)
    {
        $this->invite = $invite;
    }

    public function index()
    {
        $invite = $this->invite->getValidInviteByCode(Input::get('code', ""));

        if ($invite) {
            return View::make('register.signup');
        }

        return View::make('register.invite');
    }
}

In this controller I once again inject the InviteRepository. In the index method I will first check to see if the code that has been supplied in the GET request is valid.

If the $invite is valid I will return the view to sign up as a new user. If the invite was not valid, I can prompt the user to sign up for an invitation.

This route check would probably be better handled in a filter, but for this rough implementation this will be do fine. In one of the next tutorials I will write a proper implementation for filtering for only valid invitations.

Conclusion

In this tutorial I’ve looked at the basic implementation of setting up an invite process. The foundation for building a system can be as simple or as complicated as you want. Even with this very rough start, you’ve got the most important bits in place to build an invitation system for your application.

However as I mentioned throughout the post, there’s still a lot that can be done to improve it. I’m a big believer in getting something rough working before you try to build something really complicated. By taking it one step at a time, we can write code that meets the requirements and is also easier to work with and is maintainable.

Next week I will be building on this foundation to create a much more advanced invitation system that you can use in your applications.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.