Nowadays multi-tenant applications are more useful than single-tenant applications. We can use multi-tenant with multiple databases or single databases as per our need. But it’s better to use a single DB with a multi-tenant when you have a small application.
In this tutorial, we are going to use multi-tenant with a single database.
We will implement multi-tenant with single DB by using the following package: https://github.com/archtechx/tenancy
Assuming you already have Laravel 8 repo setup. Now please follow the given steps to implement multi-tenancy with a single DB.
Package Installation
Run following commands :
- composer require stancl/tenancy
- php artisan tenancy:install
- php artisan migrate
Add following service provider to config/app.php
App\Providers\TenancyServiceProvider::class
Create Custom Model
Now create modal named MultiTenant into app\Models
MultiTenant.php
SavingTenant::class,
'saved' => TenantSaved::class,
'creating' => CreatingTenant::class,
// 'created' => TenantCreated::class,
'updating' => UpdatingTenant::class,
'updated' => TenantUpdated::class,
'deleting' => DeletingTenant::class,
'deleted' => TenantDeleted::class,
];
}
Update Tenancy Configuration
As we have added custom model we also need to define that model into config/tenancy.php
Please change tenant_model value to our custom model.
‘tenant_model’ => \App\Models\MultiTenant::class,
Add Resolver
To use multi tenant with single DB we also need to add our customer resolver, that will be used into Middlewares that we will create ahead.
Create MultiTenantResolver into app\Resolvers
App\Resolvers\MultiTenantResolver.php
find(Auth::user()->tenant_id)) {
return $tenant;
}
throw new TenantCouldNotBeIdentifiedByPathException($id);
}
public function getArgsForTenant(Tenant $tenant): array
{
return [
[$tenant->id],
];
}
}
Add Middleware
We will create our custom middleware that will set the current tenant into cache, and that will used by package to fire default query where(‘tenant_id’, “tenant id we have set into middleware”)
App\Http\Middleware\MultiTenantMiddleware.php
tenancy = $tenancy;
$this->resolver = $resolver;
}
/**
* Handle an incoming request.
*
* @param Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$tenant = Auth::user()->tenant_id;
return $this->initializeTenancy(
$request, $next, $tenant
);
}
}
Also, don’t forget to add middleware alias into App\Http\kernel.php
protected $routeMiddleware = [
..............
'multi_tenant' => MultiTenantMiddleware::class,
];
Now we will apply this multi_tenant middleware to our routes.
Add Trait to tenant-specific models
We have to add BelongsToTenant trait to all of our tenant-specific models.
Say if we want to add tenant_id into the users table then we must have to add BelongsToTenant to the app\Models\User model.
That trait will by default add following query everytime when we will try to fetch records or update records.
Where(‘tenant_id’, ‘tenant id will taken from cache’)
Add tenant_id to tenant-specific migrations
As we have added the tenant trait, we must have to add tenant_id into tenant-specific migrations as specified below.
public function up()
{
Schema::create('users', function (Blueprint $table) {
...........................
$table->string('tenant_id');
$table->foreign('tenant_id')
->references('id')
->on('tenants')
->onUpdate('cascade')
->onDelete('cascade');
$table->timestamps();
});
Update TenancyServiceProvider
Replace the App\Providers\TenactServiceProvider by the following code.
Events\TenantCreated::class => [
JobPipeline::make([
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class,
// Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want!
])->send(function (Events\TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
],
Events\SavingTenant::class => [],
Events\TenantSaved::class => [],
Events\UpdatingTenant::class => [],
Events\TenantUpdated::class => [],
Events\DeletingTenant::class => [],
Events\TenantDeleted::class => [
JobPipeline::make([
Jobs\DeleteDatabase::class,
])->send(function (Events\TenantDeleted $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
],
// Domain events
Events\CreatingDomain::class => [],
Events\DomainCreated::class => [],
Events\SavingDomain::class => [],
Events\DomainSaved::class => [],
Events\UpdatingDomain::class => [],
Events\DomainUpdated::class => [],
Events\DeletingDomain::class => [],
Events\DomainDeleted::class => [],
// Database events
Events\DatabaseCreated::class => [],
Events\DatabaseMigrated::class => [],
Events\DatabaseSeeded::class => [],
Events\DatabaseRolledBack::class => [],
Events\DatabaseDeleted::class => [],
// Tenancy events
Events\InitializingTenancy::class => [],
Events\TenancyInitialized::class => [
// Listeners\BootstrapTenancy::class,
],
Events\EndingTenancy::class => [],
Events\TenancyEnded::class => [
Listeners\RevertToCentralContext::class,
],
Events\BootstrappingTenancy::class => [],
Events\TenancyBootstrapped::class => [],
Events\RevertingToCentralContext::class => [],
Events\RevertedToCentralContext::class => [],
// Resource syncing
Events\SyncedResourceSaved::class => [
Listeners\UpdateSyncedResource::class,
],
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
Events\SyncedResourceChangedInForeignDatabase::class => [],
];
}
public function register()
{
//
}
public function boot()
{
$this->bootEvents();
// $this->mapRoutes();
$this->makeTenancyMiddlewareHighestPriority();
}
protected function bootEvents()
{
foreach ($this->events() as $event => $listeners) {
foreach (array_unique($listeners) as $listener) {
if ($listener instanceof JobPipeline) {
$listener = $listener->toListener();
}
Event::listen($event, $listener);
}
}
}
protected function mapRoutes()
{
if (file_exists(base_path('routes/tenant.php'))) {
Route::namespace(static::$controllerNamespace)
->group(base_path('routes/tenant.php'));
}
}
protected function makeTenancyMiddlewareHighestPriority()
{
$tenancyMiddleware = [
// Even higher priority than the initialization middleware
Middleware\PreventAccessFromCentralDomains::class,
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
];
foreach (array_reverse($tenancyMiddleware) as $middleware) {
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
}
}
}
Create / Fetch Tenant
Now we have to create a tenant and give that tenant_id to related users.
each user contains their specific tenant_id.
Use the following code to create a tenant :
$tenant1 = \App\Models\MultiTenant::create([
'name' => 'Tenant 1'
]);
$tenant2 = \App\Models\MultiTenant::create([
'name' => 'Tenant 2'
]);
That will create tenant into tenants table and values will be stored into data column as a son.
$tenant1 = App\Models\MultiTenant::where(‘data->name’, ‘Tenant 1’)->first();
$tenant2 = App\Models\MultiTenant::where(‘data->name’, ‘Tenant 2’)->first();
$tenant1User = User::where('id', 'user id here')->update(['tenant_id' => $tenant1->id]);
$tenant2User = User::where('id', 'user id here')->update(['tenant_id' => $tenant2->id]);
Now we have 2 tenants with 2 separate users who contain separate tenant ids.
Add Middleware to Routes
Now do login with User 1 and try to fetch all users from the database, it will return users of logged-in users’ tenants only.
As we have the BelongToTenant trait into the User model.
Route::group(['middleware' => ['auth', 'multi_tenant']], function () { Route::get('users', function() {
// only tenant-1 users will be returned because we are setting the logged-in user tenant into the cache from `multi_tenant`middleware.
$allUsers = User::all();
});
You can use the same for other models too.
Hope this helps you.