cult3

Multi-Tenancy in Laravel 4

Mar 31, 2014

Table of contents:

  1. How do Multi-Tenant applications work?
  2. How do shared database multi-tenant applications work?
  3. Creating the Context Interface
  4. Creating the Context implementation
  5. The importance of the IoC container
  6. Authenticating and setting the context
  7. Creating the Tenant Repository
  8. Extending the Tenant Repository
  9. Testing the context
  10. Conclusion

Multi-tenant applications are where you create a single application that is used independently by many different clients. When a client authenticates with your application, they are only able to access the data that they have created in your application and cannot see any data that was created by any of your other clients.

A typical example of a multi-tenant application is Basecamp. Basecamp customers can only see their own projects, but Basecamp is a single application that is used for all customers.

Multi-tenant applications have become increasingly popular over the last couple of years due to the rise of Software as a Service and the move towards delivering applications through the cloud. It is now much more common for an application to be web based rather than installed on a client’s physical hardware.

In this tutorial I’m going to walk you through setting up the core components to allow you to create multi-tenant applications in Laravel 4.

You might be thinking, “what has this got to do with building Cribbb?!”. You would be correct in thinking that multi-tenancy has no place in consumer social web applications. It would be extremely weird to build that type of application using a multi-tenant approach.

However I think this is an important topic that would be interesting to a lot of you building your own Software as a Service businesses. Seeing as though I’m doing this in Laravel it also makes sense to include it in this series.

So for one week only we will diverge from the path of building a social consumer application, and instead delve into the murky depths of Software as a Service.

How do Multi-Tenant applications work?

As I mentioned in the introduction to this post, multi-tenant applications are a single installation of an application that allows many different clients to access their data independently.

There are a few different approaches to managing the separation of data in multi-tenant applications.

Firstly, you could use a separate database for each client. This means the connection to the correct database is established on authentication. Using separate databases ensures that data from one client is never mixed with the data of another.

Secondly you could use a separate schema. This is where you use one database but the database is able to manage the separation of data through the schema. This basically means that every client has it’s own set of tables within the same database.

And thirdly, you could use a shared database with a shared schema where you select data from the database using a tenant_id field.

The choice between the three different approaches really comes down to what type of application you a building. If you are building a high security application for a regulated industry, it makes sense to have physical separation of data through isolated databases.

However if you are building a low cost Software as a Service application, you can sacrifice this security measure for lower cost infrastructure and overheads. Maintaining separate databases or even a separate schema will be significantly more costly to run and maintain.

My chosen approach is the shared database and shared schema application. I think the benefits of lower overheads and easier maintainability greatly outweigh the security implications for the majority of multi-tenant applications. However, if you were building software for banks, hospitals or the government you will probably want to use the separate database approach.

How do shared database multi-tenant applications work?

So now that I’ve established what are multi-tenant applications and what are the different application architecture choices you have, I’ll explain how my chosen approach works.

In a typical Software as a Service application a client will authenticate and then be presented with their unique dashboard filled with their data and none of the data from any of the other clients.

When the client authenticates our application will set a global context that will scope all database queries to only return data for that current client. This means that all the code of the application is unaffected because the data is scoped at the database.

There are usually many different ways to authenticate into an application. For example you could require client credentials for your application but also offer token based authentication via an API. In either case we want a solution that will work for any type of authentication so the database scoping logic is not repeated and therefore less prone to errors or bugs.

Creating the Context Interface

The first thing I need to do is to create the Context service that will act as the scope of the application. This is basically just a global object that will be injected into other aspects of the code (for example the Repositories) to restrict data to only the current client.

The Context is simply a wrapper around the entity object of the current client. However, because business rules often change over the life of an application, it’s important to make the context open for substitution.

To enable this substitution I will create an interface for the context like this:

<?php namespace Cribbb\Contexts;

use Illuminate\Database\Eloquent\Model;

interface Context
{
    /**
     * Set the context
     *
     * @param Illuminate\Database\Eloquent\Model
     */
    public function set(Model $context);

    /**
     * Check to see if the context has been set
     *
     * @return boolean
     */
    public function has();

    /**
     * Get the context identifier
     *
     * @return integer
     */
    public function id();

    /**
     * Get the context column
     *
     * @return string
     */
    public function column();

    /**
     * Get the context table name
     *
     * @return string
     */
    public function table();
}

