Skip to content

Commit 2665689

Browse files
authored
Merge pull request #635 from victoryoalli/add-laravel-boost-skill
Add Laravel Boost skill
2 parents 787fc5b + ce3a606 commit 2665689

1 file changed

Lines changed: 385 additions & 0 deletions

File tree

  • resources/boost/skills/laravel-multitenancy-development
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
---
2+
name: laravel-multitenancy-development
3+
description: Build and work with Spatie Laravel Multitenancy features, including tenant finders, the current tenant, switch tasks, multi-database setups, tenant-aware queues and artisan commands.
4+
---
5+
6+
# Laravel Multitenancy Development
7+
8+
## When to use this skill
9+
10+
Use this skill when working with multi-tenant Laravel applications using `spatie/laravel-multitenancy`: determining the current tenant per request, isolating databases or caches per tenant, making queued jobs and artisan commands tenant-aware, or designing landlord/tenant migration strategies.
11+
12+
## Core Concepts
13+
14+
- **Intentionally minimal**: the package resolves a current tenant and runs tasks on switch — it does not add global query scopes or model isolation by itself.
15+
- **Current tenant** is bound in the IoC container under the key `currentTenant` and written to Laravel `Context` under the key `tenantId`.
16+
- A **`TenantFinder`** resolves the tenant from the current HTTP request (e.g. by domain).
17+
- **`SwitchTenantTask`** classes mutate the environment when a tenant becomes current (switch DB, prefix cache, etc.) and restore it when forgotten.
18+
- Models on the landlord DB use `UsesLandlordConnection`; models on the tenant DB use `UsesTenantConnection`.
19+
20+
## Setup
21+
22+
```bash
23+
composer require spatie/laravel-multitenancy
24+
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-config"
25+
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-migrations"
26+
```
27+
28+
Register middleware in `bootstrap/app.php`:
29+
30+
```php
31+
->withMiddleware(function (Middleware $middleware) {
32+
$middleware->web(append: [
33+
\Spatie\Multitenancy\Http\Middleware\NeedsTenant::class,
34+
\Spatie\Multitenancy\Http\Middleware\EnsureValidTenantSession::class,
35+
]);
36+
})
37+
```
38+
39+
## Configuring a Tenant Finder
40+
41+
Set the finder class in `config/multitenancy.php`:
42+
43+
```php
44+
'tenant_finder' => \Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
45+
```
46+
47+
`DomainTenantFinder` looks up the tenant by matching `$request->getHost()` against a `domain` column on the tenants table.
48+
49+
To use a custom finder, extend `TenantFinder` and implement `findForRequest`:
50+
51+
```php
52+
use Illuminate\Http\Request;
53+
use Spatie\Multitenancy\Contracts\IsTenant;
54+
use Spatie\Multitenancy\TenantFinder\TenantFinder;
55+
56+
class SubdomainTenantFinder extends TenantFinder
57+
{
58+
public function findForRequest(Request $request): ?IsTenant
59+
{
60+
$subdomain = explode('.', $request->getHost())[0];
61+
62+
return app(IsTenant::class)::whereSubdomain($subdomain)->first();
63+
}
64+
}
65+
```
66+
67+
## Working with the Current Tenant
68+
69+
```php
70+
use Spatie\Multitenancy\Models\Tenant;
71+
72+
// Make a tenant current (fires events, runs tasks)
73+
$tenant->makeCurrent();
74+
75+
// Read the current tenant
76+
Tenant::current(); // returns ?Tenant
77+
app('currentTenant'); // same, via container
78+
79+
// Check and forget
80+
Tenant::checkCurrent(); // bool
81+
$tenant->isCurrent(); // bool
82+
Tenant::forgetCurrent(); // runs forget tasks, returns the tenant
83+
```
84+
85+
## Executing Code for a Tenant or Landlord
86+
87+
`execute()` makes the tenant current, runs the callable, then restores the previous state:
88+
89+
```php
90+
$result = $tenant->execute(function (Tenant $tenant) {
91+
return cache()->get('stats');
92+
});
93+
```
94+
95+
`callback()` returns a closure — useful for the scheduler:
96+
97+
```php
98+
$schedule->call($tenant->callback(fn () => cache()->flush()))->daily();
99+
```
100+
101+
To run code **outside** any tenant context, use `Landlord`:
102+
103+
```php
104+
use Spatie\Multitenancy\Landlord;
105+
106+
Landlord::execute(function () {
107+
Artisan::call('cache:clear');
108+
});
109+
```
110+
111+
`TenantCollection` adds iteration helpers: `eachCurrent`, `mapCurrent`, `filterCurrent`, `rejectCurrent`.
112+
113+
```php
114+
Tenant::all()->eachCurrent(function (Tenant $tenant) {
115+
cache()->flush();
116+
});
117+
```
118+
119+
## Multi-Database Setup
120+
121+
Define a `tenant` connection (with `database => null`) and a `landlord` connection in `config/database.php`:
122+
123+
```php
124+
'connections' => [
125+
'tenant' => [
126+
'driver' => 'mysql',
127+
'database' => null,
128+
'host' => '127.0.0.1',
129+
'username' => 'root',
130+
'password' => '',
131+
],
132+
133+
'landlord' => [
134+
'driver' => 'mysql',
135+
'database' => 'name_of_landlord_db',
136+
'host' => '127.0.0.1',
137+
'username' => 'root',
138+
'password' => '',
139+
],
140+
],
141+
```
142+
143+
Set the connection names in `config/multitenancy.php`:
144+
145+
```php
146+
'tenant_database_connection_name' => 'tenant',
147+
'landlord_database_connection_name' => 'landlord',
148+
```
149+
150+
Apply the correct connection trait to every Eloquent model:
151+
152+
```php
153+
// Models whose table lives in the tenant DB
154+
use Spatie\Multitenancy\Models\Concerns\UsesTenantConnection;
155+
156+
class Post extends Model
157+
{
158+
use UsesTenantConnection;
159+
}
160+
161+
// Models whose table lives in the landlord DB
162+
use Spatie\Multitenancy\Models\Concerns\UsesLandlordConnection;
163+
164+
class Tenant extends Model
165+
{
166+
use UsesLandlordConnection;
167+
}
168+
```
169+
170+
## Switch Tenant Tasks
171+
172+
Tasks run every time `makeCurrent()` or `forgetCurrent()` is called. Register them in `config/multitenancy.php`:
173+
174+
```php
175+
'switch_tenant_tasks' => [
176+
\Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,
177+
// \Spatie\Multitenancy\Tasks\PrefixCacheTask::class,
178+
// \Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class,
179+
],
180+
```
181+
182+
Built-in tasks:
183+
184+
- **`SwitchTenantDatabaseTask`** — sets the `tenant` connection's `database` to `$tenant->database` and purges the connection. Required for multi-DB.
185+
- **`PrefixCacheTask`** — overrides `cache.prefix` to `tenant_{$tenant->id}`. Works with memory-based stores (Redis, APC).
186+
- **`SwitchRouteCacheTask`** — switches `APP_ROUTES_CACHE` to a per-tenant file (`bootstrap/cache/routes-v7-tenant-{id}.php`), or a shared file when `'shared_routes_cache' => true`.
187+
188+
To create a custom task, implement `SwitchTenantTask`:
189+
190+
```php
191+
use Spatie\Multitenancy\Contracts\IsTenant;
192+
use Spatie\Multitenancy\Tasks\SwitchTenantTask;
193+
194+
class SwitchStorageDiskTask implements SwitchTenantTask
195+
{
196+
public function makeCurrent(IsTenant $tenant): void
197+
{
198+
config(['filesystems.disks.s3.bucket' => $tenant->bucket]);
199+
}
200+
201+
public function forgetCurrent(): void
202+
{
203+
config(['filesystems.disks.s3.bucket' => config('filesystems.default_bucket')]);
204+
}
205+
}
206+
```
207+
208+
Tasks can receive constructor parameters via array config:
209+
210+
```php
211+
'switch_tenant_tasks' => [
212+
\App\Tasks\YourTask::class => ['key' => 'value'],
213+
],
214+
```
215+
216+
## Middleware
217+
218+
- **`NeedsTenant`** — aborts the request (throws `NoCurrentTenant`) if no tenant is current. Apply to all tenant routes.
219+
- **`EnsureValidTenantSession`** — stores the first-seen tenant ID in the session and aborts with 401 if a different tenant ID is detected later. Prevents session cross-contamination.
220+
221+
## Custom Tenant Model
222+
223+
Set `tenant_model` in `config/multitenancy.php` and point it to your own class:
224+
225+
```php
226+
'tenant_model' => \App\Models\Tenant::class,
227+
```
228+
229+
To use an existing model (e.g. a Jetstream `Team`) as a tenant, implement `IsTenant` with the `ImplementsTenant` trait:
230+
231+
```php
232+
use Spatie\Multitenancy\Contracts\IsTenant;
233+
use Spatie\Multitenancy\Models\Concerns\ImplementsTenant;
234+
use Spatie\Multitenancy\Models\Concerns\UsesLandlordConnection;
235+
236+
class Team extends JetstreamTeam implements IsTenant
237+
{
238+
use UsesLandlordConnection;
239+
use ImplementsTenant;
240+
}
241+
```
242+
243+
Use a `creating` hook to provision a database when a tenant is created:
244+
245+
```php
246+
protected static function booted(): void
247+
{
248+
static::creating(fn (Tenant $tenant) => $tenant->createDatabase());
249+
}
250+
```
251+
252+
## Migrations & Seeding
253+
254+
**Landlord** migrations live in `database/migrations/landlord`. Run them once:
255+
256+
```bash
257+
php artisan migrate --path=database/migrations/landlord --database=landlord
258+
```
259+
260+
**Tenant** migrations run for every tenant via `tenants:artisan`:
261+
262+
```bash
263+
php artisan tenants:artisan "migrate --database=tenant"
264+
php artisan tenants:artisan "migrate --database=tenant --seed" --tenant=123
265+
```
266+
267+
In seeders, branch on `Tenant::checkCurrent()`:
268+
269+
```php
270+
public function run(): void
271+
{
272+
Tenant::checkCurrent()
273+
? $this->runTenantSpecificSeeders()
274+
: $this->runLandlordSpecificSeeders();
275+
}
276+
```
277+
278+
Programmatic migrations use `MigrateTenantAction`:
279+
280+
```php
281+
use Spatie\Multitenancy\Actions\MigrateTenantAction;
282+
283+
app(MigrateTenantAction::class)->fresh()->seed()->execute($tenant);
284+
```
285+
286+
## Artisan Commands
287+
288+
`tenants:artisan` loops over all tenants (or the specified ones) and runs a command for each:
289+
290+
```bash
291+
php artisan tenants:artisan "migrate --database=tenant"
292+
php artisan tenants:artisan "cache:clear" --tenant=1 --tenant=2
293+
```
294+
295+
To make your own commands tenant-aware, add the `TenantAware` concern and a `{--tenant=*}` option:
296+
297+
```php
298+
use Illuminate\Console\Command;
299+
use Spatie\Multitenancy\Commands\Concerns\TenantAware;
300+
301+
class SendReports extends Command
302+
{
303+
use TenantAware;
304+
305+
protected $signature = 'reports:send {--tenant=*}';
306+
307+
public function handle(): void
308+
{
309+
$this->line('Sending for tenant: ' . Tenant::current()->name);
310+
}
311+
}
312+
```
313+
314+
Omitting `--tenant` runs the command for every tenant. The command instance is reused across tenants — reset any state at the top of `handle()`.
315+
316+
## Tenant-Aware Queues
317+
318+
Enable globally in `config/multitenancy.php`:
319+
320+
```php
321+
'queues_are_tenant_aware_by_default' => true,
322+
```
323+
324+
Or mark individual jobs with the `TenantAware` interface:
325+
326+
```php
327+
use Illuminate\Contracts\Queue\ShouldQueue;
328+
use Spatie\Multitenancy\Jobs\TenantAware;
329+
330+
class ProcessReport implements ShouldQueue, TenantAware
331+
{
332+
public function handle(): void { /* ... */ }
333+
}
334+
```
335+
336+
Opt out per job with `NotTenantAware`:
337+
338+
```php
339+
use Spatie\Multitenancy\Jobs\NotTenantAware;
340+
341+
class SyncGlobalData implements ShouldQueue, NotTenantAware
342+
{
343+
public function handle(): void { /* ... */ }
344+
}
345+
```
346+
347+
Or list classes in config:
348+
349+
```php
350+
'tenant_aware_jobs' => [\App\Jobs\ProcessReport::class],
351+
'not_tenant_aware_jobs' => [\App\Jobs\SyncGlobalData::class],
352+
```
353+
354+
For closures dispatched to the queue, pass the tenant explicitly:
355+
356+
```php
357+
$tenant = Tenant::current();
358+
359+
dispatch(function () use ($tenant) {
360+
$tenant->execute(function () {
361+
// tenant context is active here
362+
});
363+
});
364+
```
365+
366+
If a tenant-aware job fires but the tenant cannot be resolved, `CurrentTenantCouldNotBeDeterminedInTenantAwareJob` is thrown and the job is deleted from the queue.
367+
368+
## Events
369+
370+
All events live in the `Spatie\Multitenancy\Events` namespace and carry `public IsTenant $tenant` except where noted:
371+
372+
| Event | When |
373+
|---|---|
374+
| `MakingTenantCurrentEvent` | Before switch tasks run |
375+
| `MadeTenantCurrentEvent` | After switch tasks + container binding |
376+
| `ForgettingCurrentTenantEvent` | Before forget tasks run |
377+
| `ForgotCurrentTenantEvent` | After forget tasks + container cleared |
378+
| `TenantNotFoundForRequestEvent` | When the finder returns `null` (carries `Request $request`) |
379+
380+
## Performance
381+
382+
- Switch tasks run synchronously on every `makeCurrent()` / `forgetCurrent()` call — keep them fast.
383+
- `shared_routes_cache` avoids generating one routes file per tenant when routes are identical across tenants.
384+
- Octane is supported out of the box: the service provider hooks into `RequestReceived` / `RequestTerminated` events automatically when `LARAVEL_OCTANE` is set.
385+
- The current tenant is stored in Laravel `Context` (`tenantId`), which queue workers read to restore tenant state before processing a job.

0 commit comments

Comments
 (0)