Service Overrides

Service overrides all about making services tenant-aware, whether they're part of Laravel's core, or a third-party package.

Introduction

Your application is made up of many "services", whether they're things like file storage, sessions, authentication, or one of the many others provided by Laravel. Sometimes these services aren't part of the core Laravel installation, things like Fortify, Livewire, Inertia or Filament. Whatever they are, they aren't going to understand your application out of the box, and they're definitely not going to support your multitenancy functionality.

This is where the service override comes in. Service overrides are similar to service providers, except they're specific to the lifecycle of Sprout, as well as that of a tenant. They’re called when the current tenant changes, allowing them to set up for the new tenant, as well as clean up after the previous. They can also optionally be bootable, which means they have actions to perform once Laravel has booted.

Configuring Overrides

Service overrides are configured in the sprout.overrides config again the "service" they override, with their class as the driver, and any extra configuration options required or allowed by the driver itself. The order that the overrides appear in the sprout.overrides config is the order that they'll be registered in, and the order they'll be called in.

There are no specific rules surrounding the name that a service override is registered under, but it is recommended that you avoid changing them.

Stacking Overrides

There are times when you'll need more than one service override for a service, and rather than having to register each under a different name, Sprout supports stacking. If you need to stack, you can set the driver to \Sprout\Overrides\StackedOverride, then provide the drivers you want to stack under the overrides config key. If either driver requires config options, you can provide them normally.

1'filesystem' => [
2 'driver' => \Sprout\Overrides\StackedOverride::class,
3 'overrides' => [
4 \Sprout\Overrides\FilesystemManagerOverride::class,
5 \Sprout\Overrides\FilesystemOverride::class,
6 ],
7],

There are two menu uses for stacking.

  • The override for a particular service has been broken into smaller separate parts, and can be used independently of the others.
  • You have multiple overrides from different sources, and want to use all of them.

Per Tenancy

Once a service override is registered with Sprout, it is available for use. By default, no service override is used unless the tenancy is configured to use it. You can do this using tenancy options, by either enabling all overrides for the tenancy, which is what the default config does.

1'tenants' => [
2 'provider' => 'tenants',
3 'options' => [
4 TenancyOptions::allOverrides(),
5 ],
6],

Or by listing only the ones you want.

1'tenants' => [
2 'provider' => 'tenants',
3 'options' => [
4 TenancyOptions::overrides([
5 'job', 'filesystem', 'cache'
6 ]),
7 ],
8],

While you can enable or disable service overrides on a per tenancy basic, any that are bootable will be booted regardless, as that happens before the tenancy is identified.

Setting and Cleaning Up

Service overrides are built around the concept of setting the override up for the tenant when it becomes the current one, and cleaning up once it no longer is. While this is a core part of this feature, both the setting and cleaning up are controlled by tenancy bootstrappers.

The \Sprout\Listeners\CleanupServiceOverrides bootstrapper is responsible for making the service overrides clean-up after themselves, and the \Sprout\Listeners\SetupServiceOverrides bootstrapper is responsible for the setup.

Both the order that these bootstrappers appear in the config, and their presence in it is important for the service override functionality. Removing either, or changing their order relative to each other, will have unknown side effects and may cause your application to not function properly.

Available Service Overrides

Sprout ships with several overrides for core parts of Laravel, all of which are enabled by default.

Filesystem

The filesystem override comes in two parts. The first replace Laravel's filesystem manager with Sprouts, which only exists to simplify the second, which adds a sprout driver that allows you to create tenant scoped filesystem disks.

1'filesystem' => [
2 'driver' => \Sprout\Overrides\StackedOverride::class,
3 'overrides' => [
4 \Sprout\Overrides\FilesystemManagerOverride::class,
5 \Sprout\Overrides\FilesystemOverride::class,
6 ],
7],

This service override requires that the tenant is configured for resources, and will throw an exception if it isn't.

Filesystem Manager

The filesystem manager service override does only a single thing, and that is adding the name of the disk to the disk config as name, when calling a custom driver. This is primarily of use to the other override, as it keeps track of the tenant disks that were accessed, and uses that to perform the clean-up.

Filesystem

This service override is the one responsible for adding the sprout driver to the filesystem service. The new driver allows you to create a filesystem disk scoped to the current tenant, either based on an existing disk, or using new settings.

To use this override, create a filesystem disk in config/filesystems.php under disks, whose driver is sprout. Then under the disk option, add either the name of an existing disk.

1'tenant-disk' => [
2 'driver' => 'sprout',
3 'disk' => 'local',
4],

This override uses the config from the other disk, and then scopes it, so you can use both the sprout one, and the base one simultaneously. Just be aware that the base one will have access to all data for all tenants.

Or the config for a new disk.

1'tenant-disk' => [
2 'driver' => 'sprout',
3 'disk' => [
4 'driver' => 'local',
5 'root' => storage_path('tenants'),
6 'throw' => false,
7 ],
8],

When the disk is created, its root will be set to {tenancy}/{tenant}, where tenancy is the name of the tenancy from the multitenancy.tenancies config, and tenant is replaced with its resource key. If you want to change this, you can provide the path config option for the disk, which uses the same placeholders as the identity resolver parameter names, except that tenant uses the resource key, and not identifier.

