Single-database tenancy
Out of the box, the package uses multi-database tenancy since most applications prefer that — it doesn’t require making many (or any) changes to business logic and can be added to an existing application.
However, multi-database tenancy is just a tiny part of the package, and since the package is highly modular you can get many benefits out of this package even with single-database tenancy.
Switching to single-database tenancy
Section titled “Switching to single-database tenancy”To use single-database tenancy, make the following changes:
Tenant model change
Section titled “Tenant model change”Remove any database-related logic from your tenant model:
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;use Stancl\Tenancy\Database\Concerns\HasDatabase;use Stancl\Tenancy\Database\Concerns\HasDomains;use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class Tenant extends BaseTenant implements TenantWithDatabase{ use HasDatabase, HasDomains;}Disabling bootstrappers
Section titled “Disabling bootstrappers”Comment out or remove any database-related bootstrappers:
'bootstrappers' => [ // Bootstrappers\DatabaseTenancyBootstrapper::class, // Bootstrappers\DatabaseSessionBootstrapper::class,],Disabling jobs
Section titled “Disabling jobs”Comment out or remove any database-related jobs in the TenancyServiceProvider:
Events\TenantCreated::class => [ JobPipeline::make([ Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, // Jobs\SeedDatabase::class, // Jobs\CreateStorageSymlinks::class, ])->send(function (Events\TenantCreated $event) { return $event->tenant; })->shouldBeQueued(false),],// ...Events\TenantDeleted::class => [ JobPipeline::make([ Jobs\DeleteDatabase::class, ])->send(function (Events\TenantDeleted $event) { return $event->tenant; })->shouldBeQueued(false),],In a single-database context, there are five types of models we’ll be talking about:
- Your
Tenantmodel - Primary models — models that directly belong to a tenant (through a
belongsTorelationship) - Secondary models — models that indirectly belong to a tenant (through a primary relationship)
CommentbelongsToPostbelongsToTenant
- Tertiary models — models that indirectly belong to a tenant through a secondary relationship
VotebelongsToCommentbelongsToPostbelongsToTenant
- Global models — models that are not scoped to the current tenant.
To scope queries correctly, apply the Stancl\Tenancy\Database\Concerns\BelongsToTenant trait on primary models.
That will ensure uses of your primary models are scoped to the current tenant, and uses of their children are scoped
to the current tenant as long as those uses are through the primary model.
In other words:
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); }}tenant()->posts(); // scoped using Eloquent, regardless of traitsPost::all(); // scoped using BelongsToTenant$post->comments(); // scoped using EloquentComment::all(); // NOT SCOPEDIf you tried to use Comment::all() directly here, you’d still get posts of all tenants. To solve that,
you also need to apply the BelongsToPrimaryModel trait (from the same namespace) on all secondary models.
class Comment extends Model{ use BelongsToPrimaryModel;
public function getRelationshipToPrimaryModel(): string { return 'post'; }
public function post() { return $this->belongsTo(Post::class); }}With that, you get this scoping:
tenant()->posts(); // scoped using Eloquent, regardless of traitsPost::all(); // scoped using BelongsToTenant$post->comments(); // scoped using EloquentComment::all(); // scoped using BelongsToPrimaryModel$comment->votes(); // scoped using EloquentVote::all(); // NOT SCOPEDWe do not provide a solution for tertiary (and more indirect) relations out of the box. It is up to you to use some workaround or make sure to always access them through a parent model that is scoped.
Note that this only applies to “select many” queries. If you have a specific instance, Tenancy doesn’t enforce anything. These traits only affect what you may query using models.
Creating models
Section titled “Creating models”The BelongsToTenant model uses the FillsCurrentTenant trait which automatically fills the tenant_id column
with the current tenant ID if no ID was passed manually.
In other words, you do not need to pass the current tenant ID to Post::create() (from the example above).
Database considerations
Section titled “Database considerations”Unique indices
Section titled “Unique indices”To scope database indices, such as UNIQUE indices, you may need to change their definitions like this:
$table->unique('slug');$table->unique(['tenant_id', 'slug']);On secondary models you can reference the parent table in the first part of the index.
Validation
Section titled “Validation”Laravel’s built-in validation rules use simple DB queries, not models. You need to scope them accordingly:
Rule::unique('posts', 'slug')->where('tenant_id', tenant('id'));To avoid having to write all that out repeatedly, you can use the Stancl\Tenancy\Database\Concerns\HasScopedValidationRules
trait to be able to use these helper functions:
// Current tenant$tenant = tenant();
$rules = [ 'id' => $tenant->exists('posts'), 'slug' => $tenant->unique('posts'),];Low-level database queries
Section titled “Low-level database queries”It’s important to keep in mind that DB facade queries, or any similarly low level queries, will not be scoped. All these traits do is:
- Use Eloquent scopes to scope queries made using the respective models
- Use the
FillsCurrentTenanttrait to automatically fill the current tenant ID if a primary model is created without a tenant ID
Making global queries
Section titled “Making global queries”To disable the tenant scope, simply add withoutTenancy() to your query. That method is a macro which expands to:
$builder->withoutGlobalScope(\Stancl\Tenancy\Database\TenantScope::class);Customizing the column name
Section titled “Customizing the column name”If you’d like to change the column name from tenant_id to e.g. team_id, you can do so by changing the tenancy.models.tenant_key_column config:
/** * Name of the column used to relate models to tenants. * * This is used by the HasDomains trait, and models that use the BelongsToTenant trait (used in single-database tenancy). */'tenant_key_column' => 'tenant_id','tenant_key_column' => 'team_id',Note that, as the docblock suggests, this config key also affects the HasDomains trait, so you’d be expected to rename the domains.tenant_id database
column to domains.team_id.