Datadog collects and monitors your PHP app metrics and distributed traces in real-time with application performance monitoring. Decrease downtime and performance issues with Datadog APM by tracing requests across service boundaries and drilling into individual traces end-to-end with flame graphs. Start your 14-day trial for free today.

An unopinionated package to make Laravel apps tenant aware

Original – by Freek Van der Herten – 9 minute read

Today we released a package to make Laravel apps tenant aware, called laravel-multitenancy. The philosophy of this package is that it should only provide the bare essentials to enable multitenancy.

The package can determine which tenant should be the current tenant for the request. It also allows you to define what should happen when switching the current tenant to another one.

It works for multitenancy projects that need to use one or multiple databases.

In this blog post, I'd like to introduce the package to you.

Are you a visual learner?

I've created a 20-minute video, that walks you through a multi-DB demo app that uses laravel-multitenancy. After showing what the package can do, I explain how it works under the hood.

I'm pretty sure that everybody can pick up some new things by watching the video.

Why build another multitenancy package

Multitenancy in Laravel seems always to have been a hot topic. I think this is the case because there are so many ways to go about it. To get a feel of what multitenancy can encompass and what the possible solutions are, I highly recommend watching this talk Tom Schlick did at Laracon US 2017.

Because I never needed it for client projects, I've always steered clear of the subject.

You could argue that Oh Dear, the uptime tracker that I've built, is multitenant. We use a very lightweight solution there. We simply added a team_id to the sites table. When someone is logged in, we check which teams the users belong to and only show info for those sites. I think for most projects, such a single database solution works just fine.

Recently I started working on a new client project that should be multitenant. The requirements of this particular project are such that it makes sense to have a different database per tenant. When I was researching the subject, it was pure serendipity that Mohammed Said published a series of videos on multitenancy.

In those videos, Mohammed shared a very lightweight approach. It seemed that multitenancy wasn't that hard as I thought it would be. I decided to package up his approach. While I was doing that, and through conversations with Mohammed about it, I reached insights into what a multitenancy package should do.

Most of the existing packages felt too heavy for me. I wondered why that was. I think any multitenancy package should do these three things:

  1. It should keep track of which tenant is the current tenant
  2. It should dynamically change the configuration of the Laravel app when making a tenant current (changing the database, prefix cache)
  3. Tooling, for instance, to create a new database for a tenant, or to migration for tenants

For my taste, the existing packages are doing too much. Most of them do 1. well, but focus too much on "2." and "3."

I think that in every project that needs to be tenant aware, very project-specific things need to be done to make a tenant the current one. Instead of trying to handle all different cases, I decided to focus on making it easy to define tasks to make a tenant the current one. This way, implementing "2." of the list above remains very light.

For "3.", tooling, my colleague Seb had an excellent idea. Instead of creating specific tenant commands, just make it easy to make existing commands tenant aware. And that's what we did. Later in this blog post, I'll explain that solution more.

By keeping the solutions for "2." and "3." very generic, the package remains lightweight.

Keeping track of the current tenant

After you've installed the package, your application has a tenants table that contains a row for each tenant of your application.

To determine which tenant should be the current one for a given request, the package uses a TenantFinder. A valid tenant finder is any class that extends Spatie\Multitenancy\TenantFinder\TenantFinder. This is what that abstract class looks like:

abstract public function findForRequest(Request $request): ?Tenant;

The package ships with a class named DomainTenantFinder. That class will try to find a Tenant whose domain attribute matches the hostname of the current request.

Here's how the default DomainTenantFinder is implemented. The getTenantModel method returns an instance of the class specified in the tenant_model key of the multitenancy config file.

namespace Spatie\Multitenancy\TenantFinder;

use Illuminate\Http\Request;
use Spatie\Multitenancy\Models\Concerns\UsesTenantModel;
use Spatie\Multitenancy\Models\Tenant;

class DomainTenantFinder extends TenantFinder
{
    use UsesTenantModel;

    public function findForRequest(Request $request):?Tenant
    {
        $host = $request->getHost();

        return $this->getTenantModel()::whereDomain($host)->first();
    }
}

In the multitenancy config file, you specify the tenant finder in the tenant_finder key.

// in multitenancy.php
/*
 * This class is responsible for determining which tenant should be current
 * for the given request.
 *
 * This class should extend `Spatie\Multitenancy\TenantFinder\TenantFinder`
 *
 */
'tenant_finder' => Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,

All of this makes it easy to customize how the package determines the current tenant.

