Home » Code » Setting the Context in a Laravel Application

Setting the Context in a Laravel Application

Posted by on September 21st, 2015

Setting the Context in a Laravel Application
Last week we looked at managing context in a Laravel application.

Context is a very important aspect of a web application as this foundational structure will be relied upon for almost every piece of code.

Setting the context usually involves checking against the business rules of the application.

For example, does the current user have access to this group? Does the current task belong to this project? Can this user create a new post in this thread?

These kind of foundational business rules need to be addressed whenever a request enters the application.

It can be tempting to just deal with this kind of logic in the Controller or Service class that is handling the request.

But you will end up repeating code, duplicating logic, and never fully trusting that the business rules have been correctly enforced.

Instead, a better way to deal with this problem is to combine what we’ve looked at over the last couple of weeks to set the context and use it throughout the application.

In today’s tutorial I will show you how to check the business rules of the application when a request comes in, and then authoritatively set the context so the rest of your application code can rely upon it.

The importance of the URL

URLs are very important in web applications. A good URL scheme should allow the user to drive the application by manipulating the URL, rather than interacting with the User Interface.

URLs should be useful and should give a sense of the hierarchy of the application. For example, when you see a URL such as projects/1/tasks/2, you know that the task belongs to the project.

When a user makes a request such as the one above, there are a number of checks that take place. Does the user have access to the project? Does the user have access to the task? Does the task belong to the project?

These kinds of details aren’t really the value of your application, but it’s imperative that you get them right!

For example it’s going to be a bit strange if you can access task 3 via projects/1/tasks/3 when task 3 does not belong to project 1.

Using the URL to set the Context

URLs can be used to set the context of the application for each request.

In the example from above, we can first check that the project belongs to the user using a Guard Implementing Business Rules as Guards).

If it doesn’t we can bail out from the request by throwing a relevant Exception that describes why the business rule was not satisfied.

Next we can check to ensure the task belongs to the project, again if not we can bail out of the request.

Finally if all of the business rules are satisfied, we can set the context at the beginning of the request.

This means whenever we need to get the context of the current task we can simply resolve it from the IoC container and avoid complex checking in the Controller or passing the same model object around.

With the context set at the beginning of the request, we can write the rest of our application code safe in the knowledge that these checks have already been dealt with.

How this is going to work

To do this I’m going to create Scope class that will manage setting the Context.

Each Scope class will be injected with a number of Guards. We looked at Guards as a way of encapsulating a single business rule in Implementing Business Rules as Guards. If any of the Guards are not satisfied a specific Exception will be thrown indicating the reason for failure Dealing with Exceptions in a Laravel API application.

The Scope will also be injected with a Resolver object. The Resolver object will be responsible for gathering the required details for the Scope to perform it’s checks.

In most cases this will involve picking the id out of the URL, but there’s no reason why we should tie ourself to the Request object. This same technique can be used under a variety of circumstances.

Finally, if each Guard is satisfied we can set the Context using the Manager class from last week Managing Context in a Laravel application.

Creating the Abstract Scope class

First I will create an abstract Scope class that each Scope will extend:

abstract class Scope
{

}

The Scope class should have a set() method so we can manually set the model:

/**
 * Set the Scope to check
 *
 * @param Model $model
 * @return self
 */
public function set(Model $model)
{
    $this->model = $model;

    return $this;
}

Alternatively we might want to use a Resolver. To use a Resolver, first we need to set it:

/**
 * Set the Resolver
 *
 * @param Resolver $resolver
 * @return self
 */
public function resolver(Resolver $resolver)
{
    $this->resolver = $resolver;

    return $this;
}

Next I will provide a get() method to either return the $model if it has been set, or use the resolver to resolve it:

/**
 * Get the scope
 *
 * @return Model
 */
public function get()
{
    if ($this->model) return $this->model;

    if ($this->resolver) {
        return $this->model = $this->resolver->resolve();
    }
}

