Tenancy bootstrappers
Tenancy bootstrappers are one of the most fundamental concepts in this package.
A tenancy bootstrapper is a class switches some part of your application to the
tenant context. For instance, DatabaseTenancyBootstrapper switches the
application’s default database connection to the database of the current tenant.
CacheTenancyBootstrapper adjusts the cache store prefix to be scoped to the current
tenant. FilesystemTenancyBootstrapper scopes storage/filesystem-related calls to
the current tenant.
These classes are executed when tenancy is bootstrapped, which is typically when
tenancy()->initialize() is called — either manually or using a tenant identification
middleware such as InitializeTenancyByDomain.
Tenancy bootstrappers are configured in the tenancy.bootstrappers array:
// Basic Laravel featuresBootstrappers\DatabaseTenancyBootstrapper::class,Bootstrappers\CacheTenancyBootstrapper::class,// Bootstrappers\CacheTagsBootstrapper::class, // Alternative to CacheTenancyBootstrapper// Bootstrappers\DatabaseCacheBootstrapper::class, // Separates cache by DB rather than by prefix, must run after DatabaseTenancyBootstrapperBootstrappers\FilesystemTenancyBootstrapper::class,Bootstrappers\QueueTenancyBootstrapper::class,// Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
// Adds support for the database session driverBootstrappers\DatabaseSessionBootstrapper::class,
// Configurable bootstrappers// Bootstrappers\RootUrlBootstrapper::class,// Bootstrappers\UrlGeneratorBootstrapper::class,// Bootstrappers\MailConfigBootstrapper::class,// Bootstrappers\BroadcastingConfigBootstrapper::class,// Bootstrappers\BroadcastChannelPrefixBootstrapper::class,
// Integration bootstrappers// Bootstrappers\Integrations\FortifyRouteBootstrapper::class,// Bootstrappers\Integrations\ScoutPrefixBootstrapper::class,
// Bootstrappers\PostgresRLSBootstrapper::class,Out of the box, Tenancy ships bootstrappers for just about every “component” of Laravel.
If you’re integrating with third-party packages you may want to add your own bootstrappers.
You may also want to add your own bootstrappers for your own application logic. That becomes a bit of a matter of separation of concerns. For instance, if you have some fully isolated module that interacts with some service, and this module needs to use different API keys for each tenant, you may find it cleaner to create a bootstrapper than to add “tenancy-aware” logic to that module.
For that specific use case, the Tenant Config feature may be sufficient.
It dynamically changes config at runtime, which may be all you need to make some feature tenant-aware.
However, if there’s some persistent state involved (for instance a constructor that reads the config,
stores that in a property, and that entire class is a singleton) you may need to use a bootstrapper
that besides changing config also does app(FooService::class)->setApiKey(tenant()->foo_api_key).
Execution order
Section titled “Execution order”Bootstrappers execute in the exact order they’re defined in, so they may have dependencies on each
other. For instance, the DatabaseCacheBootstrapper separates cache by tenant database rather than
by prefix. This means it uses the 'tenant' connection and as such depends on DatabaseTenancyBootstrapper.
When ending (reverting) tenancy, bootstrappers are reverted in the opposite order.
Writing custom bootstrappers
Section titled “Writing custom bootstrappers”To write your own bootstrapper, simply create a class that implements the TenantBootstrapper interface
and add it to the array above.
<?php
namespace App\Bootstrappers;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;use Stancl\Tenancy\Contracts\Tenant;
class FooBootstrapper implements TenancyBootstrapper{ public string $originalApiKey;
public function __construct(): void { $this->originalApiKey = config('services.foo.api_key'); }
public function bootstrap(Tenant $tenant): void { if ($api_key = $tenant->foo_api_key) { config(['services.foo.api_key' => $api_key]); app(FooService::class)->setApiKey($api_key); } }
public function revert(): void { config(['services.foo.api_key' => $this->originalApiKey]); app(FooService::class)->setApiKey($this->originalApiKey); }}Only bootstrap() and revert() are required by the interface, but notice the constructor
as well. It follows a common pattern where we store the original value in the bootstrapper.
Tenancy registers all bootstrappers as singletons, so they have persistent state. It’s important
to store original state like this. Once bootstrap() changes the config, the original value may
be inaccessible, so we make sure to store it.
Constructors are guaranteed to only execute once and before bootstrap(), so you can be sure that the
values accessed there come from the “original” central context.
Alternatively, you could also use this pattern:
// no constructorpublic function bootstrap(Tenant $tenant): void{ $this->originalApiKey ??= $tenant->api_key;}Tenancy keeps track of which bootstrappers have been initialized (their bootstrap() has been executed
for the first time). If an error occurs during the bootstrapping process and you run tenancy()->end(),
it will only revert the bootstrappers that have already been initialized.
This would be a major edge case, but for the sake of completeness: revert() should ideally be idempotent
and safe to execute as long as the bootstrapper has been initialized. In other words, it should ideally
only work with the state stored in the bootstrapper’s properties during initialization (constructor or
??= assignments in bootstrap()). This is because if FooBootstrapper was bootstrapped just fine for the
first time, and is therefore considered to be initialized, but on second bootstrap (e.g. for a different
tenant when running a loop) it isn’t even reached due to a failure in a previous bootstrapper, FooBootstrapper
will still be reverted.