As you can see above, the client entity will be injected into the service. The methods on the service are available as a generic public API for working with the context in our application. This is important because, for example, if instead we need to substitute a $client entity for a $organisation entity, we would have to replace every instance of client_id for organisation_id.

Creating the Context implementation

Next I can create the specific Context implementation. In this case my context is a Client entity. The implementation is just a class that fulfils the requirements of the Context interface:

<?php namespace Cribbb\Contexts;

use Illuminate\Database\Eloquent\Model;

class ClientContext implements Context
{
    /**
     * The current context
     *
     * @var Illuminate\Database\Eloquent\Model
     */
    protected $context;

    /**
     * Set the context
     *
     * @param Illuminate\Database\Eloquent\Model
     */
    public function set(Model $context)
    {
        $this->context = $context;
    }

    /**
     * Check to see if the context has been set
     *
     * @return boolean
     */
    public function has()
    {
        if ($this->context) {
            return true;
        }

        return false;
    }

    /**
     * Get the context identifier
     *
     * @return integer
     */
    public function id()
    {
        return $this->context->id;
    }

    /**
     * Get the context column
     *
     * @return string
     */
    public function column()
    {
        return "client_id";
    }

    /**
     * Get the context table name
     *
     * @return string
     */
    public function table()
    {
        return "clients";
    }
}

The importance of the IoC container

Now that I’ve created the generic Context interface and a specific implementation for the Client model, I now need to bind the two together in Laravel’s IoC container so that when I call for an instance of Cribbb\Contexts\Context I will be returned an instance of Cribbb\Contexts\ClientContext. This is exactly the same as how the EloquentUserRepository is returned when you write UserRepository in your Controllers.

However, there is one more important detail. The Context object should be shared through the application as a single instance. When I set the context through the authentication, I want the same object to be available in the Repositories. To ensure that the same object is shared throughout the application for each request I will use the shared method of the IoC container.

Here is my ContextServiceProvider:

<?php namespace Cribbb\Contexts;

use Illuminate\Support\ServiceProvider;

class ContextServiceProvider extends ServiceProvider
{
    /**
     * Register
     */
    public function register()
    {
        $this->app["context"] = $this->app->share(function ($app) {
            return new ClientContext();
        });

        $this->app->bind("Cribbb\Contexts\Context", function ($app) {
            return $app["context"];
        });
    }
}

Finally add the following line to your list of Service Providers in app/config/app.php:

"Cribbb\Contexts\ContextServiceProvider";

Authenticating and setting the context

Now that you have the global Context object in your application you can set the context when you authenticate the request by injecting the context and setting the database entity into the Context object.

This could be as simple as extending the inbuilt Laravel authentication filter to set the context, or creating your own API filter to authenticate requests based upon a token and a secret. The choice of how you implement authentication is really up to you.

For example, you could modify the existing Laravel auth filter like this:

Route::filter("auth", function () {
    $context = App::make("Cribbb\Contexts\Context");

    if (Auth::guest()) {
        return Redirect::guest("login");
    }

    $context->set(Auth::user());
});

Here I’m resolving the context out of the IoC container and setting the user entity.

This is really open to work however you need it to work. If you authenticate against an organisation, just set the organisation entity instead of the user entity.

Creating the Tenant Repository

Now that the Context is set we can use it to scope the database requests in the Repositories. To do this I will create another Repository named TenantRepository which extends the base AbstractRepository.

For each repository that needs to be scoped per client I then extend the TenantRepository rather than the AbstractRepository directly. This enables my repositories to all inherit the basic blueprint, but the repositories that need to be scoped can inherit an extra layer of logic.

In the TenantRepository I create a couple of methods that wrap my basic building block methods to scope the query if applicable. A scoped relationship could either be any of the different types of database relationship. For example it could be based upon a foreign id column or it could be through a pivot table. In the majority of cases you will only need to scope through a column. I think it is rare that a multi-tenant application would have resources that are available to many clients. However I’ve included those methods in the example below to show you how you would achieve that using Laravel’s query builder.

I also don’t want to create different repositories for scoped access and not scoped access. For example, if I wanted to create a dashboard to monitor the application I don’t want to have to create a second set of repositories that weren’t scoped per client. You will notice that I check to see if the scope has been set to achieve this:

<?php namespace Cribbb\Repositories;

use StdClass;
use Illuminate\Database\Eloquent\Builder;

