Single-database tenancy
Single-database tenancy comes with lower devops complexity, but larger code complexity than multi-database tenancy, since you have to scope things manually, and won't be able to integrate some third-party packages.
It is preferable when you have too many shared resources between tenants, and don't want to make too many cross-database queries.
To use single-database tenancy, make sure you disable the DatabaseTenancyBootstrapper
which is responsible for switching database connections for tenants.
You can still use the other tenancy bootstrappers to separate tenant caches, filesystems, etc.
Also make sure you have disabled the database creation jobs (CreateDatabase
, MigrateDatabase
, SeedDatabase
...) from listening to the TenantCreated
event.
Concepts
In single-database tenancy, there are 4 types of models:
- your Tenant model
- primary models — models that directly belongTo tenants
- secondary models — models that indirectly belongTo tenants
- e.g. Comment belongsTo Post belongsTo Tenant
- or more complex, Vote belongsTo Comment belongsTo Post belongsTo Tenant
- global models — models that are not scoped to any tenant whatsoever
To scope your queries correctly, apply the Stancl\Tenancy\Database\Concerns\BelongsToTenant
trait on primary models. This will ensure that all calls to your parent models are scoped to the current tenant, and that calls to their child relations are scoped through the parent relationships.
And that's it. Your models are now automatically scoped to the current tenant, and not scoped at all when there's no current tenant (e.g. in a central admin panel).
However, there's one edge case to keep in mind. Consider the following set-up:
class Post extends Model
{
use BelongsToTenant;
public function comments()
{
return $this->hasMany(Comment::class);
}
}
class Comment extends Model
{
public function post()
{
return $this->belongsTo(Post::class);
}
}
Looks correct, but you might still accidentally access another tenant's comments.
If you use this:
Comment::all();
then the model has no way of knowing how to scope that query, since it doesn't directly belong to the tenant. Also note that in practice, you really shouldn't be doing this much. You should ideally access secondary models through parent models in every single case.
However, sometimes you might have a use case where you really need to do that in the tenant context. For that reason, we also provide you with a BelongsToPrimaryModel
trait, which lets you scope calls like the one above to the current tenant, by loading the parent relationship — which gets automatically scoped to the current tenant — on them.
So, to give you an example, you would do this:
class Comment extends Model
{
use BelongsToPrimaryModel;
public function getRelationshipToPrimaryModel(): string
{
return 'post';
}
public function post()
{
return $this->belongsTo(Post::class);
}
}
And this will automatically scope the Comment::all()
call to the current tenant. Note that the limitation of this is that you need to be able to define a relationship to a primary model, so if you need to do this on the "Vote" in Vote belongsTo Comment belongsTo Post belongsTo Tenant, you need to define some strange relationship. Laravel supports HasOneThrough
, but not BelongsToThrough
, so you'd need to do some hacks around that. For that reason, I recommend avoiding these Comment::all()
-type queries altogether.
Database considerations
Unique indexes
If you'd have a unique index such as:
$table->unique('slug');
in a standard non-tenant, or multi-database-tenant, application, you need to scope this unique index to the tenant, meaning you'd do this on primary models:
$table->unique(['tenant_id', 'slug']);
and this on secondary models:
// Imagine we're in a 'comments' table migration
$table->unique(['post_id', 'user_id']);
Validation
The unique
and exists
validation rules of course aren't scoped to the current tenant, so you need to scope them manually like this:
Rule::unique('posts', 'slug')->where('tenant_id', tenant('id'));
If that feels like a chore, you may use the Stancl\Tenancy\Database\Concerns\HasScopedValidationRules
trait on your custom Tenant model to add methods for these two rules.
You'll be able to use these two methods:
// You may retrieve the current tenant using the tenant() helper.
// $tenant = tenant();
$rules = [
'id' => $tenant->exists('posts'),
'slug' => $tenant->unique('posts'),
]
Low-level database queries
And the final thing to keep in mind is that DB
facade calls, or any other types of direct database queries, of course won't be scoped to the current tenant.
The package can only provide scoping logic for the abstraction logic that Eloquent is, it can't do anything with low level database queries.
Be careful with using them.
Making global queries
To disable the tenant scope, simply add withoutTenancy()
to your query.
Customizing the column name
If you'd like to customize the column name to use e.g. team_id
instead of tenant_id
— if that makes more sense given your business terminology — you can do that by setting this static property in a service provider or some such class:
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
BelongsToTenant::$tenantIdColumn = 'team_id';
Note that this is universal to all your primary models, so if you use team_id
somewhere, you use it everywhere — you can't use both team_id
and tenant_id
.