cult3

Creating an advanced Invitation System in Laravel 4 Part 1

Apr 28, 2014

Table of contents:

  1. Automatically generating the invitation code
  2. Validation through a service class
  3. Injecting the service class into the Controller
  4. A filter to protect the registration route
  5. Conclusion

Last week I looked at setting up a basic invitation system in Laravel 4. I looked at creating the invites table, the model and the repository and I showed you how to very easily create a new invitation and check for valid invitations when a new user tries to sign up for your application.

This basic foundation has put the right components together, but there is still quite a bit missing from the system to make it robust and capable of meeting all of my requirements.

In this week’s tutorial I’m going to look at building on top of last week’s foundation. In today’s tutorial I’ll be looking at:

  1. Automatically generating the invitation code
  2. Validation through a service class
  3. A filter to protect the registration route

Automatically generating the invitation code

The first thing I will do is to clean up how the invitation code is generated. You might remember from last time that I was creating the code in the create() method of the EloquentInviteRepository. This did the job but it wasn’t ideal.

The invitation code should be generated only when the new invitation is created. This means it will never have to be updated so we don’t need to pollute any of the other code with references to it. Instead, the code should just be generated for us.

Fortunately, Eloquent fires events that allow you to hook on to the various stages of the model’s lifecycle. This means we can write code that should be run based upon what the model is doing.

You can read more about the Eloquent’s Model Events and what you have available to you in the documentation.

In this situation, I want to hook on to the creating event to automatically generate the invitation code before the model is saved.

The first thing to do is to remove the code from the fillable array in the Invite model. This will prevent the code from ever being mass-assigned:

/**
 * Properties that can be mass assigned
 *
 * @var array
 */
protected $fillable = array('email');

Next I need to override the static boot method to register my event:

/**
 * Register the model events
 *
 * @return void
 */
protected static function boot()
{
    parent::boot();

    static::creating(function($model) {
        $model->generateInvitationCode();
    });
}

First you need to call the boot method on the parent Model class.

Next you can call the event method and pass in a closure. In this example, I’m calling the creating method.

The closure accepts an instance of the $model. I will then call the generateInvitationCode() method so I don’t end up with lots of code in this closure.

Finally, my generateInvitationCode() looks like this:

/**
 * Generate an invitation code
 *
 * @return void
 */
protected function generateInvitationCode()
{
    $this->code = bin2hex(openssl_random_pseudo_bytes(16));
}

As you can see, I simply set the randomly generated code to the code property. There’s no need to call the save() method as this will automatically called as part of the model’s lifecycle.

Now I can clean up the create() method on the EloquentInviteRepository as the code will be automatically generated:

/**
 * Create
 *
 * @param array $data
 * @return Illuminate\Database\Eloquent\Model
 */
public function create(array $data)
{
    return $this->model->create($data);
}

Validation through a service class

One of the problems of the implementation from last week was, there was no validation to prevent someone from requesting multiple invites for the same email address.

In order to ensure that Cribbb will only accept valid data I need to insert a layer of validation before the invitation is created.

If you remember back to Advanced Validation as a Service for Laravel 4, I’ve already got a foundation for creating validation classes in place.

However, should this go in the Controller? Or in the Repository? Or somewhere else?

In this instance I’m going to create a new service class for creating invitations. The reason for this is, I’m going to need to have different ways of creating invitations, and so separating the logic into it’s own class makes more sense than trying to crowbar it into the repository or duplicating my efforts in the controller.

The invitation classes are their own component of Cribbb and so they deserve their own namespace. I’m going to keep all of these classes under the Cribbb\Inviters namespace.

The class I will create is for requesting a new invite. This class will be called Requester.

Here is the basic outline for Requester.php:

<?php namespace Cribbb\Inviters;

use Illuminate\Support\MessageBag;
use Cribbb\Repositories\Invite\InviteRepository;

class Requester
{
    /**
     * Invite Repository
     *
     * @var Cribbb\Repositories\Invite\InviteRepository
     */
    protected $inviteRepository;

    /**
     * MessageBag errors
     *
     * @var Illuminate\Support\MessageBag;
     */
    protected $errors;

    /**
     * Create a new instance of the Invite Requester
     *
     * @param Cribbb\Repositories\Invite\InviteRepository $inviteRepository
     * @return void
     */
    public function __construct(InviteRepository $inviteRepository)
    {
        $this->inviteRepository = $inviteRepository;

        $this->errors = new MessageBag();
    }