There are several methods available to get, set, and clear the current tenant.

You can find the current method like this.

Spatie\Multitenancy\Models\Tenant::current(); // returns the current tenant, or if not tenant is current, `null`

A current tenant will also be bound in the container using the currentTenant key.

app('currentTenant'); // returns the current tenant, or if not tenant is current, `null`

You can check if there is tenant set as the current one:

Tenant::checkCurrent() // returns `true` or `false`

You can manually make a tenant the current one by calling makeCurrent() on it.

$tenant->makeCurrent();

Defining tasks that should run when making a tenant current

When a tenant is made the current one, the package will run the makeCurrent method of all tasks configured in the switch_tenant_tasks key of the multitenancy config file.

The philosophy of this package is that it should only provide the bare essentials to enable multitenancy. That's why it only offers two tasks out of the box. These tasks serve as example implementations.

Let's take a look at one of those two tasks: SwitchDatabaseTask. This task is only useful when you are using separate databases for each of your tenants.

This task can switch the configured database name of the tenant database connection. The database name used will be in the database attribute of the Tenant model.

When using a separate database for each tenant, your Laravel app needs two database connections. One named landlord, which points to the database that should contain the tenants table and other system-wide related info. The other connection, named tenant points to the database of the tenant that is considered the current tenant for a request.

Here's how that could look like in the database config file.

// in config/database.php