abstract class TenantRepository extends AbstractRepository
{
    /**
     * Scope a query based upon a column name
     *
     * @param Illuminate\Database\Eloquent\Builder
     * @return Illuminate\Database\Eloquent\Builder
     */
    public function scopeColumn(Builder $model)
    {
        if ($this->scope->has()) {
            return $model->where(
                $this->scope->column(),
                "=",
                $this->scope->id()
            );
        }

        return $model;
    }

    /**
     * Scope the query based upon a relationship
     *
     * @param Illuminate\Database\Eloquent\Builder
     * @return Illuminate\Database\Eloquent\Builder
     */
    public function scopeRelationship(Builder $model)
    {
        if ($this->scope->has()) {
            return $model->whereHas($this->scope->table(), function ($q) {
                $q->where($this->scope->column(), "=", $this->scope->id());
            });
        }

        return $model;
    }

    /**
     * Retrieve all entities through a scoped column
     *
     * @param array $with
     * @return Illuminate\Database\Eloquent\Collection
     */
    public function allThroughColumn(array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeColumn($entity)->get();
    }

    /**
     * Retrieve all entities through a scoped relationship
     *
     * @param array $with
     * @return Illuminate\Database\Eloquent\Collection
     */
    public function allThroughRelationship(array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeRelationship($entity)->get();
    }

    /**
     * Find a single entity through a scoped column
     *
     * @param int $id
     * @param array $with
     * @return Illuminate\Database\Eloquent\Model
     */
    public function findThroughColumn($id, array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeColumn($entity)->find($id);
    }

    /**
     * Find a single entity through a scoped relationship
     *
     * @param int $id
     * @param array $with
     * @return Illuminate\Database\Eloquent\Model
     */
    public function findThroughRelationship($id, array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeRelationship($entity)->find($id);
    }

    /**
     * Get Results by Page through scoped column
     *
     * @param int $page
     * @param int $limit
     * @param array $with
     * @return StdClass Object with $items and $totalItems for pagination
     */
    public function getByPageThroughColumn($page = 1, $limit = 10, $with = [])
    {
        $result = new StdClass();
        $result->page = $page;
        $result->limit = $limit;
        $result->totalItems = 0;
        $result->items = [];

        $query = $this->scopeColumn($this->make($with));

        $users = $query
            ->skip($limit * ($page - 1))
            ->take($limit)
            ->get();

        $result->totalItems = $this->model->count();
        $result->items = $users->all();

        return $result;
    }

    /**
     * Get Results by Page through scoped relationship
     *
     * @param int $page
     * @param int $limit
     * @param array $with
     * @return StdClass Object with $items and $totalItems for pagination
     */
    public function getByPageThroughRelationship(
        $page = 1,
        $limit = 10,
        $with = []
    ) {
        $result = new StdClass();
        $result->page = $page;
        $result->limit = $limit;
        $result->totalItems = 0;
        $result->items = [];

        $query = $this->scopeRelationship($this->make($with));

        $users = $query
            ->skip($limit * ($page - 1))
            ->take($limit)
            ->get();

        $result->totalItems = $this->model->count();
        $result->items = $users->all();

        return $result;
    }

    /**
     * Search for a single result by key and value through a scoped column
     *
     * @param string $key
     * @param mixed $value
     * @param array $with
     * @return Illuminate\Database\Eloquent\Model
     */
    public function getFirstByThroughColumn($key, $value, array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeColumn($entity)
            ->where($key, "=", $value)
            ->first();
    }

    /**
     * Search for a single result by key and value through a scoped relationship
     *
     * @param string $key
     * @param mixed $value
     * @param array $with
     * @return Illuminate\Database\Eloquent\Model
     */
    public function getFirstByThroughRelationship(
        $key,
        $value,
        array $with = []
    ) {
        $entity = $this->make($with);

        return $this->scopeRelationship($entity)
            ->where($key, "=", $value)
            ->first();
    }

    /**
     * Search for many results by key and value through a scoped column
     *
     * @param string $key
     * @param mixed $value
     * @param array $with
     * @return Illuminate\Database\Eloquent\Collection
     */
    public function getManyByThroughColumn($key, $value, array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeColumn($entity)
            ->where($key, "=", $value)
            ->get();
    }

    /**
     * Search for many results by key and value through a scoped relationship
     *
     * @param string $key
     * @param mixed $value
     * @param array $with
     * @return Illuminate\Database\Eloquent\Collection
     */
    public function getManyByThroughRelationship($key, $value, array $with = [])
    {
        $entity = $this->make($with);

        return $this->scopeRelationship($entity)
            ->where($key, "=", $value)
            ->get();
    }
}