    /**
     * Return the errors message bag
     *
     * @return Illuminate\Support\MessageBag
     */
    public function errors()
    {
        return $this->errors;
    }
}

As you can see from the code above, I’m injecting the InviteRepository into this class and setting the instance as a class property. This means I no longer need to inject the repository into the controller.

I will also set up the $errors property as a new instance of MessageBag. I prefer this to be an empty instance of MessageBag rather than setting it as null because the class has a couple of nice public methods for working with an empty bag. You will also notice that I instantiate a new instance of MessageBag inside the controller, rather than injecting it. This violates the dependency injection principle, but I think of the MessageBag as an enhancement of the class, rather than a dependency, so I’m cool with that.

Next I need to create a validator class that will validate the request email address. Under Cribbb\Inviters I will create a new directory called Validators to hold my validator classes.

To validate email addresses, I will use the following class:

<?php namespace Cribbb\Inviters\Validators;

use Cribbb\Validators\Validable;
use Cribbb\Validators\LaravelValidator;

class EmailValidator extends LaravelValidator implements Validable
{
    /**
     * Validation rules
     *
     * @var array
     */
    protected $rules = [
        "email" => "required|email|unique:users,email|unique:invites,email",
    ];
}

If the above code doesn’t make sense, read my post on Advanced Validation as a Service for Laravel 4.

Now that I’ve got my validator class set up, I need to inject it into the Request class. Due to the evolving nature of this kind of code, I will inject my Validable class as an array into the class. This means should I want to add more Validable instances, I won’t have to modify the Request class.

My __construct() method now looks like this:

/**
 * An array of Validators
 *
 * @var array
 */
protected $validators;

/**
 * Create a new instance of the Invite Requester
 *
 * @param Cribbb\Repositories\Invite\InviteRepository $inviteRepository
 * @param array $validators
 * @return void
 */
public function __construct(InviteRepository $inviteRepository, array $validators)
{
    $this->inviteRepository = $inviteRepository;
    $this->validators = $validators;

    $this->errors = new MessageBag;
}

This might seem like an over-engineered solution, and for a lot of instances it probably is. If your class will never need to be modified for evolving business rules, then injecting the single class would be totally fine too.

Finally I can write the create() method that will actually create the new invitation:

/**
 * Create a new Invite
 *
 * @param array $data
 * @return Illuminate\Database\Eloquent\Model
 */
public function create(array $data)
{
    foreach ($this->validators as $validator) {
        if ($validator instanceof Validable) {
            if (! $validator->with($data)->passes()) {
                $this->errors = $validator->errors();
            }
        } else {
            throw new Exception("{$validator} is not an instance of Cribbb\Validiators\Validable");
        }
    }

    if ($this->errors->isEmpty()) {
        return $this->inviteRepository->create($data);
    }
}

First I spin through all of the validators and check that they are an instance of Cribbb\Validators\Validable. If any one of the array values is not an instance of Validable I will throw an Exception. You could argue that the Exception is a bit overkill, but I think for instances where we are accepting data into the application, it’s better to be safe than sorry.

Next I check that the $data meets the requirements of the rules of the validator. If it does not, I will add the errors to the $errors class property.

Finally if there are no errors I will pass the data to the repository to create the new invitation.

The full Request class looks like this:

<?php namespace Cribbb\Inviters;

use Exception;
use Cribbb\Validators\Validable;
use Illuminate\Support\MessageBag;
use Cribbb\Repositories\Invite\InviteRepository;

class Requester
{
    /**
     * Invite Repository
     *
     * @var Cribbb\Repositories\Invite\InviteRepository
     */
    protected $inviteRepository;

    /**
     * An array of Validators
     *
     * @var array
     */
    protected $validators;

    /**
     * MessageBag errors
     *
     * @var Illuminate\Support\MessageBag;
     */
    protected $errors;

    /**
     * Create a new instance of the Invite Creator
     *
     * @param Cribbb\Repositories\Invite\InviteRepository $inviteRepository
     * @param array $validators
     * @return void
     */
    public function __construct(
        InviteRepository $inviteRepository,
        array $validators
    ) {
        $this->inviteRepository = $inviteRepository;
        $this->validators = $validators;

        $this->errors = new MessageBag();
    }