Finally I will require an abstract check() method that should be implemented on the classes that extend this class.

/**
 * Check to see if the Scope is valid
 *
 * @return self
 */
abstract public function check();

Creating the GroupScope implementation

Next I will create the GroupScope implementation. This class will be responsible for iterating through the Guards and ensuring the Scope is satisfied:

<?php namespace Cribbb\Groups;

use Cribbb\Foundation\Scopes\Scope;
use Cribbb\Foundation\Facades\Context;

class GroupScope extends Scope
{
    /**
     * @var array
     */
    private $guards;

    /**
     * @param array $guards
     * @return void
     */
    public function __construct(array $guards = [])
    {
        $this->guards = $guards;
    }

    /**
     * Check to see if the Scope is valid
     *
     * @return self
     */
    public function check()
    {
        $user  = Context::get('User')->model();
        $group = $this->get();

        foreach ($this->guards as $guard) {
            $guard->check(compact('user', 'group'));
        }

        return $this;
    }
}

First I inject this class with the relevant Guards to check against. It’s important to inject the Guard classes because these rules are likely to change as the application evolves and theoretically we might have slightly different rules to check against within the same application.

Next I will implement the check() method. First I will resolve the UserContext. This will rely on the UserContext being set earlier in the lifecycle of the request.

Next I will use the get() method to get the Group model, and then I will iterate over each of the Guards and pass the two model objects.

If any of the Guards are not satisfied, an Exception will be thrown.

The tests for this class is fairly simple:

<?php namespace Cribbb\Tests\Groups;

use Cribbb\Groups\Group;
use Cribbb\Groups\GroupScope;
use Cribbb\Foundation\Scopes\Resolver;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class GroupScopeTest extends \TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function should_set_model()
    {
        $scope = new GroupScope;

        $scope->set(factory(Group::class)->make());

        $this->assertInstanceOf(Group::class, $scope->get());
    }

    /** @test */
    public function should_use_resolver()
    {
        $scope = new GroupScope;

        $scope->resolver(new StubResolver(factory(Group::class)->make()));

        $this->assertInstanceOf(Group::class, $scope->get());
    }

    /** @test */
    public function should_return_self_on_check()
    {
        $scope = new GroupScope;

        $this->assertInstanceOf(GroupScope::class, $scope->check());
    }
}

class StubResolver implements Resolver
{
    /**
     * @var Group
     */
    private $model;

    /**
     * @param Group $model
     * @return void
     */
    public function __construct(Group $model)
    {
        $this->model = $model;
    }

    /**
     * Resolve the Entity
     *
     * @return Model
     */
    public function resolve()
    {
        return $this->model;
    }
}

Another benefit of separating the Guards from the Scope is that we don’t need to involve the business logic of the application when testing this file.

Creating the Resolvers

Next I can create the Resolver classes. In this example I’m going to be resolving the group from the URL, but there’s no reason why we should be tied to this one method. Therefore, Resolver classes should be interchangeable and so we first need to define an interface:

<?php namespace Cribbb\Foundation\Scopes;

interface Resolver
{
    /**
     * Resolve the Entity
     *
     * @return Model
     */
    public function resolve();
}

Next I will create the GroupResolver class:

<?php namespace Cribbb\Groups\Resolvers;

use Cribbb\Groups\Group;
use Illuminate\Http\Request;
use Cribbb\Foundation\Scopes\Resolver;
use Cribbb\Groups\Exceptions\GroupNotFound;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class RequestResolver implements Resolver
{
    /**
     * @var Request
     */
    private $request;

    /**
     * @param Request
     * @return void
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * Resolve the Entity
     *
     * @return Model
     */
    public function resolve()
    {
        $route = $this->request->route();

        $uuid = $route->getParameter('group_id');

        try {
            return Group::where('uuid', $uuid)->firstOrFail();
        }

        catch (ModelNotFoundException $e) {
            throw new GroupNotFound('group_not_found', $uuid);
        }
    }
}

