cult3

Creating an advanced Invitation System in Laravel 4 Part 2

May 05, 2014

Table of contents:

  1. Allow existing users to invite new users
  2. Policies and business rules
  3. Writing tests
  4. Conclusion

Over the last two weeks I’ve been looking at building an invitation system in Laravel 4. First I looked at setting up a basic foundation that would allow a user to create an invite and then only allow valid invites to be able to register for the application.

Last week I looked at turning that simple foundation into a more robust process that included validation, a route filter and automatic invitation code generation.

In this week’s tutorial I’m going to be looking at building the following components:

  1. Allow existing users to invite new users
  2. Writing tests

Allow existing users to invite new users

An important aspect of social consumer applications is the ability to invite your friends to the application. Users are more likely to use an new application if people they know are already using it, and getting an invite from a friend is a much better first impression than landing on the site cold.

Recording available invitations

When a current user is logged in to the application, I want to be able to send them a notification when they have invitations to send to their friends.

The easiest way to do this is to add a column to the users table that will record how many invitations the current user has available:

$table->integer("invitations")->default(0);

Add this column to your migration by either creating a new migration or just chucking it in the users table migration if you haven’t shipped your application yet.

You will notice by default I’m setting the value to 0. In a future tutorial I will show you how the number of invitations and the rate of new users will be calculated.

The Inviter service

Last week I looked at creating a Requester service class that would be used to accept the invitation request, validate the input and then create and store the new invitation entity.

Inviting a new user is a similar process to requesting an invite, but it involves some slightly different rules and logic.

To encapsulate this process, I will be creating a new service class called Inviter.

The Inviter class will follow the same basic structure as the Requester class from last week. This means I can move some of the methods and duplicated code into an abstract class so they can both inherit it.

So the first thing to do is to create a new file called AbstractInviter.php:

<?php namespace Cribbb\Inviters;

use Exception;
use Cribbb\Validators\Validable;

abstract class AbstractInviter
{
    /**
     * Run the validation checks on the input data
     *
     * @param array $data
     * @return bool
     */
    public function runValidationChecks(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 true;
        }
    }

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

Next I can create the Inviter class:

<?php namespace Cribbb\Inviters;

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

class Inviter extends AbstractInviter
{
    /**
     * 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 Inviter
     *
     * @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();
    }
}

As you can see, so far this is pretty much exactly the same as the Requester class from last week.

Policies and business rules

The big difference between the Requester class and the Inviter class is, the Inviter class is subject to certain policies and business rules that must also be enforced.

When a user requests an invite, as long as their email address is valid we can add them straight into the list.

But the logic around an existing user inviting another user is slightly different and is an area of the code that would likely evolve due to the performance of the application.

For example, an important policy that we have to abide by is that the user must have assigned invitations. If the current user has made a request without an assigned invitation, we need to abort.

Imagine if you had hard coded that check into your create method, but then a couple of weeks down the line you needed to add another user check before allowing the invitation to be created. Do you create another if..else block to run the check? This situation will quickly get out of hand.

Instead I will create Policy classes that can be injected into the service in pretty much the same way as the Validator classes from last week. This means it will be easy to add additional policies into the service when things inevitably evolve in the future.

So the first thing to do is to create a Policies directory under Cribbb\Inviters.

Next I will create a Policy interface that all of my policies will implement:

<?php namespace Cribbb\Inviters\Policies;

use User;

interface Policy
{
    /**
     * Run the policy check on the current user
     *
     * @param User $user
     * @return bool
     */
    public function run(User $user);
}

If one of the policies is not correctly satisfied, I want to be able to throw a custom exception so I can deal with the problem in a better way than a generic error message. I will create a custom exception to handle this situation:

<?php namespace Cribbb\Inviters\Policies;

use Exception;

class InvitePolicyException extends Exception
{
}

And finally I can create the individual policy classes:

<?php namespace Cribbb\Inviters\Policies;

use User;

class UserHasInvitations implements Policy
{
    /**
     * Run the policy check on the current user
     *
     * @param User $user
     * @return bool
     */
    public function run(User $user)
    {
        if ($user->invitations > 0) {
            return true;
        }

        throw new InvitePolicyException(
            "{$user->name} does not have any invitations"
        );
    }
}

