An unopinionated package to make Laravel apps tenant aware
Photo by Kenny Eliason on Unsplash
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?
The Laravel Package Training contains 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:
- It should keep track of which tenant is the current tenant
- It should dynamically change the configuration of the Laravel app when making a tenant current (changing the database, prefix cache)
- 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.
fermevc, driesvints, joedixon, ufukayyildiz liked this article