Tenants

At the core of every multitenanted application is the tenant. Tenants are entities, but they also require supporting functionality to simplify working with them.

Introduction

One of the core driving points behind Sprout is for it to be seamless, and allow you to just write code as you normally would, without having to write multitenancy specific code. This is no more noticeable than when it comes to tenants, where the use of a simple interface and a little bit of configuring will have everything up and running.

For Sprout to function, there needs to be a tenant, which is a class that implements the Sprout\Contracts\Tenant interface. There needs to be a tenant provider configured to use this tenant, and a tenancy needs to be set to use the provider. However, the whole process for this is no more complex than the steps you can find in the installation guide.

If you're familiar with Laravels auth functionality, you can consider the Tenant interface Sprouts version of Laravel's Authenticatable interface.

The Anatomy of a Tenant

Tenants inside Sprout, the specific class that implements the interface, require only two things, which is enforced by the interface itself. The first is a unique identifier, which is used as part of a HTTP request to identify the tenant. The second is a unique key, which is used to identify the tenant internally, from the database and the likes.

The Tenant Identifier

Tenant identifiers have the following rules:

  • They MUST be unique for that type of tenant
  • They MUST be a string, or castable to a string
  • They SHOULD be URL safe
  • They CAN be immutable

The first two rules are pretty clear, identifiers must be unique, otherwise how you would ever identify the correct tenant, and they must be a string, because almost everything that's part of a HTTP request is one. The second two, however, are less clear, and they have a few caveats.

Having the identifier be URL safe is recommended, though the actual definition of "URL safe" depends entirely on how you're identifying tenants. If you're using subdomains, then the identifier must be a valid subdomain. If you're using the URL path, then the identifier must be valid for a path segment.

Immutability is the same, it's recommended, but again depends on other factors. If you're using part of the URL to identify your tenants, the tenants themselves are probably going to want control over that, so it can't be immutable. In situations where you're allowing users to control their identifiers, keep these things in mind:

  • There will be terms you don't want them using, especially if you're using subdomains (www, official, mail, etc.)
  • You'll want to make sure that identifiers cannot be reused immediately, to avoid impersonation, fraud, etc.
  • You'll also want to handle redirects for a while after the identifier is changed, to avoid breaking links

The Tenant Key

Tenants keys have the following rules:

  • They MUST be unique for that type of tenant
  • They MUST be a string, int, or castable to those
  • They MUST be immutable

Tenant keys are used internally, as database foreign keys and the likes, so it's important that they be unique and immutable, because no one wants to update hundreds or thousands of records to change a key. Because of how these keys are used, they will almost always be the primary key of a database table, and so they will be an int by default with Laravel, but they can also be a string for things like UUIDs or ULIDs.

Creating a Tenant

As you will have seen from the "creating your tenant" section of the installation guide, tenants are very easy to create, especially when using Eloquent. That being said, there will be times when Eloquent isn't used, or you want to override something, so let's take a look at what the interface actually adds.

1public function getTenantIdentifier(): string;
2 
3public function getTenantIdentifierName(): string;
4 
5public function getTenantKey(): int|string;
6 
7public function getTenantKeyName(): string;

I've stripped out the docblocks, but even without them the methods are pretty self-explanatory. The getTenantIdentifier method returns the tenants' identifier, whereas getTenantIdentifierName returns it name, which would be the attribute, or column name that stores the identifier. The same goes for getTenantKey and getTenantKeyName, but for the tenant key instead.

Tenant Models

If you're using Eloquent, you can drop in the Sprout\Database\Eloquent\Concerns\IsTenant trait, which adds default implementations for all these methods. It assumes that the tenant identifier is stored in identifier, and it piggybacks off Larvels primary key functionality for the tenant key. If you wish to use a different attribute for the tenant identifier, you only need to override the getTenantIdentifierName method and returns its name. Tenant models should be paired with the eloquent tenant provider.

Non-Eloquent Tenants

Tenants can be anything within Sprout, though how their data is read and written will be different, and will require a custom tenant provider. Unless you want to use the database without Eloquent. In that case, Sprout comes with a GenericTenant class entity that acts as your tenant implementation, or forms the base of it. If you wish to use the database directly, there's the database tenant provider.

You can provide an Eloquent model class for the table option, and the database driver will use the models table. This exists to allow you to access tenants without the overhead of Eloquent.

Tenants with Resources

Some features within Sprout require that a tenant has resources, which is mostly used for anything to do with files and filesystems. These features require that the tenant also implements the Sprout\Contracts\TenantHasResources interface, which requires that a tenant has a resource key. These keys have the following rules:

  • They MUST be unique for that type of tenant
  • They MUST be a string, or castable to a string
  • They MUST be immutable
  • They MUST be safe for use in file paths