    /**
     * Create a new Invite
     *
     * @param array $data
     * @return Illuminate\Database\Eloquent\Model
     */
    public function create(array $data)
    {
        foreach ($this->validators as $validator) {
            if ($validator instanceof Validable) {
                if (!$validator->with($data)->passes()) {
                    $this->errors = $validator->errors();
                }
            } else {
                throw new Exception(
                    "{$validator} is not an instance of Cribbb\Validiators\Validable"
                );
            }
        }

        if ($this->errors->isEmpty()) {
            return $this->inviteRepository->create($data);
        }
    }

    /**
     * Return the errors message bag
     *
     * @return Illuminate\Support\MessageBag
     */
    public function errors()
    {
        return $this->errors;
    }
}

Injecting the service class into the Controller

Now that I’ve set up the Request service class I can inject it into the Controller to replace the injected repository:

use Cribbb\Inviters\Requester;

class InviteController extends BaseController
{
    /**
     * The Invite Request service
     *
     * @var Cribbb\Inviters\Requester
     */
    protected $requester;

    /**
     * Create a new instance of the InviteController
     *
     * @param Cribbb\Inviters\Requester
     */
    public function __construct(Requester $requester)
    {
        $this->requester = $requester;
    }

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

        if ($invite) {
            // yay
        }

        // oh no
        $this->requester->errors();
    }
}

As you can see from the code above, this is a simple switch-a-roo of the repository for the service class. However, now we don’t have any weird logic in the controller, and we can report errors back if the input data did not meet the requirements.

A filter to protect the registration route

Another problem the initial implementation had was protecting certain routes to only allow user’s with a valid invitation. This is required so I can limit who has access to the registration form.

In last week’s tutorial I simply checked for a valid invitation in the Controller method. This works, but it’s a kinda messy way of doing it. The valid invite check isn’t really the concern of the Controller method, and if I needed that same logic in another method on a different Controller I would have to duplicate my code.

Instead I can create a filter to ensure only users with a valid invitation are allowed to hit the route. If the user does not have a valid invitation I can just 404 the response because I can just pretend the registration route does not exist.

If you are new to Laravel filters, I would recommend that you also read How to use Laravel 4 Filters.

Here is my route filter:

/*
|————————————————————————-
| Invitation Filter
|————————————————————————-
|
| The invite filter will only allow requests that include a valid
| "code" as part of the query string to access the given route.
| If the request does not have a valid code in the query
| string a "404 Not Found" response will be returned
|
*/

Route::filter("invite", function () {
    if (!Input::has("code")) {
        App::abort(404);
    }

    $repository = App::make("Cribbb\Repositories\Invite\InviteRepository");

    if (!$repository->getValidInviteByCode(Input::get("code"))) {
        App::abort(404);
    }
});

First I check to see if the request has a code parameter. If the request does not have a code, we can just bail out here.

Next I resolve an instance of the InviteRepository out of the IoC container. I then check to see if the code in the request is valid. If the code is not valid I can throw a 404 error.

Remember, a request is considered valid if the filter does not throw an exception or return a response.

Next in my RegisterContoller I can delete the checking logic from last week and replace it with the beforeFilter:

class RegisterController extends BaseController
{
    /**
     * Create a new instance of the RegisterController
     *
     * @return void
     */
    public function __construct()
    {
        $this->beforeFilter("invite", ["only" => "index"]);
    }

    /**
     * Display the form for creating a new user
     *
     * @return View
     */
    public function index()
    {
        return "Sign up here";
    }
}

Now if you try to hit the /register route, you should be returned a 404 Not Found exception. Next, sign up with a valid email and grab the code from the database. Now if you try to hit /register?code=your_code you should be allowed to hit the index method on the controller.

Conclusion

In part 1 of building an advanced invitation system, I looked at leveraging Eloquent’s event lifecycle to automatically generate invitation codes, and setting up a service class to deal with the logic of validating and creating a new invitation.

I think the thing to take away from this tutorial is, how much easier your life will be if you fence off bits of functionality so you don’t have to repeat yourself and you don’t have to rethink about how something should be implemented.

Generating the invitation codes should only happen when the invite is first created and should just be taken care for us just like the auto-incrementing id. None of the code for working with an invitation should need to know how to generate a code.

The question of should I put this in the Controller, the Repository or should I create a new Service class comes up often. My response is usually, “well, it depends”. In this example, I think you definitely do need a separate service class. In my case, there will be multiple ways for creating invitations in my application and the rules around what is considered “valid” data required for a new invitation is also likely to change. Hopefully this was a solid example of how to make the decision that you can use in your applications.

Over the next couple of weeks I will look at how existing users can invite new users, how to create a queue jump system and how to build a family tree that will record how new users were invited to the application.

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.