First I grab the group_id from the route from the request. Next I attempt to find the Group in the database and return it if it is found.

However, if the Group is not found, I will catch the ModelNotFoundException Exception and throw my own application Exception that will include the relevant HTTP Status code (in this case 404) as well as the relevant error details from the error.php config file.

Here are the tests for this class:

<?php namespace Cribbb\Tests\Groups\Resolvers;

use Mockery as m;
use Cribbb\Groups\Resolvers\RequestResolver;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class RequestResolverTest extends \TestCase
{
    use DatabaseMigrations;

    /** @var Request */
    private $request;

    /** @return void */
    public function setUp()
    {
        parent::setUp();

        $route = m::mock('Illuminate\Routing\Route');
        $route->shouldReceive('getParameter')->andReturn('e2a99972-d99a-47ee-a749-e4fdacdc07f2');

        $this->request = m::mock('Illuminate\Http\Request');
        $this->request->shouldReceive('route')->andReturn($route);
    }

    /** @test */
    public function should_throw_exception_when_group_does_not_exist()
    {
        $this->setExpectedException('Cribbb\Groups\Exceptions\GroupNotFound');

        $resolver = new RequestResolver($this->request);
        $resolver->resolve();
    }

    /** @test */
    public function should_return_the_group()
    {
        factory('Cribbb\Groups\Group')->create([
            'uuid' => 'e2a99972-d99a-47ee-a749-e4fdacdc07f2'
        ]);

        $resolver = new RequestResolver($this->request);
        $account = $resolver->resolve();

        $this->assertInstanceOf('Cribbb\Groups\Group', $account);
    }
}

We’re working with the Laravel’s Request object and so we need to mock it on the setUp() method.

The first test asserts that the correct Exception is thrown when the Group is not found and the second Exception asserts that the Group is found and returned from the resolver.

Setting the Scope

Finally we need a way of setting the scope as the request enters the application. We can does this using Laravel’s Middleware.

First create a new Middleware class:

<?php namespace Cribbb\Http\Middleware;

use Closure;
use Cribbb\Groups\GroupScope;
use Cribbb\Groups\GroupContext;
use Cribbb\Foundation\Facades\Context;
use Cribbb\Groups\Resolvers\RequestResolver;

class GroupContext
{
    /**
     * Handle an incoming request
     *
     * @param Request $request
     * @param Closure $next
     * @return Response
     */
    public function handle($request, Closure $next)
    {
        $context = Context::get('Group');

        $scope = new GroupScope([]);
        $scope->resolver(new RequestResolver($request));

        $context->set($scope->check()->get());

        return $next($request);
    }
}

First we need to resolve the GroupContext from the Context Manager.

Next we can create a new instance of the GroupScope class and inject an array of Guard classes to check against.

Next, set the Resolver on the Scope. In this example I’m resolving from the URL so I will need to use the RequestResolver implementation.

Finally, check the scope and set the returned model from the get() method as the context. This will be the Group model after it has passed through the Guard classes.

Now you can register this middleware in the Kernel.php class and then use it whenever you need to set the context of a Group.

Conclusion

Dealing with these kinds of business rules is very important. You don’t want to duplicate the knowledge of a business rule because updating it will be a nightmare!

By checking against the business rules and setting the context early in the request we can be sure that the rules have been satisfied.

This means when we resolve the object from the controller there’s nothing else to do. Now anywhere in the application you can resolve the current context safe in the knowledge that if any business rule was not satisfied, an Exception would have aborted the request.

Philip Brown

Hey, I'm Philip Brown, a designer and developer from Durham, England. I create websites and web based applications from the ground up. In 2011 I founded a company called Yellow Flag. If you want to find out more about me, you can follow me on Twitter or Google Plus.

  • HRcc

    In GroupScope – line 33, there is a small typo. Guards have handle method instead of check method.

    Thank you for this context related series, I really like the ideas you’ve presented here :)