Tenant resource keys are used as part of a file path, whether it's a directory or file, so they should be safe to be used like this, and like with the tenant identifier, they must be unique and a string. Similarly to the tenant key, tenant resource keys must be immutable because you don't want to be mass-renaming files, and moving directories around.

1public function getTenantResourceKey(): string;
2 
3public function getTenantResourceKeyName(): string;

Eloquent Tenants with Resources

If you're using Eloquent, you can use theSprout\Database\Eloquent\Concerns\HasTenantResources trait, which does the following:

  • Defines the resource key as being named resource_key (Inside getTenantResourceKeyName)
  • Adds a listener to the model creating event, setting the resource key to a UUID if it's not set

Tenant Providers

While any class can be a tenant within Sprout, every type of tenant needs a tenant provider that is capable of working with it. These providers do not save or store tenants, they simply retrieve them, and they can do this in one of three different ways.

  • Via a tenant identifier
  • Via a tenant key
  • Via a tenant resource key

It's unlikely that the majority of users will need to do anything with a tenant provider beyond the initial configuration. However, there may come a time when you need to write one yourself.

If you want to find out more about tenant providers, such as how they work, how to interact with them, and how to create your own, you can check out the tenant providers documentation.

Eloquent Tenant Provider

The Eloquent tenant provider is most likely to be the only one you'll ever need; after all, we're working with Laravel. The provider itself does nothing fancy, it simply creates a new query from the model class its given, and then queries it using the appropriate name method (getTenantKeyName, getTenantIdentifierName, getTenantResourceKeyName), and the value its given.

If you wish to use this provider, you'll want to make sure that you're using the eloquent driver for the tenant provider config, with your tenant model set as the model option.

1'providers' => [
2 'tenants' => [
3 'driver' => 'eloquent',
4 'model' => \App\Models\Blog::class,
5 ],
6],

Database Tenant Provider

If you want to go down this route, you'll need to make sure you're using the database driver for the tenant provider config, which also has the following config options.

  • table - The name of the database table that stores the tenants. This is required
  • entity - The class, who represents the tenant. This is optional and will default to the GenericTenant
  • connection - The configured database connection to use. This is optional and will default to using database.default
1'providers' => [
2 'tenants' => [
3 'driver' => 'database',
4 'table' => 'blogs',
5 'entity' => BlogEntity::class,
6 'connection' => 'core',
7 ],
8],

Tenant Children

Tenant children are entities within your application that belong to a tenant. If Blog was your tenant, than Post and Category would be tenant children, as they both belong to a Blog. Sprout comes with supporting functionality that simplifies and automates the process of using Eloquent models as tenant children. When creating these models, you can implement one of two traits, depending on how it relates to the tenant.

  • Sprout\Database\Eloquent\Concern\BelongsToTenant - The model belongs to a single tenant.
  • Sprout\Database\Eloquent\Concern\BelongsToManyTenants - The model belongs to many tenants.

Once these traits have been added, all you need to do is use the Sprout\Attributes\TenantRelation attribute to mark a relationship as the tenant relation.

1use Sprout\Attributes\TenantRelation;
2use Sprout\Database\Eloquent\Concern\BelongsToTenant;
3 
4class Post extends Model
5{
6 use BelongsToTenant;
7 
8 #[TenantRelation]
9 public function blog(): BelongsTo
10 {
11 return $this->belongsTo(Blog::class);
12 }
13}

Adding this trait to your child models will have the following effects, if within the multitenanted context.

  • All read queries for that model, made while there's an active tenant, will automatically be scoped to the current tenant.
  • Creating and saving a new model, while there's an active tenant, will cause that model to automatically be associated with the current tenant.

There are also two other things that will happen, but whether each happens depends on the configured tenancy options.

  • Throw if not Related - If this tenancy option is enabled, an exception will be thrown when a model is created from the database, if it belongs to a tenant other than the current active one.
  • Hydrate Tenant Relation - If this tenancy option is enabled, the tenant relation will be populated with the current tenant, assuming that the model belongs to it.

Avoiding Restrictions

If you wish to avoid the restrictions, like having queries automatically scoped and model ownership validated, there are a handful of helper methods to help with this. These methods are called on any child model, but they affect the restrictions of all child models. The functionality is similar to Laravel's transaction functionality, so there are two options. The first is a pair of methods, one that disables the restrictions, and one that resets the restrictions.

1Post::ignoreTenantRestrictions();
2 
3$posts = Post::all();
4 
5Post::resetTenantRestrictions();

The second option for this allows you to wrap everything that needs to avoid restrictions within a callback. Whatever your callback returns will be returned by the helper method.

1$posts = Post::withoutTenantRestrictions(function () {
2 return Post::all();
3});

The restrictions and processes put in place by the tenant child model functionality helps keep tenant data separate. Avoiding it may cause tenant data to leak, making it accessible to other tenants, so please be careful in its usage.

Optional Children