Extending the Tenant Repository

Now that I’ve got the abstract TenantRepository set up I can extend it to create my individual repositories.

For example, if this was an application like Basecamp I might have a ProjectRepository that was scoped through a column. My repository might look something like this:

<?php namespace Cribbb\Repositories\Project;

use Cribbb\Contexts\Context;
use Illuminate\Database\Eloquent\Model;
use Cribbb\Repositories\TenantRepository;

class EloquentProjectRepository extends TenantRepository implements
    ProjectRepository
{
    /**
     * @var Model
     */
    protected $model;

    /**
     * @var Context
     */
    protected $scope;

    /**
     * Construct
     *
     * @param Cribbb\Contexts\Context $scope
     * @param Illuminate\Database\Eloquent\Model $model
     */
    public function __construct(Model $model, Context $scope)
    {
        $this->model = $model;
        $this->scope = $scope;
    }

    /**
     * Return all projects
     *
     * @param array $with
     * @return Illuminate\Database\Eloquent\Collection
     */
    public function all(array $with = [])
    {
        return $this->allThroughColumn($with);
    }

    /**
     * Return a single project
     *
     * @param array $with
     * @return Illuminate\Database\Eloquent\Model
     */
    public function find($id, array $with = [])
    {
        return $this->findThroughColumn($id, $with);
    }

    /**
     * Get Results by Page
     *
     * @param int $page
     * @param int $limit
     * @param array $with
     * @return StdClass Object with $items and $totalItems for pagination
     */
    public function getByPage($page = 1, $limit = 10, $with = [])
    {
        return $this->getByPageThroughColumn($page, $limit, $with);
    }

    /**
     * Search for a single result by key and value
     *
     * @param string $key
     * @param mixed $value
     * @param array $with
     * @return Illuminate\Database\Eloquent\Model
     */
    public function getFirstBy($key, $value, array $with = [])
    {
        return $this->getFirstByThroughColumn($key, $value, $with);
    }

    /**
     * Search for many results by key and value
     *
     * @param string $key
     * @param mixed $value
     * @param array $with
     * @return Illuminate\Database\Eloquent\Collection
     */
    public function getManyBy($key, $value, array $with = [])
    {
        return $this->getManyByThroughColumn($key, $value, $with);
    }
}

Testing the context

Now that you have set up everything, we can do a dirty little test to ensure everything is working correctly. First seed your database with enough data to clearly show that your tests are working correctly. By this I mean a couple of clients and lots of projects.

Next create a new test route you can hit and copy the following code:

Route::get("/test", function () {
    $client = Client::find(1);
    $context = App::make("Cribbb\Contexts\Context");
    $context->set($client);

    $repository = App::make("Cribbb\Repositories\Project\ProjectRepository");

    $projects = $repository->all();

    foreach ($projects as $project) {
        var_dump($project->title);
    }
});

In the example above I’m grabbing the first client and resolving the Context out of the IoC container. Next I resolve the ProjectRepository out of the IoC container and call the all method to select all the available projects.

Now when you hit the test route in the browser you should see a list of available projects. However, when you change which client you select from the database and hit the route again the project list will automatically change.

Congratulations! You’ve just set up automatically scoping repositories for your new Software as a Service application!

Conclusion

Phew! We covered a lot in this tutorial. Hopefully if you’ve been following a long with this series you will see that the individual building blocks of what I’ve implemented in this tutorial have mostly been covered in previous tutorials. It just goes to show that if we learn and understand the individual building blocks, we can build some pretty powerful applications.

In this tutorial we’ve covered a couple of interesting things. Firstly, creating a Context that wraps an existing object and provides an easily accessible public API. This approach makes it really easy to substitute how you want to scope your application if the business rules of your company change in the future.

Secondly we looked at setting the scope through authentication. There are many different ways to authenticate an application. You now have the basic simple building blocks for authenticating and setting a global scope that can be used through the application.

And thirdly I showed you how to add another layer of inheritance to add scoping logic to the repositories in your application that need to be scoped. This can optionally be applied to whichever repositories that you want to be scoped, but also offers you the flexibility to ignore the scope or to scope different types of database relationships.

As I mentioned at the top of this post, this multi-tenant architecture has no place in Cribbb. However hopefully you can use this as a building block or inspiration to build your own multi-tenant applications for your company or for your clients.

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.