1'tenant-disk' => [
2 'driver' => 'sprout',
3 'disk' => 'local',
4 'path' => '/{tenancy}/files/{tenant}'
5],

Sprout will not take steps to ensure that the disk path exists. This is something that you will need to do as part of your applications lifecycle whenever a new tenant is created.

Job

The job service override is arguably the simplest of the defaults. All it does is register an event listener for a job event that makes sure that whenever a job is run, it has the same tenancies and tenants available as it did when it was first queued.

1'job' => [
2 'driver' => \Sprout\Overrides\JobOverride::class,
3],

This is registered as job because it technically doesn't do anything with the queue, and naming it something like that may get in the way or cause confusion.

Cache

The cache service override does not wrap drivers like many of the others, but instead it prefixes cache keys with the name of the tenancy, and the tenants’ key.

1'cache' => [
2 'driver' => \Sprout\Overrides\CacheOverride::class,
3],

To use this override, you need to create a cache store in config/cache.php, under stores, whose driver is sprout, with the store you want to override defined under the override config key.

1'tenant-store' => [
2 'driver' => 'sprout',
3 'override' => 'file',
4],

If you were trying to retrieve the cache key users.list, the key would be something like tenancy_8_users.list. However, if the store you're overriding already has a prefix, than it will come before the tenant prefix, like this prefix_tenancy_8_users.list.

This override does not currently support controlling prefixes through parameter patterns, though there are plans to add this in the future.

Auth

The auth service override comes in two parts. The first replaces Laravel's password broker with Sprouts, which has Sprout-specific implementations of the cache and database token repositories. The second is called the "guard override", though all it does is ensure that all auth guards are purged when accessing a new tenant.

1'auth' => [
2 'driver' => \Sprout\Overrides\StackedOverride::class,
3 'overrides' => [
4 \Sprout\Overrides\AuthGuardOverride::class,
5 \Sprout\Overrides\AuthPasswordOverride::class,
6 ],
7],

Auth Password Broker

The Sprout-specific password broker only exists as Laravel's default one doesn’t allow you to control the resolution logic for its drivers. This manager returns Sprout-specific implementations, though they have fallback functionality and will function as normal if outside tenant context. The only thing that you need to do, if you want to use this override, with the database driver, is to add two fields to the password_resets table in the default migration.

1Schema::create('password_reset_tokens', function (Blueprint $table) {
2 $table->string('email')->primary();
3 $table->string('tenancy')->nullable();
4 $table->bigInt('tenant_id')->nullable();
5 $table->string('token');
6 $table->timestamp('created_at')->nullable();
7});

The tenant_id column should match the primary key of your tenant model, which by default within Laravel would be BIGINT, which is why bigInt() is used here.

The cookie service override ensures that cookies created within a tenant context, have the appropriate settings. Sprout has internal settings that function almost like runtime configuration, though they keep track of contextual information, such as the current domain and/or path.

1'cookie' => [
2 'driver' => \Sprout\Overrides\CookieOverride::class,
3],

The path identity resolver sets the cookie path, and the subdomain identity resolver sets the cookie domain. So if a cookie is created within a tenant context, and one of these identity resolvers is used, the cookie will have the appropriate settings. If there are no values set, it will default to the config values in config/session.php.

Because of how Laravel handles cookies, it is not possible to use this service override, and the cookie identity resolver. If using this service override, with the identity resolver, an exception will be thrown during Laravel's boot phase.

Session

The session service override replaces the default file (also used by native), and domain drivers with Sprout's tenant-aware versions. If you're using file/native, the path will be prefixed using the tenants’ resource key, and if you're using the database driver, the columns tenancy and tenant_id will be populated.

1'session' => [
2 'driver' => \Sprout\Overrides\SessionOverride::class,
3 'database' => false,
4],

This service override requires that the tenant is configured for resources if using the file or native drivers, and will throw an exception if it isn't.

Because of how Laravel handles sessions, it is not possible to use this service override, and the session identity resolver. If using this service override, with the identity resolver, an exception will be thrown during Laravel's boot phase.

Database Session Handling

If you haven’t followed the installation guide, you want to update the default migration, to include the tenant-specific columns.

1Schema::create('sessions', function (Blueprint $table) {
2 $table->string('id')->primary();
3 $table->string('tenancy')->nullable();
4 $table->bigInt('tenant_id')->nullable();
5 $table->foreignId('user_id')->nullable()->index();
6 $table->string('ip_address', 45)->nullable();
7 $table->text('user_agent')->nullable();
8 $table->longText('payload');
9 $table->integer('last_activity')->index();
10});

You can completely disable the database session handling by setting the database config option to false. This exists for multi-database setups, where you wouldn’t need a tenant-scoped session driver, as the whole database connection itself would be tenant-scoped.

Config Hot-swapping

Unfortunately, this service override has to hot-swap the session configuration at runtime, due to how Laravel handles sessions. I’m aware that this is not ideal, and it’s something I’ve managed to avoid everywhere else, but to circumvent it here would require overriding a huge chunk, if not all of, the session service.