'connections' => [
   'tenant' => [
       'driver' => 'mysql',
       'database' => null,
       // other options such as host, username, password, ...
   ],

   'landlord' => [
       'driver' => 'mysql',
       'database' => 'name_of_landlord_db',
       // other options such as host, username, password, ...
   ],

You'll notice that the database key for the tenant connection is set to null. When making a tenant the current one, the SwitchDatabaseTask will automatically set that database key to the database name that is in the database attribute of the tenant.

Here's what that SwitchDatabaseTask looks like. The makeCurrent method will be called when a tenant is being made the current one.


namespace Spatie\Multitenancy\Tasks;

use Illuminate\Support\Facades\DB;
use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
use Spatie\Multitenancy\Exceptions\InvalidConfiguration;
use Spatie\Multitenancy\Models\Tenant;

class SwitchTenantDatabaseTask implements SwitchTenantTask
{
    use UsesMultitenancyConfig;

    public function makeCurrent(Tenant $tenant): void
    {
        $this->setTenantConnectionDatabaseName($tenant->getDatabaseName());
    }

    public function forgetCurrent(): void
    {
        $this->setTenantConnectionDatabaseName(null);
    }

    protected function setTenantConnectionDatabaseName(?string $databaseName)
    {
        $tenantConnectionName = $this->tenantDatabaseConnectionName();

        if (is_null(config("database.connections.{$tenantConnectionName}"))) {
            throw InvalidConfiguration::tenantConnectionDoesNotExist($tenantConnectionName);
        }

        config([
            "database.connections.{$tenantConnectionName}.database" => $databaseName,
        ]);

        DB::purge($tenantConnectionName);
    }
}

It's trivial to create tasks of your own. A task is any class that implements Spatie\Multitenancy\Tasks\SwitchTenantTask. Here is how that interface looks like.

namespace Spatie\Multitenancy\Tasks;

use Spatie\Multitenancy\Models\Tenant;

interface SwitchTenantTask
{
    public function makeCurrent(Tenant $tenant): void;

    public function forgetCurrent(): void;
}

The makeCurrent function will be called when making a tenant current. A common thing to do would be to change some configuration values dynamically.

After creating a task, you must register it by putting its class name in the switch_tenant_tasks key of the multitenancy config file.

Making artisan commands tenant aware

If you want to execute an artisan command for all tenants, you can use tenants:artisan <artisan command>. This command will loop over tenants and, for each of them, make that tenant current and execute the artisan command.

When tenants each have their own database, you could migrate each tenant database with this command (given you are using a task like SwitchTenantDatabase):

php artisan tenants:artisan migrate

Closing off

I think the best feature of our package is its unopinionated nature. By not being specific at all, and letting the package user define all desired behavior when switching to a tenant, it stays very flexible.

To learn more about our package, head over to the extensive documentation.

I've already mentioned above that I also created a nice video in which I show how to make a Laravel app multitenant aware using our package.

If you expect more features out of a multitenancy package, take a look at these excellent alternatives:

I'd like to give some credits to Mohammed Said. His videos on multitancy inspired me to create our package.

laravel-multitenancy isn't the first package my team and I have created. Here's a big list of all the things we open-sourced previously.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

Webmentions

Adewale Toluwani retweeted on 24th May 2020
Adewale Toluwani liked on 24th May 2020
Tobias Scharikow 🦊 liked on 23rd May 2020
Damilola E. Olowookere liked on 23rd May 2020
leunggamciu retweeted on 22nd May 2020
Daniel Tsou liked on 22nd May 2020
Loris Roncali retweeted on 22nd May 2020
Loris Roncali liked on 22nd May 2020
trungpv retweeted on 22nd May 2020
Percy Mamedy liked on 22nd May 2020
trungpv liked on 22nd May 2020
Ihab liked on 22nd May 2020
Martin Medina liked on 22nd May 2020
Robin Dirksen liked on 22nd May 2020
Jamiul Alam liked on 22nd May 2020
Richard Radermacher liked on 21st May 2020
mel liked on 21st May 2020
Alejandro Pérez liked on 21st May 2020
Román Miranda liked on 21st May 2020
Laravel Cameroon liked on 21st May 2020
Anthony Peck liked on 21st May 2020
ahgood liked on 21st May 2020
Thomas Venturini liked on 21st May 2020
Volkan Baskın liked on 21st May 2020
kashif nawaz liked on 21st May 2020
Amritpal Singh liked on 21st May 2020
Niels liked on 21st May 2020
AFC_Vote_Bernie retweeted on 21st May 2020
AFC_Vote_Bernie replied on 21st May 2020
Gonna try this indeed. Thanks!
AFC_Vote_Bernie liked on 21st May 2020
Rajesh Dewle liked on 21st May 2020
Manojkiran liked on 21st May 2020
Kylian_LM liked on 21st May 2020
Josiah N retweeted on 21st May 2020
Ronan Whelan retweeted on 21st May 2020
Ronan Whelan liked on 21st May 2020
NAbeel Yousaf retweeted on 21st May 2020
NAbeel Yousaf liked on 21st May 2020
Monney Arthur liked on 21st May 2020
Michael Dyrynda liked on 20th May 2020
Peter Sowah liked on 20th May 2020
Mohamed Said replied on 20th May 2020
Glad my gibberish turned out to be helpful somehow 😅
José Cage retweeted on 20th May 2020
Dannie L. liked on 20th May 2020
Ken V. liked on 20th May 2020
Benjamin Crozat liked on 20th May 2020
ArielMejiaDev retweeted on 20th May 2020
Rolf den Hartog 🏡 retweeted on 20th May 2020
Bezhan Salleh retweeted on 20th May 2020
David On liked on 20th May 2020
George Jipa liked on 20th May 2020
José Cage liked on 20th May 2020
Nuno Souto liked on 20th May 2020
Rolf den Hartog 🏡 liked on 20th May 2020
ArielMejiaDev liked on 20th May 2020
Adam 🤙🏼 Mench liked on 20th May 2020
Bezhan Salleh liked on 20th May 2020
Tanmoy Goswami liked on 20th May 2020
Mohan Raj retweeted on 20th May 2020
Boris DEHOUMON retweeted on 20th May 2020
Chris liked on 20th May 2020
Gianmarco Nalin liked on 20th May 2020
Swapnil Bhavsar liked on 20th May 2020
Grant Williams liked on 20th May 2020
Samy liked on 20th May 2020
Boris DEHOUMON replied on 20th May 2020
🎊Happy
Efren Colín liked on 20th May 2020
Nelson Melecio retweeted on 20th May 2020
Set Kyar Wa Lar retweeted on 20th May 2020
Paul Redmond 🇺🇸 liked on 20th May 2020
Owen Voke retweeted on 20th May 2020
Mario Inostroza liked on 20th May 2020
Mohamad Norouzi liked on 20th May 2020
Steve Bauman liked on 20th May 2020
Willan Correia liked on 20th May 2020
Nelson Melecio liked on 20th May 2020
Haneef Ansari liked on 20th May 2020
Cyril de Wit liked on 20th May 2020
Set Kyar Wa Lar liked on 20th May 2020
Samuel De Backer liked on 20th May 2020
Travis Elkins liked on 20th May 2020
Owen Voke liked on 20th May 2020
ali ali liked on 20th May 2020
Steve Bauman replied on 20th May 2020
You all at spatie on are fire! 🔥 Providing so much for free -- it's borderline insane, but I love it, so thank you ❤