cult3

Managing Context in a Laravel application

Sep 14, 2015

Table of contents:

  1. Understanding Context
  2. How this is going to work
  3. Creating the Abstract Context class
  4. Creating the User Context
  5. Creating the Context Manager
  6. Registering the Contexts in the IoC Container
  7. Using Context within your application
  8. Conclusion

Context is a very important thing within a web application. The majority of all web applications will be used by multiple users, companies or groups. Context determines how business rules should be enforced.

Managing context can be tricky. You need to set the context authoritatively and then refer to it throughout the application.

However, it’s easy to get yourself into a tangle when a consistent approach is not enforced.

I’ve recently started taking a new approach to setting the context. I’ve found it really easy to set up and reason about. When an application starts to become too big to hold in your head at once, you need systems in place to keep things as simple as possible.

In today’s tutorial I will be showing you how to set up and manage context within your Laravel application.

Understanding Context

Context is a bit of an ambiguous term, especially within the context of a web application (pun intended).

By “context” I mean the current thing that should be scoped against.

For example, when a user is authenticated, all of the data should be scoped to the current user. A user should only be able to see their own inbox messages.

When accessing a company account, only data for that company should be accessible.

Or when looking at a project, only the tasks for the project should be returned.

Enforcing these scopes is important because they will usually relate to the business rules of your application.

For example, if you view a post, you will probably want to ensure that the post belongs to the group and the user has access to the group.

These kinds of checks are usually fairly straightforward to perform. However, things can get out of hand if you don’t manage the context effectively.

How this is going to work

In order to set and use the context within the application, we can make good use of Laravel’s IoC Container.

The IoC Container allows you to bind a singleton object. This means whenever you resolve that object from the container, it will always be the same instance.

When a request comes into the application we can set the relevant context based upon the request. We can do this early within the lifecycle of the request to check against any business rules we have in place. If any of the business rules aren’t satisfied we can simply throw an appropriate Exception to abort the request (Dealing with Exceptions in a Laravel API application).

Now whenever we resolve the context from the container, we will automatically have access to the relevant user, account, group, project or whatever the context you’re working with.

This means we can manage setting the context once, early in the request, and then have that context available for the rest of the request.

With this structure you no longer have to find the relevant resource or do any checks within the Controller or any of your other service classes, the context has already been set.

Creating the Abstract Context class

So the first thing to do to implement this system is to create context classes for each important thing in your application. This is really dependant on the business rules of your application, but typically you would want context classes for your users, accounts plus any other important entities such as projects, groups, records, folders etc.

For this example I’m just going to use the User entity, but it is the exactly the same principle no matter what the context is.

First I will create an abstract context class that each context can extend:

<?php namespace Cribbb\Foundation\Context;

use Illuminate\Database\Eloquent\Model;

abstract class Context
{
}

Each context class will be basically exactly the same so we can simply extend this abstract class.

Each context class should hold the Model object that represents this context. In this example I’m using an Eloquent model, but theoretically it could be anything:

/**
 * @var Model
 */
private $model;

I’m also going to include the “type” of context. This will be useful later:

/**
 * @var string
 */
protected static $type;

Next I’m going to include a check() method to check to see if the context has been set:

/**
 * Check to see if the context has been set
 *
 * @return bool
 */
public function check()
{
    return ! is_null($this->model);
}

Next I will include an id() method as a shortcut for getting the id of the context:

/**
 * Get the id of the context
 *
 * @return int
 */
public function id()
{
    if ($this->check()) {
        return $this->model->id;
    }
}

We will need a way to set the context, and so we need a set() method:

/**
 * Set the context
 *
 * @param Model $model
 * @return Context
 */
public function set(Model $model)
{
    $this->model = $model;

    return $this;
}

And finally, we need a way of getting the underlying Model object, and so for that I will use the model() method:

/**
 * Get the underlying Model
 *
 * @return Model
 */
public function model()
{
    if ($this->check()) {
        return $this->model;
    }

    return new UnknownContext(self::$type);
}

As you can see in this method, first I check to see if the model has been set, and if it has I will return it.

However if the model has not been set I will create a new instance of UnknownContext.

Typically when using a context you will get the model using the model() method and then chain a method or property call. However, if the model has not been set you will get a weird error.

By using the UnknownContext class we can provide a better error to the developer. Here is that class:

<?php namespace Cribbb\Foundation\Context;

use Cribbb\Foundation\Context\Exceptions\ContextNotSet;