Sometimes you'll want to have models that can belong to a tenant, or tenants, but can also belong to none, making the relationship optional. Take the Blog example. It makes sense that a Post would belong to a Blog, and it makes sense that a Category would also belong to a Blog. But, what if the application provided a handful of default categories that every blog has access to.

To allow for this, you can add the Sprout\Database\Eloquent\Contracts\OptionalTenant interface to your model. This interface is known as a "marker interface", as its only used to mark a model as having its tenant relation be optional.

1use Sprout\Attributes\TenantRelation;
2use Sprout\Database\Eloquent\Concern\BelongsToTenant;
3use Sprout\Database\Eloquent\Contracts\OptionalTenant;
4 
5class Category extends Model implements OptionalTenant
6{
7 use BelongsToTenant;
8 
9 #[TenantRelation]
10 public function blog(): BelongsTo
11 {
12 return $this->belongsTo(Blog::class);
13 }
14}

Marking a model in this way will have the following effects.

  • The tenant relation will not be automatically populated.
  • An exception will not be thrown when hydrating the model if the model has no tenant.
  • The tenant relation will not be populated if the model has no tenant.
  • Read queries will be automatically scoped to the current tenant, and null.

This means that you MUST manually set the tenant relation. In the example this would involve associating the Category model with an existing Blog model, using the blog() relation.

Non-Eloquent Children

Out-of-the-box Sprout only supports using Eloquent and the database directly, and since the database functionality of Laravel does not allow for the same sort of usage, it's not possible for Sprout to automate using it. However, if you're using a third-party (or maybe first-party) addon that adds support for something like another ORM, it is likely that will come with supporting functionality, and you'd need to look at its documentation for details.

Tenancies

Tenancies are Sprouts version of Laravel's auth guards. Auth guards are responsible for locating and retrieving the current user, as well as keeping track of them, and Sprouts tenancies do the same, but for tenants rather than users. These tenancies are defined in the multitenancy.tenancies configuration, and just like auth guards, you can have more than one.

While you can currently have multiple tenancies within your application, the behaviour of both Sprout and Laravel is uncertain when layering tenancies (having subtenancies). This is something that will be looked into.

Tenancies are always instances of Sprout\Contracts\Tenancy, and Sprout ships with only one implementation, Sprout\Support\DefaultTenancy, so unless you're using an addon or custom implementation, this will be the class used. There are a handful of ways to retrieve the current tenancy, using a contextual attribute, a facade, or a helper function.

1// Using dependency injection
2public function __construct(#[CurrentTenancy] Tenancy $tenancy) {}
3 
4// Using facades
5Sprout::getCurrentTenancy()
6 
7// Using helper methods
8sprout()->getCurrentTenancy();

If you want to find out more about tenancies, such as how they work, how to interact with them, and how to create your own, you can check out the tenancy documentation.

Tenancy Options

Sprout allows you some control over the behaviour of tenancies using tenancy options. These options all come from the Sprout\TenancyOptions class, and can be added to the options part of the tenancy config.

Hydrate Tenant Relation

This option will enable the hydration of the tenant relation when retrieving child models. When enabled, the relation is set to the current tenant, and marked as loaded, without the need to query and retrieve a new model instance.

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

This option is also to do with child-model functionality, and tells the tenancy whether or throw an exception if the model being retrieved does not belong to the current tenant. It is recommended that this option be kept in, as it can help avoid cross-tenant data leaking.

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

All Overrides

This option tells Sprout that all the configured service overrides should be enabled and used for this tenancy.

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

Overrides

This option allows you to specify exactly which service overrides should be enabled and used for the tenancy. It takes an array of the service overrides name, as registered in the sprout.core.overrides config.

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

Working with Tenants

Sprout has been built to be seamless, so beyond the initial configuration and setting up, you're unlikely to encounter much, beyond the odd import of a Sprout class here and there. That being said, there are a few things that you'll need to know to work with tenants.

Tenant Context

A number of Sprouts features, such as the CurrentTenancyand CurrentTenant attributes, will only work while inside a multitenanted context, with some going as far as to throw an exception if outside one. Out-of-the-box there are two possible places that Sprout will consider a multitenanted context.

  • When handling a tenant route. Everything beyond the first attempt to identify a tenant for the route is within the context.
  • When processing a job while the job service override is enabled.

Getting the Current Tenant

Since most of the functionality around tenants is automatic and happens in the background, the tenant itself isn't exposed to your code, though it is available. The best way for you to get the tenant, is to use the contextual attribute, Sprout\Attributes\CurrentTenant. As with all contextual attributes, it doesn't care about the type of the parameter, so you can safely typehint your tenant model.

1public function __construct(#[CurrentTenant] Blog $blog) {
2 $this->blog = $blog;
3}

Sprout does not create a binding for the class you use as a tenant, so type hinting it without the CurrentTenant attribute will not work unless you've added something yourself to support it.