3.x redesign

This commit is contained in:
Samuel Štancl 2020-06-08 21:20:15 +02:00
parent 857122540f
commit f8f354c323
229 changed files with 201175 additions and 22440 deletions

View file

@ -0,0 +1,19 @@
---
title: Automatic tenancy mode
extends: _layouts.documentation
section: content
---
# Automatic mode
By default, the package bootstraps tenancy automatically in the background. This means that when a tenant is identified (usually using middleware), the default database/cache/filesystem/etc is switched to that tenant's context. You can read more about this on the [Tenancy bootstrappers]({{ $page->link('tenancy-bootstrappers') }}) page.
The flow goes like this:
`TenancyInitialized` fired → `BootstrapTenancy` listens → executes tenancy bootstrappers
It's recommended to use this mode, because:
- Separation of concerns. Tenancy happens one layer below your application. If you need to change the details how tenancy is bootstrapped, you can do that without having to change a ton of your app code.
- You don't have to think about the internals of how tenancy works when writing your application code. When you're writing the tenant part of the application, you're simply writing an e.g. e-commerce application, not a multi-tenant e-commerce application. No need to think about database connections when writing validation rules.
- Great integration with other packages. Switching the default database connection (and other things) is the only way to integrate many packages into the tenant part of the application. For example, you can use Laravel Nova to manage resources inside the tenant application.

View file

@ -0,0 +1,34 @@
---
title: Cached tenant lookup
extends: _layouts.documentation
section: content
---
# Cached lookup
If you're using multiple databases, you may want to avoid making a query to the central database on **each tenant request** — for tenant identification. Even though the queries are very simple, the app has to establish a connection with the central database which is expensive.
To avoid this, you may enable caching on the tenant resolvers (all in the `Stancl\Tenancy\Resolvers` namespace):
- `DomainTenantResolver` (also used for subdomain identification)
- `PathTenantResolver`
- `RequestDataTenantResolver`
On each of these classes, you may set the following static properties:
```php
// TenancyServiceProvider::boot()
use Stancl\Tenancy\Resolvers;
// enable cache
DomainTenantResolver::$shouldCache = true;
// seconds, 3600 is the default value
DomainTenantResolver::$cacheTTL = 3600;
// specify some cache store
// null resolves to the default cache store
DomainTenantResolver::$cacheStore = 'redis';
```

View file

