Quickstart Tutorial
This tutorial focuses on getting you started with stancl/tenancy 3.x quickly. It implements multi-database tenancy & domain identification. If you need a different implementation, then that's absolutely possible with this package and it's very easy to refactor to a different implementation.
We recommend following this tutorial just to get things working so that you can play with the package. Then if you need to, you can refactor the details of the multi-tenancy implementation (e.g. single-database tenancy, request data identification, etc).
Installation
First, require the package using composer:
composer require stancl/tenancy
Then, run the tenancy:install
command:
php artisan tenancy:install
This will create a few files: migrations, config file, route file and a service provider.
Let's run the migrations:
php artisan migrate
Register the service provider in config/app.php
. Make sure it's on the same position as in the code snippet below:
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TenancyServiceProvider::class, // <-- here
Creating a tenant model
Now you need to create a Tenant model. The package comes with a default Tenant model that has many features, but it attempts to be mostly unopinonated and as such, we need to create a custom model to use domains & databases. Create App\Tenant
like this:
<?php
namespace App;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}
Now we need to tell the package to use this custom model:
// config/tenancy.php
'tenant_model' => \App\Tenant::class,
Events
The defaults will work out of the box here, but a short explanation will be useful. The TenancyServiceProvider
file in your app/Providers
directory maps tenancy events to listeners. By default, when a tenant is created, it runs a JobPipeline
(a smart thing that's part of this package) which makes sure that the CreateDatabase
, MigrateDatabase
and optionally other jobs (e.g. SeedDatabase
) are ran sequentially.
In other words, it creates & migrates the tenant's database after he's created — and it does this in the correct order, because normal event-listener mapping would execute the listeners in some stupid order that would result in things like the database being migrated before it's created, or seeded before it's migrated.
Central routes
We'll make a small change to the app/Providers/RouteServiceProvider.php
file. Specifically, we'll make sure that central routes are registered on central domains only.
protected function mapWebRoutes()
{
foreach ($this->centralDomains() as $domain) {
Route::middleware('web')
->domain($domain)
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
}
protected function mapApiRoutes()
{
foreach ($this->centralDomains() as $domain) {
Route::prefix('api')
->domain($domain)
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
}
protected function centralDomains(): array
{
return config('tenancy.central_domains');
}
If you're using Laravel 8, call these methods manually from your RouteServiceProvider
's boot()
method, instead of the $this->routes()
calls.
public function boot()
{
$this->configureRateLimiting();
$this->mapWebRoutes();
$this->mapApiRoutes();
}
Central domains
Now we need to actually specify the central domains. A central domain is a domain that serves your "central app" content, e.g. the landing page where tenants sign up. Open the config/tenancy.php
file and add them in:
'central_domains' => [
'saas.test', // Add the ones that you use. I use this one with Laravel Valet.
],
Tenant routes
Your tenant routes will look like this by default:
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::get('/', function () {
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});
});
These routes will only be accessible on tenant (non-central) domains — the PreventAccessFromCentralDomains
middleware enforces that.
Let's make a small change to dump all the users in the database, so that we can actually see multi-tenancy working.
Route::get('/', function () {
dd(\App\User::all());
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});
Migrations
To have users in tenant databases, let's move the users
table migration to database/migrations/tenant
. This will prevent the table from being created in the central database, and it will be instead created in the tenant database when a tenant is created — thanks to our event setup.
Creating tenants
For testing purposes, we'll create a tenant in tinker
— no need to waste time creating controllers and views for now.
$ php artisan tinker
>>> $tenant1 = Tenant::create(['id' => 'foo']);
>>> $tenant1->domains()->create(['domain' => 'foo.localhost']);
>>>
>>> $tenant2 = Tenant::create(['id' => 'bar']);
>>> $tenant2->domains()->create(['domain' => 'bar.localhost']);
Now we'll create a user inside each tenant's database:
App\Tenant::all()->runForEach(function () {
factory(App\User::class)->create();
});
If you use Laravel 8, the the command is slightly different:
App\Models\Tenant::all()->runForEach(function () {
App\Models\User::factory()->create();
});
Trying it out
Now we visit foo.localhost
in our browser and we should see a dump of the users table where we see some user. If we visit bar.localhost
, we should see a different user.