As you can see, this tiny class has the sole responsibility of checking that the current user has assigned invitations.

Back in the Inviter class, I will modify the constructor to accept an array of policies:

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

$this->errors = new MessageBag;
}

And I will add the create method so that it runs through each of the policies before creating the new invitation:

/**
 * Create a new Invite
 *
 * @param array User
 * @param array $data
 * @return Illuminate\Database\Eloquent\Model
 */
public function create(User $user, $data)
{
    foreach ($this->policies as $policy) {
        if ($policy instanceof Policy) {
            $policy->run($user);
        } else {
            throw new Exception("{$policy} is not an instance of Cribbb\Inviters\Policies\Policy");
        }
    }

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

Finally in the InvitersServiceProvider I can set up the service to resolve correctly out of the IoC container with the correct dependencies injected:

/**
 * Register the Inviter service
 *
 * @return void
 */
public function registerInviter()
{
    $this->app->bind('Cribbb\Inviters\Inviter', function($app) {
        return new Inviter(
            $this->app->make('Cribbb\Repositories\Invite\InviteRepository'),
            array( new EmailValidator($app['validator']) ),
            array( new UserHasInvitations )
        );
    });
}

Writing tests

Something that I’ve neglected so far is writing tests. I won’t go into detail about testing every single character of the Invitation component, instead I’ll just show you a couple of tests that assert that the component works as it is intended to.

Firs set up your basic test file structure:

class InvitersTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();
    }

    public function tearDown()
    {
    }
}

The first test I will write will be test the request a new invite process:

public function testRequestNewInvitation()
{
    $requester = App::make('Cribbb\Inviters\Requester');

    $invite = $requester->create(array('email' => 'name@domain.com'));
    $this->assertInstanceOf('Invite', $invite);

    $invite = $requester->create(array('email' => ""));
    $this->assertTrue(is_null($invite));
    $this->assertEquals(1, count($requester->errors()));
}

In this test first I request an invite with a valid email address and assert that I’m returned an instance of the model.

Next I request an invite with an invalid email. This time I assert that the returned value is null and the number of errors is 1.

Whilst writing these tests, I encountered a weird Eloquent issue where the model events do not seem to flush themselves between tests. This seems to be a known issue.

A workaround is to add the following method to your test class and call it in the setUp() method like this:

public function setUp()
{
    parent::setUp();
    $this->resetEvents();
}

private function resetEvents()
{
    $models = array('Invite');

    foreach ($models as $model) {
        call_user_func(array($model, 'flushEventListeners'));
        call_user_func(array($model, 'boot'));
    }
}

Next I will test inviting another user to the application:

public function testUserInviteAnotherUser()
{
    $inviter = App::make('Cribbb\Inviters\Inviter');
    $user = new User;
    $user->invitations = 1;

    $invite = $inviter->create($user, array('email' => 'name@domain.com'));
    $this->assertInstanceOf('Invite', $invite);
}

Again, here I’m ensuring that when a user with an assigned invitation invites another user with a valid email address, the returned value is an instance of Invite.

And finally, when a user without any assigned invites tries to invite another user, we should get an exception:

/**
 * @expectedException Cribbb\Inviters\Policies\InvitePolicyException
 */
public function testUserHasNoInvitations()
{
    $inviter = App::make('Cribbb\Inviters\Inviter');
    $user = new User;
    $invite = $inviter->create($user, array('email' => 'name@domain.com'));
}

Conclusion

In this tutorial I’ve looked at building a service class to allow current users to invite their friends to the application.

I think the most important part of this tutorial was looking at introducing policies to our code. This means when the rules around the component inevitably change, we don’t have to start messing with the class. Instead we can just inject another policy.

At first it can seem a bit redundant to write a class with a single method. However these tiny little classes are much nicer to work with as they are easier to understand, test, and combine in different ways to get your desired outcome. Big monolithic classes quickly become a headache, but tiny little classes continue to be a breeze to work with.

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.