@ -0,0 +1,102 @@
---
title: Configuration
extends: _layouts.documentation
section: content
---
# Configuration
The package is highly configurable. This page covers what you can configure in the `config/tenancy.php` file, but note that many more things are configurable. Some things can be changed by extending classes (e.g. the `Tenant` model), and **many** things can be changed using static properties. These things will *usually* be mentioned on the respective pages of the documentation, but not every time. For this reason, don't be afraid to dive into the package's source code — whenever the class you're using has a `public static` property, **it's intended to be configured**.
### Tenant model
`tenancy.tenant_model`
This config specifies what `Tenant` model should be used by the package. There's a high chance you're using a custom model, as instructed to by the [Tenants]({{ $page->link('tenants') }}) page, so be sure to change it in the config.
### Unique ID generator
`tenancy.id_generator`
The `Stancl\Tenancy\Database\Concerns\GeneratesIds` trait, which is applied on the default `Tenant` model, will generate a unique ID (uuid by default) if no tenant id is supplied.
If you wish to use autoincrement ids instead of uuids:
1. set this config key to null, or create a custom tenant model that doesn't use this trait
2. update the `tenants` table migration to use an incrementing column type instead of `string`
### Domain model
`tenancy.domain_model`
Similar to the Tenant model config. If you're using a custom model for domains, change it in this config. If you're not using domains (e.g. if you're using path or request data identification) at all, ignore this config key altogether.
### Central domains
`tenancy.central_domains`
The list of domains that host your [central app]({{ $page->link('the-two-applications') }}). This is used by (among other things):
- the `PreventAccessFromCentralDomains` middleware, to prevent access from central domains to tenant routes,
- the `InitializeTenancyBySubdomain` middleware, to check whether the current hostname is a subdomain on one of your central domains.
### Bootstrappers
`tenancy.bootstrappers`
This config array lets you enable, disable or add your own [tenancy bootstrappers]({{ $page->link('tenancy-bootstrappers') }}).
### Database
`tenancy.database.*`
This section is relevant to the multi-database tenancy, specifically, to the `DatabaseTenancyBootstrapper` and logic that manages tenant databases.
See this section in the config, it's documented with comments.
### Cache
`tenancy.cache.*`
This section is relevant to cache separation, specifically, to the `CacheTenancyBootstrapper`.
Note: To use the cache separation, you need to use a cache store that supports tagging, which is usually Redis.
See this section in the config, it's documented with comments.
### Filesystem
`tenancy.filesystem.*`
This section is relevant to cache separation, specifically, to the `FilesystemTenancyBootstrapper`.
See this section in the config, it's documented with comments.
### Redis
`tenancy.redis.*`
This section is relevant to Redis data separation, specifically, to the `RedisTenancyBootstrapper`.
Note: To use the this bootstrapper, you need phpredis.
See this section in the config, it's documented with comments.
### Features
`tenancy.features`
This config array lets you enable, disable or add your own [feature classes]({{ $page->link('feature-classes') }}).
### Migration parameters
`tenancy.migration_parameters`
This config array lets you set parameters used by default when running the `tenants:migrate` command (or when this command is executed using the `MigrateDatabase` job). Of course, all of these parameters can be overridden by passing them directly in the command call, be it in CLI or using `Artisan::call()`.
### Seeder parameters
`tenancy.seeder_parameters`
The same as migration parameters, but for `tenants:seed` and the `SeedDatabase` job.

View file

@ -0,0 +1,70 @@
---
title: Console commands
extends: _layouts.documentation
section: content
---
# Console commands
The package comes with some useful artisan commands.
## **Migrate**
The most important command. To use tenants, you have to be able to migrate their databases.
You can use the `tenants:migrate` command to migrate tenant's databases. You can also specify which tenants' databases should be migrated using the `--tenants` option.
```
php artisan tenants:migrate --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
You may use multiple `--tenants=<...>` options.
> Note: By default, the migrations should be in database/migrations/tenant. If you wish to use a different path, you may use the `--path` argument.
## **Rollback & seed**
- Rollback: `tenants:rollback`
- Seed: `tenants:seed`
Similarly to `migrate`, these commands accept a `--tenants` option.
## **Migrate fresh**
This package also offers a simplified, tenant-aware version of `migrate:fresh`. It runs `db:wipe` and `tenants:migrate` on the tenant's database.
You may use it like this:
```
php artisan tenants:migrate-fresh --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
## **Run**
You can use the `tenants:run` command to run your own commands for tenants.
If your command's signature were `email:send {--queue} {--subject=} {body}`, you would run this command like this:
```
php artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..."
```
## **Tenant list**
```
php artisan tenants:list
Listing all tenants.
[Tenant] id: dbe0b330-1a6e-11e9-b4c3-354da4b4f339 @ localhost
[Tenant] id: 49670df0-1a87-11e9-b7ba-cf5353777957 @ dev.localhost
```
## **Selectively clearing tenant cache**
You can delete specific tenants' cache by using the `--tags` option on `cache:clear`:
```
php artisan cache:clear --tags=tenantdbe0b330-1a6e-11e9-b4c3-354da4b4f339
```
The tag is derived from `config('tenancy.cache.tag_base') . $id`.

View file

@ -0,0 +1,65 @@
---
title: Customizing tenant databases
extends: _layouts.documentation
section: content
---
# Customizing databases
You may customize how a tenant's DB connection is constructed by storing specific internal keys on the tenant.
If you changed the internal prefix on the tenant model, then use that instead of `tenancy_`.
## Specifying database names
You may specify the tenant's database name by setting the `tenancy_db_name` key when creating the tenant.
```php
Tenant::create([
'tenancy_db_name' => 'acme',
]);
```
When you don't specify the tenant's database name, it's constructed using:
`tenancy.database.prefix` config + tenant id + `tenancy.database.suffix` config
Therefore, another way to specify database names is to set the tenant id during creation, rather than letting it be randomly generated:
```php
Tenant::create([
'id' => 'acme',
]);
```
## Specifying database credentials
Database user & password are only created when you use the permission controlled MySQL database manager. See the database config for more info.
You may specify the username and password for the user that will be created along with the tenant database.
```php
Tenant::create([
'tenancy_db_username' => 'foo',
'tenancy_db_password' => 'bar',
]);
```
The user will be given the grants specified in the `PermissionControlledMySQLDatabaseManager::$grants` array. Feel free to customize this by setting it to a different value like any other public static property.
Note that you don't want to grant the users the ability to grant themselves more grants.
## Specifying template connections
To specify the connection that should be used to construct this tenant's database connection (the array like you'd find in `config/database.php`, set the `tenancy_db_connection` key. Otherwise, the connection whose name is in the `tenancy.database.template_connection` config will be used. If that key is null, the central connection will be used.
## Specifying other connection details
You may also set specific connection details without necessarily creating a new connection. The final "connection array" will be constructed by merging the following:
- the template connection
- the database name
- optionally, the username and password
- all `tenancy_db_*` keys
This means that you can store a value for e.g. `tenancy_db_charset` if you want to specify the charset for the tenant's database connection for whatever reason.

View file

@ -0,0 +1,19 @@
---
title: Domains
extends: _layouts.documentation
section: content
---
# Domains
Note: Domains are optional. If you're using path or request data identification, you don't need to worry about them.
To add a domain to a tenant, use the `domains` relationship:
```php
$tenant->domains()->create([
'domain' => 'acme',
]);
```
If you use the subdomain identification middleware, the example above will work for `acme.{any of your central domains}`. If you use the domain identification middleware, use the full hostname like `acme.com`. If you use the combined domain/subdomain identification middleware, `acme` will work as a subdomain and `acme.com` will work as a domain.

View file

@ -0,0 +1,76 @@
---
title: Early identification
extends: _layouts.documentation
section: content
---
# Early identification
A slight "gotcha" with using the automatic approach to transition the application's context based on a route middleware is that **route-level middleware is executed after controller constructors.**
The implication of this is if you're using dependency injection to inject some services in the controller constructors, **they will read from the central context**, because route-level middleware hasn't initialized tenancy yet.
There are two ways to solve it, the former of which is preferable.
## Not using constructor DI
You can inject dependencies in route **actions**, meaning: If you have a route that binds a `Post` model, you can still inject dependencies like this:
```php
// Note that this is sort-of pseudocode. Notice the route action DI
// and just skim the rest :)
Route::get('/post/{post}/edit', 'PostController@edit');
class PostController
{
public function edit(Request $request, Post $post, Cloudinary $cloudinary)
{
if ($request->has('image')) {
$post->image_url = $cloudinary->store($request->file('image'));
}
}
}
```
If you don't like that because you access some dependency from many actions, consider creating a memoized method:
```php
class PostController
{
protected Cloudinary $cloudinary;
protected cloudinary(): Cloudinary
{
// If you don't like the service location here, injecting Application
// in the controller constructor is one thing that's 100% safe.
return $this->cloudinary ??= app(Cloudinary::class);
}
public function edit(Request $request, Post $post)
{
if ($request->has('image')) {
$post->image_url = $this->cloudinary()->store(
$request->file('image')
);
}
}
}
```
## Using a more complex middleware setup
> Note: There's a new MW in v3 for preventing access from central domains. v2 was doing this a bit differently.
The manual for implementing this will come soon, for now you can look at how 2.x does this.
In short: The `InitializeTenancy` mw is part of the global middleware stack, which doesn't have access to route information, but is executed prior to controller constructors. The `PreventAccessFromTenantDomains` mw checks that we're vising a tenant route on a tenant domain, or a central route on a central domain — and if not, it aborts the request, either by 404 or by redirecting us to a home url on the tenant domain.
Here's the logic visually:
![Early identification middleware](/assets/images/stancl_tenancy_middleware.png)
And here are the relevant files in 2.x codebase:
- [https://github.com/stancl/tenancy/blob/2.x/src/Middleware/InitializeTenancy.php](https://github.com/stancl/tenancy/blob/2.x/src/Middleware/InitializeTenancy.php)
- [https://github.com/stancl/tenancy/blob/2.x/src/Middleware/PreventAccessFromTenantDomains.php](https://github.com/stancl/tenancy/blob/2.x/src/Middleware/PreventAccessFromTenantDomains.php)

View file

@ -0,0 +1,187 @@
---
title: Event system
extends: _layouts.documentation
section: content
---
# Event system
This package is heavily based around events, which makes it incredibly flexible.
By default, the events are configured in such a way that the package works like this:
- A request comes in for a tenant route and hits an identification middleware
- The identification middleware finds the correct tenant and runs
```php
$this->tenancy->initialize($tenant);
```
- The `Stancl\Tenancy\Tenancy` class sets the `$tenant` as the current tenant and fires a `TenancyInitialized` event
- The `BootstrapTenancy` class catches the event and executes classes known as [tenancy bootstrappers]({{ $page->link('tenancy-bootstrappers') }}).
- The tenancy bootstrappers make changes to the application to make it "scoped" to the current tenant. This by default includes:
- Switching the database connection
- Replacing `CacheManager` with a scoped cache manager
- Suffixing filesystem paths
- Making queues store the tenant id & initialize tenancy when being processed
Again, all of the above is configurable. You might even disable all tenancy bootstrappers, and just use tenant identification and scope your app manually around the tenant stored in `Stancl\Tenancy\Tenancy`. The choice is yours.
# TenancyServiceProvider
This package comes with a very convenient service provider that's added to your application when you install the package. This service provider is used for mapping listeners to events specific to the package and is the place where you should put any tenancy-specific service container calls — to not pollute your AppServiceProvider.
Note that you can register listeners to this package's events **anywhere you want**. The event/listener mapping in the service provider exists only to make your life easier. If you want to register the listeners manually, like in the example below, you can.
```php
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
```
# Bootstrapping tenancy
By default, the `BootstrapTenancy` class is listening to the `TenancyInitialized` event (exactly as you can see in the example above). That listener will execute the configured tenancy bootstrappers to transition the application into the tenant's context. You can read more about this on the [tenancy bootstrappers]({{ $page->link('tenancy-bootstrappers') }}) page.
Conversely, when the `TenancyEnded` event fires, the `RevertToCentralContext` event transitions the app back into the central context.
# Job pipelines
You may job pipelines even in projects that don't use this package — we think they're a cool concept so they're extracted into a separate package: [github.com/stancl/jobpipeline](https://github.com/stancl/jobpipeline)
The `JobPipeline` is a simple, yet **extremely powerful** class that lets you **convert any (series of) jobs into event listeners.**
You may use a job pipeline like any other listener, so you can register it in the `TenancyServiceProvider`, `EventServiceProvider` using the `$listen` array, or in any other place using `Event::listen()` — up to you.
## Creating job pipelines
To create a job pipeline, start by specifying the jobs you want to use:
```php
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};
JobPipeline::make([
CreateDatabase::class,
MigrateDatabase::class,
SeedDatabase::class,
])
```
Then, specify what variable you want to pass to the jobs. This will usually come from the event.
```php
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};
use Stancl\Tenancy\Events\TenantCreated;
JobPipeline::make([
CreateDatabase::class,
MigrateDatabase::class,
SeedDatabase::class,
])->send(function (TenantCreated $event) {
return $event->tenant;
})
```
Next, decide if you want to queue the pipeline. By default, pipelines are synchronous (= not queued) by default.
If you **do** want pipelines to be queued by default, you can do that by setting a static property:
`\Stancl\JobPipeline\JobPipeline::$shouldBeQueuedByDefault = true;`
```php
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};
JobPipeline::make([
CreateDatabase::class,
MigrateDatabase::class,
SeedDatabase::class,
])->send(function (TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(true),
```
Finally, convert the pipeline to a listener and bind it to an event:
```php
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};
use Illuminate\Support\Facades\Event;
Event::listen(TenantCreated::class, JobPipeline::make([
CreateDatabase::class,
MigrateDatabase::class,
SeedDatabase::class,
])->send(function (TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(true)->toListener());
```
Note that you can use job pipelines even for converting single jobs to event listeners. That's useful if you have some logic in job classes and don't want to create listener classes just to be able to run these jobs as a result of an event being fired.
# Available events
Note: Some database events (`DatabaseMigrated`, `DatabaseSeeded`, `DatabaseRolledback` and possibly others) are **fired in the tenant context.** Depending on how your application bootstraps tenancy, you might need to be specific about interacting with the central database in these events' listeners — that is, if you need to.
Note: All events are located in the `Stancl\Tenancy\Events` namespace.
### **Tenancy**
- `InitializingTenancy`
- `TenancyInitialized`
- `EndingTenancy`
- `TenancyEnded`
- `BootstrappingTenancy`
- `TenancyBootstrapped`
- `RevertingToCentralContext`
- `RevertedToCentralContext`
Note the difference between *initializing tenancy and bootstrapping* tenancy. Tenancy is initialized when a tenant is loaded into the `Tenancy` object. Whereas boostrapping happens **as a result of initialization** — if you're using automatic tenancy, the `BootstrapTenancy` class is listening to the `TenancyInitialized` event and after it's done executing bootstrappers, it fires an event saying that tenancy was bootstrapped. You want to use the bootstrapped event if you want to execute something **after the app has been transitioned to the tenant context.**
### Tenant
The following events are dispatched as a result of Eloquent events being fired in the default `Tenant` implementation (the most often used events are bold):
- `CreatingTenant`
- `**TenantCreated**`
- `SavingTenant`
- `TenantSaved`
- `UpdatingTenant`
- `TenantUpdated`
- `DeletingTenant`
- `**TenantDeleted**`
### Domain
These events are optional. They're only relevant to you if you're using domains for your tenants.
- `CreatingDomain`
- `**DomainCreated**`
- `SavingDomain`
- `DomainSaved`
- `UpdatingDomain`
- `DomainUpdated`
- `DeletingDomain`
- `**DomainDeleted**`
### Database
These events are also optional. They're relevant to you if you're using multi-database tenancy:
- `CreatingDatabase`
- `**DatabaseCreated**`
- `MigratingDatabase`
- `DatabaseMigrated`
- `SeedingDatabase`
- `DatabaseSeeded`
- `RollingBackDatabase`
- `DatabaseRolledBack`
- `DeletingDatabase`
- `**DatabaseDeleted**`
### Resource syncing
- `**SyncedResourceSaved**`
- `SyncedResourceChangedInForeignDatabase`

View file

@ -0,0 +1,18 @@
---
title: Feature classes
extends: _layouts.documentation
section: content
---
# Feature classes
"Features" are classes that provide additional functionality that's not needed for the core tenancy logic. Out of the box, the package comes with these Features:
- [`UserImpersonation`]({{ $page->link('features/user-impersonation') }}) for generating impersonation tokens for users of a tenant's DB from other contexts
- [`TelescopeTags`]({{ $page->link('features/telescope-tags') }}) for adding tags with the current tenant id to Telescope entries
- [`TenantConfig`]({{ $page->link('features/tenant-config') }}) for mapping keys from the tenant storage into the application config
- [`CrossDomainRedirect`]({{ $page->link('features/cross-domain-redirect') }}) for adding a `domain()` macro on `RedirectResponse` letting you change the intended hostname of the generated route
- [`UniversalRoutes`]({{ $page->link('features/universal-routes') }}) for route actions that work in both the central & tenant context
All of the package's Features are in the `Stancl\Tenancy\Features` namespace.
You may register features by adding their class names to the `tenancy.features` config.

View file

@ -0,0 +1,15 @@
---
title: Cross-domain redirect
extends: _layouts.documentation
section: content
---
# Cross-domain redirect
To enable this feature, uncomment the `Stancl\Tenancy\Features\CrossDomainRedirect::class` line in your `tenancy.features` config.
Sometimes you may want to redirect the user to a specific route on a different domain (than the current one). Let's say you want to redirect a tenant to the `home` path on their domain after they sign up:
```php
return redirect()->route('home')->domain($domain);
```

View file

@ -0,0 +1,9 @@
---
title: Telescope tags
extends: _layouts.documentation
section: content
---
# Telescope tags
TODO

View file

@ -0,0 +1,49 @@
---
title: Tenant-specific config
extends: _layouts.documentation
section: content
---
# Tenant config
It's likely you will need to use tenant-specific config in your application. That config could be API keys, things like "products per page" and many other things.
You could just use the the tenant model to get these values, but you may still want to use Laravel's `config()` because of:
- separation of concerns — if you just write tenancy implementation-agnostic `config('shop.products_per_page')`, you will have a much better time changing tenancy implementations
- default values — you may want to use the tenant storage only to override values in your config file
## **Enabling the feature**
Uncomment the following line in your `tenancy.features` config:
```php
// Stancl\Tenancy\Features\TenantConfig::class,
```
## **Configuring the mappings**
This feature maps keys in the tenant storage to config keys based on the `$storageToConfigMap` public property.
For example, if your `$storageToConfigMap` looked like this:
```php
\Stancl\Tenancy\Features\TenantConfig::$storageToConfigMap = [
'paypal_api_key' => 'services.paypal.api_key',
],
```
the value of `paypal_api_key` in tenant model would be copied to the `services.paypal.api_key` config when tenancy is initialized.
## Mapping the value to multiple config keys
Sometimes you may want to copy the value to multiple config keys. To do that, specify the config keys as an array:
```php
\Stancl\Tenancy\Features\TenantConfig::$storageToConfigMap = [
'locale' => [
'app.locale',
'locales.default',
],
],
```

View file

@ -0,0 +1,37 @@
---
title: Universal routes
extends: _layouts.documentation
section: content
---
# Universal Routes
Sometimes, you may want to use the exact same **route action** both in the central application and the tenant application. Note the emphasis on route **action** — you may use the same **path** with different actions in central & tenant routes, whereas this section covers using the same **route and action**.
Generally, try to avoid these use cases as much as possible and prefer duplicating the code. Using the same controller and model for users in central & tenant apps will break down once you need slightly different behavior — e.g. different views returned by controllers, different behavior on models, etc.
First, enable the `UniversalRoutes` feature by uncommenting the following line in your `tenancy.features` config:
```php
Stancl\Tenancy\Features\UniversalRoutes::class,
```
Next, go to your `app/Http/Kernel.php` file and add the following middleware group:
```php
'universal' => [],
```
We will use this middleware group as a "flag" on the route, to mark it as a universal route. We don't need any actual middleware inside the group.
Then, create a route like this:
```php
Route::get('/foo', function () {
// ...
})->middleware(['universal', InitializeTenancyByDomain::class]);
```
And the route will work in both central and tenant applications. Should a tenant be found, tenancy will be initialized. Otherwise, the central context will be used.
If you're using a different middleware, look at the `UniversalRoutes` feature source code and change the public static property accordingly.

View file

@ -0,0 +1,117 @@
---
title: User impersonation
extends: _layouts.documentation
section: content
---
# User impersonation
This package comes with a feature that lets you impersonate users inside tenant databases. This feature works with **any identification method** and **any auth guard** — even if you use multiple.
## How it works
You generate an impersonation token an store it in the central database, in the `tenant_user_impersonation_tokens` table.
Each record in the table holds the following data:
- The token value (128 character string)
- The tenant's id
- The user's id
- The name of the auth guard
- The URL to redirect to after the impersonation takes place
You visit an impersonation route that you create — though little work is needed on your side, your route will mostly just call a method provided by the feature. This route is a **tenant route**, meaning it's on the tenant domain if you use domain identification, or prefixed with the tenant id if you use path identification.
This route checks tries to find a record in that table based on the token, and if it's valid it authenticates you with the stored user id against the auth guard and redirects you to the stored URL.
If the impersonation succeeds, the token is deleted from the database.
All tokens expire after 60 seconds by default, and this TTL can be customized — see the section at the very bottom.
## Enabling the feature
To enable this feature, go to your `config/tenancy.php` file and make sure the following class is in your `features` part of the config:
```jsx
Stancl\Tenancy\Features\UserImpersonation::class,
```
Next, publish the migration for creating the table with impersonation tokens:
```jsx
php artisan vendor:publish --tag=impersonation-migrations
```
And finally, run the migration:
```jsx
php artisan migrate
```
## Usage
First, you need to create a tenant route that looks like this:
```jsx
use Stancl\Tenancy\Features\UserImpersonation;
// We're in your tenant routes!
Route::get('/impersonate/{token}', function ($token) {
return UserImpersonation::makeResponse($token);
});
// Of course use a controller in a production app and not a Closure route.
// Closure routes cannot be cached.
```
Note that the route path or name are completely up to you. The only logic that the package does is generating tokens, verifying tokens, and doing the impersonated user log in.
Then, to use impersonation in your app, generate a token like this:
```jsx
// Let's say we want to be redirected to the dashboard
// after we're logged in as the impersonated user.
$redirectUrl = '/dashboard';
$token = tenancy()->impersonate($tenant, $user->id, $redirectUrl);
```
And redirect the user (or, presumably an "admin") to the route you created.
### Domain identification
```jsx
// Note: This is not part of the package, it's up to you to implement
// a concept of "primary domains" if you need them. Or maybe you use
// one domain per tenant. The package lets you do anything you want.
$domain = $tenant->primary_domain;
return redirect("https://$domain/impersonate/$token");
```
### Path identification
```jsx
// Make sure you use the correct prefix for your routes.
return redirect("{$tenant->id}/impersonate/$token");
```
And that's it. The user will be redirected to your impersonation route, logged in as the impersonated user, and finally redirected to your redirect URL.
### Custom auth guards
If you're using multiple auth guards, you may want to specify what auth guard the impersonation logic should use.
To do this, simply pass the auth guard name as the fourth argument to the `impersonate()` method. So to expand on our example above:
```jsx
tenancy()->impersonate($tenant, $user->id, $redirectUrl, 'jwt');
```
## Customization
You may customize the TTL of impersonation tokens by setting the following static property to the amount of seconds you want to use:
```jsx
Stancl\Tenancy\Features\UserImpersonation::$ttl = 120; // 2 minutes
```

View file

@ -0,0 +1,24 @@
---
title: How it works
extends: _layouts.documentation
section: content
---
# How it works
This package is very flexible and lets you use tenancy however you want. But it comes with sensible defaults that work like this out of the box:
- a request comes in
- the domain is used to identify the tenant
- the database, cache, etc are switched to that tenant's context
This happens using identification middleware and events.
[Tenant identification]({{ $page->link('tenant-identification') }})
[Event system]({{ $page->link('event-system') }})
Note that even though the default assumes you're using domains and the database-per-tenant model, you're free to customize this any way you want. **And it's easy to customize!** Just read on to get an understanding of how everything works.
TODO: Expand. Why this approach, etc. Maybe on the other page?

View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url={{ $page->baseUrl }}/docs/v3/introduction">
<title>stancl/tenancy</title>
</head>
</html>

View file

@ -0,0 +1,42 @@
---
title: Installation
extends: _layouts.documentation
section: content
---
# Installation
Require the package using composer:
```php
composer require stancl/tenancy:3.x-dev
```
Then run the following command:
```php
php artisan tenancy:install
```
It will create:
- migrations
- a config file (`config/tenancy.php`),
- a routes file (`routes/tenant.php`),
- and a service provider file `app/Providers/TenancyServiceProvider.php`
Then add the service provider to your `config/app.php` file:
```php
/*
* 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
```
And finally, name your central connection (in `config/database.php`) `central` — or however else you want, but make sure it's the same name as the `tenancy.central_connection` config.

View file

@ -0,0 +1,22 @@
---
title: Integration with other packages
extends: _layouts.documentation
section: content
---
# Integrating with other packages
If you're using the [automatic mode]({{ $page->link('features/automatic-mode') }}) & [multi-database tenancy]({{ $page->link('multi-database-tenancy') }}), you'll be able to integrate with other packages easily.
[Integration with Spatie packages]({{ $page->link('features/spatie') }})
[Laravel Horizon]({{ $page->link('features/horizon') }})
[Laravel Passport]({{ $page->link('features/passport') }})
[Laravel Nova]({{ $page->link('features/nova') }})
[Laravel Telescope]({{ $page->link('features/telescope') }})
[Livewire]({{ $page->link('features/livewire') }})

View file

@ -0,0 +1,20 @@
---
title: Laravel Horizon integration
extends: _layouts.documentation
section: content
---
# Laravel Horizon
Make sure your [queues]({{ $page->link('queues') }}) are configured correctly before using this.
You may add the current tenant's id to your job tags:
```php
public function tags()
{
return [
'tenant' => tenant('id'),
];
}
```

View file

@ -0,0 +1,27 @@
---
title: Livewire integration
extends: _layouts.documentation
section: content
---
# Livewire
Open the `config/livewire.php` file and change this:
```php
'middleware_group' => ['web'],
```
to this:
```php
'middleware_group' => [
'web',
'universal',
InitializeTenancyByDomain::class, // or whatever tenancy middleware you use
],
```
Now you can use Livewire both in the central app and the tenant app.
Also make sure to enable *universal routes*:

View file

@ -0,0 +1,53 @@
---
title: Laravel Nova integration
extends: _layouts.documentation
section: content
---
# Laravel Nova
## In the central app
If you wish to use Laravel Nova in the central application (to manage tenants), you need to make a small change to the Nova migrations, they expect your model primary keys to always be unsigned big integers, but your tenants might be using `string` ids.
You can find the full Nova setup for managing tenants in the [SaaS boilerplate](/saas-boilerplate):
## In the tenant app
To use Nova inside of the tenant part of your application, do the following:
- Publish the Nova migrations and move them to the `database/migrations/tenant` directory.
```
php artisan vendor:publish --tag=nova-migrations
```
- Prevent Nova from adding its migrations to your central migrations by adding `Nova::ignoreMigrations()` to `NovaServiceProvider::boot()` (Don't do this if you want to use Nova **[both in the central & tenant parts]({{ $page->link('features/universal-routes') }})** of the app.)
- Add the tenancy middleware to your `nova.middleware` config. Example:
```php
'middleware' => [
// You can make this simpler by creating a tenancy route group
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
'web',
Authenticate::class,
DispatchServingNovaEvent::class,
BootTools::class,
Authorize::class,
],
```
- In your `NovaServiceProvider`'s `routes()` method, replace the following lines:
```php
->withAuthenticationRoutes()
->withPasswordResetRoutes()
```
with these lines:
```php
->withAuthenticationRoutes(['web', 'tenancy'])
->withPasswordResetRoutes(['web', 'tenancy'])
```

View file

@ -0,0 +1,54 @@
---
title: Laravel Passport integration
extends: _layouts.documentation
section: content
---
# Laravel Passport
> If you just want to write an SPA, but don't need an API for some other use (e.g. mobile app), you can avoid a lot of the complexity of writing SPAs by using [Inertia.js](https://inertiajs.com/).
To use Passport inside the tenant part of your application, you may do the following.
- Add this to the `register` method in your `AppServiceProvider`:
```php
Passport::ignoreMigrations();
Passport::routes(null, ['middleware' => [
// You can make this simpler by creating a tenancy route group
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
]]);
```
- `php artisan vendor:publish --tag=passport-migrations` & move to `database/migrations/tenant/` directory
## **Shared keys**
If you want to use the same keypair for all tenants, do the following.
- Don't use `passport:install`, use just `passport:keys`. The install command creates keys & two clients. Instead of creating clients centrally, create `Client`s manually in your [tenant database seeder]({{ $page->link('configuration#seeder-params') }}).
## **Tenant-specific keys**
If you want to use a unique keypair for each tenant, do the following. (Note: The security benefit of doing this isn't probably that big, since you're likely already using the same `APP_KEY` for all tenants.)
There are multiple ways you can store & load tenant keys, but the most straightforward way is to store the keys in the on the tenant model and load them into the `passport` configuration using the **[Tenant Config]({{ $page->link('features/tenant-config') }})** feature:
- Uncomment the `TenantConfig` line in your `tenancy.features` config
- Configure the mapping as follows:
```php
[
'passport_public_key' => 'passport.public_key',
'passport_private_key' => 'passport.private_key',
],
```
And again, you need to create clients in your tenant database seeding process.
## Using Passport in both the central & tenant app
![Passport for both central & tenant app](/assets/images/passport_universal.png)
And make sure you enable the *Universal Routes* feature.

View file

@ -0,0 +1,37 @@
---
title: Integration with Spatie packages
extends: _layouts.documentation
section: content
---
# Integration with Spatie packages
## **laravel-activitylog**
### For the tenant app:
- Set the `database_connection` key in `config/activitylog.php` to `null`. This makes activitylog use the default connection.
- Publish the migrations and move them to `database/migrations/tenant`. (And, of course, don't forget to run `artisan tenants:migrate`.)
### For the central app:
- Set the `database_connection` key in `config/activitylog.php` to the name of your central database connection.
## **laravel-permission**
Install the package like usual, but publish the migrations and move them to `migrations/tenant`:
```
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations"
mv database/migrations/*_create_permission_tables.php database/migrations/tenant
```
Then add this to your `AppServiceProvider::boot()` method:
```php
Event::listen(TenancyBootstrapped::class, function (Tenancy $tenancy) {
\Spatie\Permission\PermissionRegistrar::$cacheKey = 'spatie.permission.cache.tenant.' . $tenancy->tenant->id;
});
```
The reason for this is that spatie/laravel-permission caches permissions & roles to save DB queries, which means that we need to separate the permission cache by tenant.

View file

@ -0,0 +1,11 @@
---
title: Laravel Telescope integration
extends: _layouts.documentation
section: content
---
# Laravel Telescope
Enable the the *Telescope tags* feature to have all Telescope requests tagged with the current tenant's id.
Note that Telescope (& its migrations) will be part of the central app.

View file

@ -0,0 +1,36 @@
---
title: Introduction
extends: _layouts.documentation
section: content
---
# Introduction
## What is multi-tenancy?
Multi-tenancy is the ability to provide your service to multiple users (tenants) from a single hosted instance of the application. This is contrasted with deploying the application separately for each user.
You may find this talk insightful: [https://multitenantlaravel.com/](https://multitenantlaravel.com/). Simply going through the slides will give you 80% of the value of the talk in under five minutes.
Note that if you just want to, say, scope todo tasks to the current user, there's no need to use a multi-tenancy package. Just use calls like `auth()->user()->tasks()`. This is the simplest for of multi-tenancy.
This package is built around the idea that multi-tenancy usually means letting tenants have their own users which have their own resources, e.g. todo tasks. Not just users having tasks.
## Types of multi-tenancy
There are two **types** of multi-tenancy:
- single-database tenancy — tenants share one database and their data is separated using e.g. `where tenant_id = 1` clauses.
- multi-database tenancy — each tenant has his own database
This package lets you do both, though it focuses more on multi-database tenancy because that type requires more work on the side of the package and less work on your side. Whereas for single-database tenancy you're provided with a class that keeps track of the current tenant and model traits — and the rest is up to you.
## Modes of multi-tenancy
The tenancy "mode" is a unique property of this package. In previous versions, this package was intended primarily for [**automatic tenancy**]({{ $page->link('automatic-mode') }}), which means that after a tenant was identified, things like database connections, caches, filesystems, queues etc were switched to that tenant's context — his data completely isolated from the rest.
In the current version, we're also making [**manual tenancy**]({{ $page->link('manual-mode') }}) a first-class feature. We provide you with things like model traits if you wish to scope the data yourself.
## Tenant identification
For your application to be tenant-aware, a tenant has to be identified. This package ships with a large number of identification middlewares. You may identify tenants by domain, subdomain, domain OR subdomain at the same time, path or request data.

View file

@ -0,0 +1,17 @@
---
title: Manual tenancy initialization
extends: _layouts.documentation
section: content
---
# Manual initialization
Sometimes you may want to initialize tenancy manually — that is, not using web middleware, command traits, queue tenancy etc.
For that, use the `initialize()` method on `Stancl\Tenancy\Tenancy`. You can resolve the `Tenancy` instance out of the container using the `tenancy()` helper.
```php
$tenant = Tenant::find('some-id');
tenancy()->initialize($tenant);
```

View file

@ -0,0 +1,27 @@
---
title: Manual tenancy mode
extends: _layouts.documentation
section: content
---
# Manual mode
> See: [Automatic mode]({{ $page->link('automatic-mode') }})
If you wish to use the package only to keep track of the current tenant and make the application tenant-aware manually — without the use [Tenancy bootstrappers]({{ $page->link('tenancy-bootstrappers') }}), you can absolutely do that.
You may use the `Stancl\Tenancy\Database\Concerns\CentralConnection` and `Stancl\Tenancy\Database\Concerns\TenantConnection` model traits to make models explicitly use the given connections.
To create the tenant connection, set up the `CreateTenantConnection` listener:
```php
// app/Providers/TenancyServiceProvider.php
Events\TenancyInitialized::class => [
Listeners\CreateTenantConnection::class,
],
```
This approach is generally discouraged, because you lose all of the benefits of the [automatic mode]({{ $page->link('automatic-mode') }}), but **there won't be any issues with the package** if you decide to use the manual mode. You might not be able to integrate other packages as easily, but if for whatever reason it makes more sense for your project to use this approach, feel comfortable to do so.

View file

@ -0,0 +1,11 @@
---
title: Migrations
extends: _layouts.documentation
section: content
---
# Migrations
Move your tenant migrations to the `database/migrations/tenant` directory. You can execute them using `php artisan tenants:migrate`.
Note that all migrations share the same PHP namespace, so even if you use the same table name in the central and tenant databases, you have to use different migration (class) names.

View file

@ -0,0 +1,9 @@
---
title: Multi-database tenancy
extends: _layouts.documentation
section: content
---
# Multi-database tenancy
TODO

View file

@ -0,0 +1,114 @@
---
title: Compared to other packages
extends: _layouts.documentation
section: content
---
# Compared to other packages
## hyn/multi-tenancy
This package intends to provide you with the necessary tooling for adding multi-tenancy **manually** to your application. It will give you model traits, classes for creating tenant databases, and some additional tooling.
It would have been a good option for when you want to implement multi-tenancy manually, but:
- It isn't being actively developed — no features have been added in the past ~year
- It makes testing a nightmare.
Over the last few months, I've received this feedback:
> But, I still can't run any tests in Hyn, and had some queuing problems I'm still nervous about.
> At the moment, our app is using latest Laravel and latest hyn/tenancy. The only thing I don't like about it is that tests are extremely fragile to the point where I don't dare mess with anything for risk of breaking everything
> By the way, this package is awesome! It's so much better than the hyn alternative which is a bit of a mess in my opinion... It is a pity that I did not come across it in the first place.
I'm not sharing this to intentionally make hyn/multi-tenancy bad, but **be very careful if you decide to go with that package**.
- It will very likely be sunset — or, in other words, killed — soon, since tenancy/tenancy is being released.
## tenancy/tenancy
This package intends to provide you with a framework for building your own multi-tenancy implementation. The documentation is quite lacking, so I couldn't get a too detailed idea of what it does, but from my understanding, it gives you things like events which you can use to build your own multi-tenancy logic.
If you want the absolute highest flexibility and would otherwise build your own implementation, looking into this package might be useful.
However, if you're looking for a package that will help you make a multi-tenant project quickly, this is probably not the right choice.
## spatie/laravel-multitenancy
It's hard to find good things to say about this package. It's basically a simplified copy of v2 of stancl/tenancy.
stancl/tenancy was the first package to use the automatic approach. The Spatie package is a copy of that, though in Freek's words, he hasn't looked into other packages prior to developing his.
[https://twitter.com/freekmurze/status/1257765177174482944](https://twitter.com/freekmurze/status/1257765177174482944)
(That, in my opinion, is a red flag, since you'd want to do research before just blindly coding something and marketing it.)
The package is basically Mohamed Said's video series on multi-tenancy glued into a package.
They also don't seem to have an interest in having their package help the largest amount of people:
[https://twitter.com/freekmurze/status/1260187383191961601](https://twitter.com/freekmurze/status/1260187383191961601)
The only benefit I see in this package compared to v2 of stancl/tenancy is that it uses Eloquent out of the box, which makes things like Cashier integration easier. But, that's irrelevant since we're in v3 already and v3 uses Eloquent.
So, I suggest you consider this package only if you value simplicity for some reason, and aren't building anything with any amount of complexity and need for "business features".
## stancl/tenancy
In my — biased, of course, but likely true as well — opinion, this package is the absolute best choice for the vast majority of applications.
The only packages I'd realistically consider are mine (of course) and tenancy/tenancy if you need something **very** custom, though I don't see the reason for that in 99% of applications.
This package attempts to be about as flexible as tenancy/tenancy, but also provide you with the absolute largest amount of out-of-the-box features and other tooling. It continues its path as the first package to have been using the automatic approach with adding many more features — most of which are "enterprise" features, in v3.
To give you an incomplete-but-good-enough list of features, this package supports:
- Multi-database tenancy
- creating databases
- MySQL
- PostgreSQL
- PostgreSQL (schema mode)
- SQLite
- creating database users
- automatically switching the database
- CLI commands, with more features than e.g. spatie/laravel-multitenancy
- migrate
- migrate:fresh
- seed
- Single-database tenancy
- model traits with global scopes
- Rich event system
- **Very high testability**
- Automatic tenancy
- tenancy bootstrappers to switch:
- database connections
- redis connections
- cache tags
- filesystem roots
- queue context
- Manual tenancy
- model traits
- Out of the box tenant identification
- domain identification
- subdomain identification
- path identification
- request data identification
- middleware classes for the methods above
- CLI argument identification
- manual identification (e.g. in tinker)
- Integration with many packages
- spatie/laravel-medialibrary
- spatie/laravel-activitylog
- Livewire
- Laravel Nova
- for managing tenants
- for **using inside the tenant application**
- Laravel Horizon
- Laravel Telescope
- Laravel Passport
- **Syncing users (or any other database resources) between multiple tenant databases**
- Dependency injection of the current tenant
- Tenant **user impersonation**
- **Cached tenant lookup**, universal for all tenant resolvers

View file

@ -0,0 +1,50 @@
---
title: Queues
extends: _layouts.documentation
section: content
---
# Queues
If you're using the `QueueTenancyBootstrapper`, queued jobs dispatched from the tenant context will be automatically tenant-aware. The jobs will be stored centrally — if you're using the database queue driver, they will be stored in the `jobs` table in the central database. And tenancy will be initialized for the current tenant prior to the job being processed.
**However**, note that if you're using the `DatabaseTenancyBootstrapper` and the `database` queue driver, or the `RedisTenancyBootstrapper` and the `redis` queue driver, you will need to make sure the jobs don't get dispatched into the tenant context for these drivers.
Note: You cannot inject model **instances** with the `SerializesModels` trait, because it tries to hydrate the models before the `tenant` connection is created. Inject model ids instead and use `find()` in the handle method.
### Database queue driver
To force the database queue driver to use the central connection, open your `queue.connections.database` config and add the following line:
```jsx
'connection' => 'central',
```
(Replace `central` with the name of your central database connection.)
### Redis queue driver
Make sure the connection used by the queue is not in `tenancy.redis.prefixed_connections`.
## Central queues
Jobs dispatched from the central context will remain central. However, it's recommended not to mix queue **connections** for central & tenant jobs due to potential leftover global state, e.g. central jobs thinking they're in the previous tenant's context.
To dispatch a job such that it will run centrally under all circumstances, create a new queue connection and set the `central` key to `true`. For example:
```jsx
// queue.connections
'central' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'central' => true, // <---
],
```
And use this connection like this:
```jsx
dispatch(new SomeJob(...))->onConnection('central');
```

View file

@ -0,0 +1,181 @@
---
title: Quickstart Tutorial
extends: _layouts.documentation
section: content
---
# 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:
```php
composer require stancl/tenancy:3.x-dev
```
After stable 3.x is released, update your version constraint by running `composer require stancl/tenancy:^3.0`
Then, run the `tenancy:install` command:
```php
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
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:
```php
/*
* 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
<?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:
```php
// 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.
```php
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');
}
```
## 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:
```php
'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:
```php
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.
```php
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
$ 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:
```php
App\Tenant::cursor()->runForEach(function () {
factory(App\User::class)->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.

View file

@ -0,0 +1,79 @@
---
title: Routes
extends: _layouts.documentation
section: content
---
# Routes
This package has a concept of central routes and tenant routes. Central routes are only available on central domains, and tenant routes are only available on tenant domains. If you don't use domain identification, then all routes are always available and you may skip the details about preventing access from other domains.
## Central routes
You may register central routes in `routes/web.php` or `routes/api.php` like you're used to. However, you need to make one small change to your RouteServiceProvider.
You don't want central routes — think landing pages and sign up forms — to be accessible on tenant domains. For that reason, register them in such a way that they're **only** accessible on your central domains:
```php
// RouteServiceProvider
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');
}
```
Note: If you're using multiple central domains, you can't use route names, because different routes (= different combinations of domains & paths) can't share the same name. If you need to use a different central domain for testing, use `config()->set()` in your TestCase `setUp()`.
## Tenant routes
You may register tenant routes in `routes/tenant.php`. These routes have no middleware applied on them, and their controller namespace is specified in `app/Providers/TenancyServiceProvider`.
By default, you will see the following setup:
```php
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');
});
});
```
Routes within this group will have the `web` middleware group, an initialization middleware, and finally, a middleware related to the section below applied on them.
You may do the same for the `api` route group, for instance.
Also, you may use different initialization middleware than the domain one. For a full list, see the [Tenant identification]({{ $page->link('tenant-identification') }}) page.
### Conflicting paths
Due to the order in which the service providers (and as such, their routes) are registered, tenant routes will take precedence over central routes. So if you have a `/` route in your `routes/web.php` file but also `routes/tenant.php`, the tenant route will be used on tenant domains.
However, tenant routes that don't have their central counterpart will still be accessible on central domains and will result in a "Tenant could not be identified on domain ..." error. To avoid this, use the `Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains` middleware on all of your tenant routes. This middleware will abort with a 404 if the user is trying to visit a tenant route on a central domain.
## Universal routes

View file

@ -0,0 +1,27 @@
---
title: Session scoping
extends: _layouts.documentation
section: content
---
# Session scoping
Session scoping is one thing that you might have to deal with yourself.
The issue occurs when you're using multiple tenant domains and databases. Users can change their session cookie's domain and their session data will be shared in another tenant's application.
Here's how you can prevent this.
## Storing sessions in the database
Since the databases are automatically separated, simply using the database as the session driver will make this problem disappear altogether.
## Storing sessions in Redis
This is the same solution as using the DB session driver. If you use the [`RedisTenancyBootstrapper`]({{ $page->link('tenancy-bootstrappers') }}), your Redis databases will be automatically separated for your tenants, and as such, any sessions stored in those Redis databases will be scoped correctly.
## Using a middleware to prevent session forgery
Alternatively, you may use the `Stancl\Tenancy\Middleware\ScopeSessions` middleware on your tenant routes to make sure that any attempts to manipulate the session will result in a 403 unauthorized response.
This will work with all storage drivers, **but only assuming you use a domain per tenant.** If you use path identification, you **need** to store sessions in the database (if using multi-DB tenancy), or you need to use single-DB tenancy (which is probably more common with path identification).

View file

@ -0,0 +1,161 @@
---
title: Single-database tenancy
extends: _layouts.documentation
section: content
---
# 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]({{ $page->link('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 **Tenant** 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:
```php
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:
```php
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:
```php
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:
```php
$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:**
```php
$table->unique(['tenant_id', 'slug']);
```
and this on **secondary models:**
```php
// Imagine we're in a 'comments' table migration
$table->unique(['post_id', 'user_id']);
```
TODO: Unique constraints, validation, mention that eloquent models will be scoped, but DB calls won't
### 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:
```php
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.
[Tenants](Tenants%20e929a50afae2436c936b323fab4a0f60.md)
You'll be able to use these two methods:
```php
// 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:
```php
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`.

View file

@ -0,0 +1,220 @@
---
title: Synced resources between tenants
extends: _layouts.documentation
section: content
---
# Synced resources between tenants
If you'd like to share certain resources, usually users, between tenant databases, you can use our resource syncing feature. This will let you **sync specific columns between specific tenant databases and the central database.**
## Database
The resource exists in the central database, for example a `users` table. Another table exists in the tenants' databases. It can use the same name as the central database or a different name — up to you.
Then there's a pivot table in the central database that maps the resource (`users` in our case) to tenants.
The resource isn't synced with all tenant databases — that would be unwanted, e.g. a user typically only exists in select few tenants.
## Concepts
You will need two models for the resource. One for the tenant database and one for the central database. The tenant model must implement the `Syncable` interface and the central model must implement the `SyncMaster` interface.
`SyncMaster` is an extension of `Syncable`, it requires one extra method — the relationship to tenants, to know which tenants also have this resource.
Both models must use the `ResourceSyncing` trait. This trait makes sure that a `SyncedResourceSaved` event is fired whenever the model is saved. The `UpdateSyncedResource` listener will update the resource in the central database and in all tenant databases where the resource exists. The listener is registered in your `TenancyServiceProvider`.
An important requirement of the `Syncable` interface is the `getSyncedAttributeNames()` method. You don't want to sync all columns (or more specifically, attributes, since we're talking about Eloquent models — **accessors & mutators are supported**). In the `users` example, you'd likely only want to sync attributes like the name, email and password, while keeping tenant-specific (or workspace-specific/team-specific, whatever makes sense in your project's terminology) attributes independent.
The resource needs to have the same global ID in the central database and in tenant databases.
## How it works
Let's write an example implementation:
```php
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Database\Models\TenantPivot;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
public function users()
{
return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id')
->using(TenantPivot::class);
}
}
class CentralUser extends Model implements SyncMaster
{
// Note that we force the central connection on this model
use ResourceSyncing, CentralConnection;
protected $guarded = [];
public $timestamps = false;
public $table = 'users';
public function tenants(): BelongsToMany
{
return $this->belongsToMany(Tenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id')
->using(TenantPivot::class);
}
public function getTenantModelName(): string
{
return User::class;
}
public function getGlobalIdentifierKey()
{
return $this->getAttribute($this->getGlobalIdentifierKeyName());
}
public function getGlobalIdentifierKeyName(): string
{
return 'global_id';
}
public function getCentralModelName(): string
{
return static::class;
}
public function getSyncedAttributeNames(): array
{
return [
'name',
'password',
'email',
];
}
}
class User extends Model implements Syncable
{
use ResourceSyncing;
protected $guarded = [];
public $timestamps = false;
public function getGlobalIdentifierKey()
{
return $this->getAttribute($this->getGlobalIdentifierKeyName());
}
public function getGlobalIdentifierKeyName(): string
{
return 'global_id';
}
public function getCentralModelName(): string
{
return CentralUser::class;
}
public function getSyncedAttributeNames(): array
{
return [
'name',
'password',
'email',
];
}
}
// Pivot table migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTenantUsersTable extends Migration
{
public function up()
{
Schema::create('tenant_users', function (Blueprint $table) {
$table->increments('id');
$table->string('tenant_id');
$table->string('global_user_id');
$table->unique(['tenant_id', 'global_user_id']);
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
$table->foreign('global_user_id')->references('global_id')->on('users')->onUpdate('cascade')->onDelete('cascade');
});
}
public function down()
{
Schema::dropIfExists('tenant_users');
}
}
```
Here's how it will work:
- You create a user in the central database. It only exists in the central DB.
```php
$user = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'superadmin', // unsynced
]);
```
- Now you create a user **with the same global id** in a tenant's database:
```php
tenancy()->initialize($tenant);
// Create the same user in tenant DB
$user = TenantUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // unsynced
]);
```
- You update some attribute on the tenant:
```php
$user->update([
'name' => 'John Foo', // synced
'email' => 'john@foreignhost', // synced
'role' => 'admin', // unsynced
]);
```
- The central user's `name` and `email` have changed.
If you create more tenants and create the user in those tenants' databases, the changes will be synced between all these tenants' databases and the central database.
Creating the user inside a tenant's database will copy the resource 1:1 to the central database, including the unsynced columns (here they act as default values).
## Attaching resources tenants
You can see that in the example above we're using the `TenantPivot` model for the BelongsToMany relationship. This lets us cascade synced resources from the central database to tenants:
```php
$user = CentralUser::create(...);
$user->attach($tenant);
```
Attaching a tenant to a user will copy even the unsynced columns (they act as default values), similarly to how creating the user inside the tenant's database will copy the tenant to the central database 1:1.
If you'd like to use a custom pivot model, look into the source code of `TenantPivot` to see what to copy (or extend it) if you want to preserve this behavior.
## Queueing
In production, you're almost certainly want to queue the listener that copies the changes to other databases. To do this, change the listener's static property:
```php
\Stancl\Tenancy\Listeners\UpdateSyncedResource::$shouldQueue = true;
```

View file

@ -0,0 +1,95 @@
---
title: Tenancy bootstrappers
extends: _layouts.documentation
section: content
---
# Tenancy bootstrappers
Tenancy bootstrappers are classes which make your application tenant-aware in such a way that you don't have to change a line of your code, yet things will be scoped to the current tenant.
The package comes with these bootstrappers out of the box:
## Database tenancy bootstrapper
The database tenancy bootstrapper switches the **default** database connection to `tenant` after it constructs the connection for that tenant.
[Customizing databases]({{ $page->link('customizing-databases') }})
Note that only the **default** connection is switched. If you use another connection explicitly, be it using `DB::connection('...')`, a model `getConnectionName()` method, or a model trait like `CentralConnection`, **it will be respected.** The bootstrapper doesn't **force** any connections, it merely switches the default one.
## Cache tenancy bootstrapper
The cache tenancy bootstrapper replaces the Laravel's CacheManager instance with a custom CacheManager that adds tags with the current tenant's ids to each cache call. This scopes cache calls and lets you selectively clear tenants' caches:
```php
php artisan cache:clear --tag=tenant_123
```
Note that you must use a cache store that supports tagging, e.g. Redis.
## Filesystem tenancy bootstrapper
This bootstrapper does the following things:
- Suffixes roots of disks used by the `Storage` facade
- Suffixes `storage_path()` (useful if you're using the local disk for storing tenant data)
- Makes `asset()` calls use the TenantAssetController to retrieve tenant-specific data
- Note: For some assets, e.g. images, you may want to use `global_asset()` (if the asset is shared for all tenants). And for JS/CSS assets, you should use `mix()` or again `global_asset()`.
This bootstrapper is the most complex one, by far. We will have a — better written — explanation in v3 docs soon, but for now, refer to the 2.x docs for information about filesystem tenancy. [https://tenancyforlaravel.com/docs/v2/filesystem-tenancy/](https://tenancyforlaravel.com/docs/v2/filesystem-tenancy/)
If you don't want to bootstrap filesystem tenancy in this way, and want to — for example — provision an S3 bucket for each tenant, you can absolutely do that. Look at the package's bootstrappers to get an idea of how to write one yourself, and feel free to implement it any way you want.
## Queue tenancy bootstrapper
This bootstrapper adds the current tenant's ID to the queued job payloads, and initializes tenancy based on this ID when jobs are being processed.
You can read more about this on the *Queues* page:
[Queues]({{ $page->link('queues') }})
## Redis tenancy bootstrapper
If you're using `Redis` calls (not cache calls, **direct** Redis calls) inside the tenant app, you will want to scope Redis data too. To do this, use this bootstrapper. It changes the Redis prefix for each tenant.
Note that you need phpredis, predis won't work.
## Writing custom bootstrappers
If you want to bootstrap tenancy for something not covered by this package — or something covered by this package, but you want different behavior — you can do that by creating a bootstrapper class.
The class must implement the `Stancl\Tenancy\Contracts\TenancyBootstrapper` interface:
```php
namespace App;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
class MyBootstrapper implements TenancyBootstrapper
{
public function bootstrap(Tenant $tenant)
{
// ...
}
public function revert()
{
// ...
}
}
```
Then, register it in the `tenancy.bootstrappers` config:
```php
'bootstrappers' => [
Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
App\MyBootstrapper::class,
],
```

View file

@ -0,0 +1,98 @@
---
title: Tenant identification
extends: _layouts.documentation
section: content
---
# Tenant identification
The package lets you identify tenants using the following methods:
- Domain identification (`acme.com`)
- Subdomain identification (`acme.yoursaas.com`)
- Domain OR subdomain identification (both of the above)
- Path identification (`yoursaas.com/acme/dashboard`)
- Request data identification (`yoursaas.com/users?tenant_id=acme` — or using request headers)
However, **you're free to write additional tenant resolvers.**
All of the identification methods mentioned above come with their own middleware. You can read more about each identification method below.
## Domain identification
To use this identification method, make sure your tenant model uses the `HasDomains` trait.
Be sure to read the [Domains]({{ $page->link('domains') }}) page of the documentation.
The relationship is `Tenant hasMany Domain`. Store the hostnames in the `domain` column of the `domains` table.
This identification method comes with the `Stancl\Tenancy\Middleware\InitializeTenancyByDomain` middleware.
## Subdomain identification
This is the exact same as domain identification, except you store the subdomains in the `domain` column of the `domains` table.
The benefit of this approach rather than storing the subdomain's full hostname in the `domain` column is that you can use this subdomain on **any** of your central domains.
The middleware for this method is `Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain`.
## Combined domain/subdomain identification
If you'd like to use subdomains and domains at the same time, use the `Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain` middleware.
Records that contain **dots** in the `domain` column will be treated as domains (hostnames) and records that don't contain any dots will be treated as subdomains.
## Path identification
Some applications will want to use a single domain, but use paths to identify the tenant. This would be when you want customers to use your branded product rather than giving them a whitelabel product that they can use on their own domains.
To do this, use the `Stancl\Tenancy\Middleware\InitializeTenancyByPath` middleware **and make sure your routes are prefixes with `/{tenant}`**.
```php
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
Route::group([
'prefix' => '/{tenant}',
'middleware' => [InitializeTenancyByPath::class],
], function () {
Route::get('/foo', 'FooController@index');
});
```
If you'd like to customize the name of the argument (e.g. use `team` instead of `tenant`, look into the middleware for the public static property).
## Request data identification
You might want to identify tenants based on request data (headers or query parameters). Applications with SPA frontends and API backends may want to use this approach.
The middleware for this identification method is `Stancl\Tenancy\Middleware\InitializeTenancyByRequestData`.
You may customize what this middleware looks for in the request. By default, it will look for the `X-Tenant` header. If the header is not found, it will look for the `tenant` query parameter.
If you'd like to use a different header, change the static property:
```php
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
InitializeTenancyByRequestData::$header = 'X-Team';
```
If you'd like to only use the query parameter identification, set the header static property to null:
```php
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
InitializeTenancyByRequestData::$header = null;
```
If you'd like to disable the query parameter identification and only ever use the header, set the static property for the parameter to null:
```php
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
InitializeTenancyByRequestData::$queryParameter = null;
```
## Manually identifying tenants
See the [manual initialization page]({{ $page->link('manual-initialization') }}) to see how to identify tenants manually.

View file

@ -0,0 +1,126 @@
---
title: Tenants
extends: _layouts.documentation
section: content
---
# Tenants
A tenant can be any model that implements the `Stancl\Tenancy\Contracts\Tenant` interface.
The package comes with a base `Tenant` model that's ready for common things, though will require extending in most cases as it attempts not to be too opinionated.
The base model has the following features on top of the ones that are necessary by the interface:
- Forced central connection (lets you interact with Tenant models even in tenant contexts)
- Data column trait — lets you store arbitrary keys. Attributes that don't exist as columns on your `tenants` table go to the `data` column as serialized JSON.
- Id generation trait — when you don't supply an ID, a random uuid will be generated. An alternative to this would be using AUTOINCREMENT columns. If you wish to use numerical ids, change the `create_tenants_table` migration to use `bigIncrements()` or some such column type, and set `tenancy.id_generator` config to null. That will disable the ID generation altogether, falling back to the database's autoincrement mechanism.
**Most** applications using this package will want domain/subdomain identification and tenant databases.
To do this, create a new model, e.g. `App\Tenant`, that looks like this:
```php
<?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;
}
```
Then, configure the package to use this model in `config/tenancy.php`:
```php
'tenant_model' => \App\Tenant::class,
```
If you want to customize the `Domain` model, you can do that too.
**If you don't need domains or databases, ignore the steps above.** Everything will work just as well.
## Creating tenants
You can create tenants like any other models:
```php
$tenant = Tenant::create([
'plan' => 'free',
]);
```
After the tenant is created, an event will be fired. This will result in things like the database being created and migrated, depending on what jobs listen to the event.
## Custom columns
Attributes of the tenant model which don't have their own column will be stored in the `data` JSON column.
You may define the custom columns be overriding the `getCustomColumns()` method on your `Tenant` model:
```php
public static function getCustomColumns(): array
{
return [
'id',
'plan',
'locale',
];
}
```
**Don't forget to keep `id` in the custom columns!**
If you want to rename the `data` column, rename it in a migration and implement this method on your model:
```php
public static function getDataColumn(): string
{
return 'my-data-column';
}
```
## Running commands in the tenant context
You may run commands in a tenant's context and then return to the previous context (be it central, or another tenant's) by passing a callable to the `run()` method on the tenant object. For example:
```php
$tenant->run(function () {
User::create(...);
});
```
## Internal keys
Keys that start with the internal prefix (`tenancy_` by default, but you can customize this by overriding the `internalPrefix()` method) are for internal use, so don't start any attribute/column names with that.
## Events
The `Tenant` model dispatches Eloquent events, all of which have their own respective class. You can read more about this on the *Event system* page.
[Event system]({{ $page->link('event-system') }})
## Accessing the current tenant
You may access the current tenant using the `tenant()` helper. You can also pass an argument to get an attribute from that tenant model, e.g. `tenant('id')`.
Alternatively, you may typehint the `Stancl\Tenancy\Contracts\Tenant` interface to inject the model using the service container.
## Incrementing IDs
By default, the migration uses `string` for the `id` column, and the model generates UUIDs when you don't supply an `id` during tenant creation.
If you'd like to use incrementing ids instead, you can override the `getIncrementing()` method:
```php
public function getIncrementing()
{
return true;
}
```

View file

@ -0,0 +1,60 @@
---
title: Testing
extends: _layouts.documentation
section: content
---
# Testing
TODO: Review
## Central app
To test your central app, just write normal Laravel tests.
## Tenant app
Note: If you're using multi-database tenancy & the automatic mode, it's not possible to use `:memory:` SQLite databases or the `RefreshDatabase` trait due to the switching of default database.
To test the tenant part of the application, create a tenant in the `setUp()` method and initialize tenancy.
You may also want to do something like this:
```php
class TestCase // extends ...
{
protected $tenancy = false;
public function setUp(): void
{
if ($this->tenancy) {
$this->initializeTenancy();
}
}
public function initializeTenancy()
{
$tenant = Tenant::create();
tenancy()->initialize($tenant);
}
// ...
}
```
And in your individual test cases:
```php
class FooTest extends TestCase
{
protected $tenancy = true;
/** @test */
public function some_test()
{
$this->assertTrue(...);
}
}
```
Or you may want to create a separate TestCase class for tenant tests, for better organization.

View file

@ -0,0 +1,22 @@
---
title: The two applications
extends: _layouts.documentation
section: content
---
# The two applications
You will find these two terms a lot throughout this documentation:
- central application
- tenant application
Those terms refer to the parts of your application that house the central logic, and the tenant logic.
The tenant application is executed in tenant context — usually with the tenant's database, cache, etc. The central application is executed **when there is no tenant**.
The central application will house your signup page **where tenants are created**, your admin panel used to **manage your tenants**, etc.
The tenant application will likely house the larger part of your application — the real service being used by your tenants.
TODO: Explain how to structure application code for clarity

View file

@ -0,0 +1,77 @@
---
title: Upgrading from 2.x
extends: _layouts.documentation
section: content
---
# Upgrading from 2.x
> This hasn't been tested yet and might not be 100% right. It *should* work well, but note the todo at the bottom of the script for updating the `domains` table.
- Move all `_tenancy_` prefixed keys in tenant storage to `tenancy_` prefixed keys
Run this **with 2.x code** (but put application down first, this will break your app and require that you update to 3.x immediately after). Of course make a backup of the DB first.
(Note: You may want to make this part of a migration)
```php
use Stancl\Tenancy\Tenant;
tenancy()
->all()
->each(function (Tenant $tenant) {
$keys = array_keys($tenant->data);
$keys = array_filter($keys, function ($key) {
return Str::startsWith($key, '_tenancy_');
});
foreach ($keys as $key) {
$value = $tenant->data[$key];
unset($tenant->data[$key]);
$tenant->data[str_replace('_tenancy_', 'tenancy_', $key)] = $value;
}
$tenant->save();
});
```
- Add an `id` autoincrement column to `domains` table and retroactively generate the ids
(Note: You may want to make this part of a migration)
```php
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Schema\Blueprint;
$domains = DB::table('domains')->get();
Schema::table('domains', function (Blueprint $table) {
$table->unsignedBigInteger('id')->nullable();
});
$counter = 1;
foreach ($domains as $domain) {
DB::table('domains')
->where('domain', $domain->domain)
->update(['id' => $counter]);
$counter += 1;
}
Schema::table('domains', function (Blueprint $table) {
$table->dropPrimary();
});
Schema::table('domains', function (Blueprint $table) {
$table->primary('id'); // todo: here we want to make it autoincrement, not just primary
});
```
- Replace your Http Kernel with a stock version (copy it from laravel/laravel: [https://github.com/laravel/laravel/blob/master/app/Http/Kernel.php](https://github.com/laravel/laravel/blob/master/app/Http/Kernel.php)) and add back in any changes you made. The package now doesn't necessitate any Kernel changes, so remove all of the 2.x ones.
- Delete config, publish it & the new files using `php artisan tenancy:install`
- Create Tenant model, as instructed on the Tenants page
[Tenants](Tenants%20e929a50afae2436c936b323fab4a0f60.md)
- Update routes to use the correct middleware, see the Routes page
[Routes](Routes%20f0bfa87432bb4a7da9d9f5f150bd1a45.md)