class UnknownContext
{
    /**
     * @var string
     */
    private $type;

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

    /**
     * Catch all method calls
     *
     * @param string $name
     * @param mixed $arguments
     * @return void
     */
    public function __call($name, $arguments)
    {
        $this->handle();
    }

    /**
     * Catch all get calls
     *
     * @param string $name
     * @return void
     */
    public function __get($name)
    {
        $this->handle();
    }

    /**
     * Catch all set calls
     *
     * @param string $name
     * @param string $value
     * @return void
     */
    public function __set($name, $value)
    {
        $this->handle();
    }

    /**
     * Handle all calls to the class
     *
     * @return void
     */
    private function handle()
    {
        throw new ContextNotSet(
            sprintf("The Context type %s has not been set", $this->type)
        );
    }
}

As you can see I’m simply catching any calls and then throwing a more relevant Exception.

Alternatively you might want to create a class for an unknown context that can continue gracefully. However, that would be dependant on how your application should behave under those circumstances.

Creating the User Context

With the abstract Context class in place, each individual concrete context class is very simple:

<?php namespace Cribbb\Users;

use Cribbb\Foundation\Context\Context;

class UserContext extends Context
{
    /**
     * @var string
     */
    protected static $type = "User";
}

Each concrete context can simply extends the abstract Context class. The only thing you need to set is the $type.

This makes it incredibly easy to add as many contexts as your application needs without the overhead of lots of boilerplate.

Creating the Context Manager

Next I’m going to create the “Context Manager”. This will simply be a class that we use to return a particular context.

The reason why I’m creating this Manager class is simply to provide a better interface for getting and setting the context. This is going to be something that you do through your codebase and so we can stop it looking messing by slapping a better API on it, rather than just resolving straight from the IoC container.

Here is the Manager class:

<?php namespace Cribbb\Foundation\Context;

use Cribbb\Foundation\Context\Exceptions\InvalidContext;

class Manager
{
    /**
     * @var array
     */
    private $contexts;

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

    /**
     * Return the Context
     *
     * @param string $key
     * @return Context
     */
    public function get($key)
    {
        $context = array_get($this->contexts, $key);

        if ($context) {
            return app($context);
        }

        throw new InvalidContext(sprintf("%s is not a valid Context", $key));
    }
}

This class will search for the requested context and return it if it is available. If the developer has requested a context that is not available, we can throw an appropriate exception.

I’m also going to provide a Facade:

<?php namespace Cribbb\Foundation\Facades;

use Illuminate\Support\Facades\Facade;

class Context extends Facade
{
    /**
     * Get the registered name of the component
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return "context";
    }
}

Registering the Contexts in the IoC Container

Finally we need to register each context as well as the Manager within the IoC Container using a Service Provider.

To register a context, use the singleton() method:

$this->app->singleton(UserContext::class, function () {
    return new UserContext();
});

When registering the Manager class, make sure you pass each context when the class is initialised:

$this->app->singleton("context", function () {
    return new Manager([
        "User" => UserContext::class,
    ]);
});

Using Context within your application

Next week we will be looking at setting the context when a request comes into your application in more detail.

But for now we will look at using the contexts and the context manager in your application.

Because we’re resolving each context from the IoC container we can simply type hint them in the __construct method of a class:

class SendWelcomeEmail
{
    /**
     * @param UserContext $user
     */
    public function __construct(UserContext $user)
    {
        // Send email to the current user
    }
}

Larval also allows you to automatically resolve classes from the IoC container from Controller methods:

class ProjectsController
{
    /**
     * Return a Project by it's id
     *
     * @param ProjectContext
     * @return JsonResponse
     */
    public function show(ProjectContext $context)
    {
        return response()->json($context->model()->toArray());
    }
}

This above example is particularly nice because we don’t have to deal with finding the correct project and then asserting that it should be viewable in the Controller as that logic has already been dealt with. The Controller can simply get back to it’s job of returning a response.

And finally, anywhere else in your application where you need to get a particular context, you can simply use the Manager via the Facade:

$id = Context::get("Account")->id();

Conclusion

Dealing with Context is something you will have to deal with in a web application.

I’ve seen a lot of applications where context is not really dealt with in a consistent way. This will lead to problems because it becomes difficult to scope data and ensure that the current request satisfies the business rules of the application.

By providing a unified way of setting and getting the context we have made this aspect of the application a lot clearer!

Next week we will be looking at setting the context.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.