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,158 @@
---
title: Application Testing
description: Application Testing..
extends: _layouts.documentation
section: content
---
# Application Testing {#application-testing}
> Note: At the moment it's not possible to use `:memory:` SQLite databases or the `RefreshDatabase` trait due to the switching of default database. This will hopefully change in the future.
### Initializing tenancy
You can create tenants in the `setUp()` method of your test case:
```php
protected function setUp(): void
{
parent::setUp();
tenancy()->create('test.localhost');
tenancy()->init('test.localhost');
}
```
If you don't want to initialize tenancy before each test, you may 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($domain = 'test.localhost')
{
tenancy()->create($domain);
tenancy()->init($domain);
}
// ...
}
```
And in your individual test classes:
```php
class FooTest
{
protected $tenancy = true;
/** @test */
public function some_test()
{
$this->assertTrue(...);
}
}
```
### Cleanup
To delete tenants & their databases after tests, you may use this:
```php
public function tearDown(): void
{
config([
'tenancy.queue_database_deletion' => false,
'tenancy.delete_database_after_tenant_deletion' => true,
]);
tenancy()->all()->each->delete();
parent::tearDown();
}
```
### Storage setup
If you're using the database storage driver, you will need to run the migrations in `setUp()`:
```php
protected function setUp(): void
{
parent::setUp();
$this->artisan('migrate:fresh');
// ...
}
```
If you're using the Redis storage driver, flush the database in `setUp()`:
```php
protected function setUp(): void
{
parent::setUp();
// make sure you're using a different connection for testing to avoid losing data
Redis::connection('tenancyTesting')->flushdb();
// ...
}
```
### Sample TestCase
Put together, here's a ready-to-use base TestCase for the DB storage driver
```php
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
public function setUp(): void
{
parent::setUp();
$this->artisan('migrate:fresh');
config([
'tenancy.queue_database_creation' => false,
]);
config(['tenancy.exempt_domains' => [
'127.0.0.1',
'localhost',
]]);
}
public function tearDown(): void
{
config([
'tenancy.queue_database_deletion' => false,
'tenancy.delete_database_after_tenant_deletion' => true,
]);
tenancy()->all()->each->delete();
parent::tearDown();
}
}
```
phpunit.xml:
```xml
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value="database/testing.sqlite"/>
```
> Don't forget to create an empty database/testing.sqlite
You may also wish to add `testing.sqlite` to `database/.gitignore`.

View file

@ -0,0 +1,16 @@
---
title: Cached Tenant Lookup
description: Cached Tenant Lookup
extends: _layouts.documentation
section: content
---
# Cached Tenant Lookup {#cached-tenant-lookup}
If you're using the database storage driver, you may want to cache tenant lookup (domain -> tenant id -> `Tenant` object mapping). Running DB queries on each request to identify the tenant is somewhat expensive, as a separate database connection has to be established.
To avoid this, you may want to enable caching.
You may enable this feature by setting the `tenancy.storage_drivers.db.cache_store` config key to the name of your cache store (e.g. `redis`), and optionally setting `cache_ttl` (default is 3600 seconds).
The cache invalidation of course happens automatically, as long as you modify your tenants using `Tenant` objects and not direct DB calls.

View file

@ -0,0 +1,54 @@
---
title: Central App
description: Central App
extends: _layouts.documentation
section: content
---
# Central App {#central-app}
This package uses routes to separate the tenant part of the application from the central part of the application. The central part will commonly include a landing page, sign up form, and some sort of dashboard.
## Central routes {#central-routes}
Routes in the `routes/web.php` file are the central routes. When they are visited, tenancy is *not* intialized and any model, cache call, controller, job dispatch, Redis call and anything else that is called in during this request will be central.
## Central domains {#central-domains}
However, since you don't want routes related to the app on your main domain and sign up forms on tenant domains, you must also define what domains host the central stuff in the `tenancy.exempt_domains` config.
The exempt domains should not include the protocol. For example, you should include `example.com` rather than `https://example.com`.
## Using central things inside the tenant app {#using-central-things-inside-the-tenant-app}
To use central things (databases/caches/Redis connections/filesystems/...) on special places of your tenant app, you may do the following.
### Central database {#central-database}
Create a new connection and use it like `DB::connection($connectionName)->table('foo')->where(...)`
If you want to use models, create a `getConnectionName()` method that returns the name of the central connection
### Central redis {#central-redis}
Create a new connection, *don't* put it into `tenancy.redis.prefixed_connections`, and use it like `Redis::connection('foo')->get('bar')`
### Central cache {#central-cache}
Use the `GlobalCache` facade, or the `global_cache()` helper.
### Central storage {#central-storage}
Create a disk and *don't* add it to `tenancy.filesystem.disks`.
### Central assets {#central-assets}
Mix is intended for template-related assets and as such, it's not scoped to the current tenant.
Alternatively, the package provides a `global_asset()` helper which is a non-tenant-aware replacement for `asset()`, in case you don't want to use `mix()`.
It's recommended to use `mix()` though, due to its features such as version tagging.
### Central queues {#central-queues}
Create a new queue connection with the `central` key set to `true`.

View file

@ -0,0 +1,137 @@
---
title: Configuration
description: Configuring stancl/tenancy
extends: _layouts.documentation
section: content
---
# Configuration {#configuration}
The `config/tenancy.php` file lets you configure how the package behaves.
> If the `tenancy.php` file doesn't exist in your `config` directory, you can publish it by running `php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config`
### `storage_driver, storage` {#storage}
This lets you configure the driver for tenant storage, i.e. what will be used to store information about your tenants. You can read more about this on the [Storage Drivers]({{ $page->link('storage-drivers') }}) page.
Available storage drivers:
- `Stancl\Tenancy\StorageDrivers\RedisStorageDriver`
- `Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver`
#### db {#db-storage-driver}
- `data_column` - the name of column that holds the tenant's data in a single JSON string
- `custom_columns` - list of keys that shouldn't be put into the data column, but into their own column
- `connection` - what database connection should be used to store tenant data (`null` means the default connection)
- `table_names` - the table names used by the models that come with the storage driver
> Note: Don't use the models directly. You're supposed to use [storage]({{ $page->link('tenant-storage') }}) methods on `Tenant` objects.
#### redis {#redis-db-driver}
- `connection` - what Redis connection should be used to store tenant data. See the [Storage Drivers]({{ $page->link('storage-drivers') }}) documentation.
### `tenant_route_namespace` {#tenant-route-namespace}
Controller namespace used for routes in `routes/tenant.php`. The default value is the same as the namespace for `web.php` routes.
### `exempt_domains` {#exempt-domains}
If a hostname from this array is visited, the `tenant.php` routes won't be registered, letting you use the same routes as in that file. This should be the domain without the protocol (i.e., `example.com` rather than `https://example.com`).
### `database` {#database}
The application's default connection will be switched to a new one — `tenant`. This connection will be based on the connection specified in `tenancy.database.based_on`. The database name will be `tenancy.database.prefix + tenant id + tenancy.database.suffix`.
You can set the suffix to `.sqlite` if you're using sqlite and want the files to be with the `.sqlite` extension. Conversely, you can leave the suffix empty if you're using MySQL, for example.
### `redis` {#redis}
If the `RedisTenancyBootstrapper` is enabled (see `bootstrappers` below), any connections listed in `tenancy.redis.prefixed_connections` will be prefixed with `config('tenancy.redis.prefix_base') . $id`.
> Note: You need phpredis. Predis support will dropped by Laravel in version 7.
### `cache` {#cache}
The `CacheManager` instance that's resolved when you use the `Cache` or the `cache()` helper will be replaced by `Stancl\Tenancy\CacheManager`. This class automatically uses [tags](https://laravel.com/docs/master/cache#cache-tags). The tag will look like `config('tenancy.cache.tag_base') . $id`.
### `filesystem` {#filesystem}
The `storage_path()` will be suffixed with a directory named `config('tenancy.filesystem.suffix_base') . $id`.
The root of each disk listed in `tenancy.filesystem.disks` will be suffixed with `config('tenancy.filesystem.suffix_base') . $id`.
For disks listed in `root_override`, the root will be that string with `%storage_path%` replaced by `storage_path()` *after* tenancy has been initialized. All other disks will be simply suffixed with `tenancy.filesystem.suffix_base` + the tenant id.
Read more about this on the [Filesystem Tenancy]({{ $page->link('filesystem-tenancy') }}) page.
### `database_managers` {#database_managers}
Tenant database managers handle the creation & deletion of tenant databases. This configuration array maps the database driver name to the `TenantDatabaseManager`, e.g.:
```php
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class
```
### `database_manager_connections` {#database_maanger_connections}
Connections used by TenantDatabaseManagers. They tell, for example, that the manager for the `mysql` driver (`MySQLDatabaseManager`) should use the `mysql` connection. You may want to change this if your connection is named differently, e.g. a MySQL connection named `central`.
### `bootstrappers` {#bootstrappers}
These are the classes that do the magic. When tenancy is initialized, TenancyBootstrappers are executed, making Laravel tenant-aware.
This config is an array. The key is the alias and the value is the full class name.
```php
'cache' => Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper::class,
```
The aliases are used by the [event system]({{ $page->link('hooks') }})
### `features` {#bootstrappers}
[Features]({{ $page->link('optional-features') }}) are similar to bootstrappers, but they are executed regardless of whether tenancy has been initialized or not. Their purpose is to provide additional functionality beyond what is necessary for the package to work. Things like easy redirects to tenant domains, tags in Telescope, etc.
### `home_url` {#home-url}
When a user tries to visit a non-tenant route on a tenant domain, the `PreventAccessFromTenantDomains` middleware will return a redirect to this url.
### `queue_database_creation` {#queue-database-creation}
- Default: `false`
### `migrate_after_creation` {#migrate-after-creation}
Run migrations after creating a tenant.
- Default: `false`
### `seed_after_migration` {#seed-after-migration}
Run seeds after creating a tenant.
- Default: `false`
### `seeder_parameters` {#seeder_parameters}
Parameters passed to the `tenants:seed` command.
- Default: `['--class' => 'DatabaseSeeder']`
### `delete_database_after_tenant_deletion` {#delete-database-after-tenant-deletion}
Delete the tenant's database after deleting the tenant.
- Default: `false`
### `queue_database_deletion` {#queue-database-deletion}
- Default: `false`
### `unique_id_generator` {#unique-id-generator}
The class used to generate a random tenant ID (when no ID is supplied during the tenant creation process).
- Default: `Stancl\Tenancy\UUIDGenerator`

View file

@ -0,0 +1,91 @@
---
title: Console Commands
description: Console commands..
extends: _layouts.documentation
section: content
---
# Console Commands {#console-commands}
The package comes with some artisan commands that will help you during development.
## Migrate {#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: Tenant migrations must be located in `database/migrations/tenant`.
You can use these commands outside the command line as well. If you want to migrate a tenant's database in a controller, you can use the `Artisan` facade.
```php
$tenant = tenancy()->create('tenant1.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant['id']]
]);
```
## Rollback & seed {#rollback}
- Rollback: `tenants:rollback`
- Seed: `tenants:seed`
Similarly to [migrate](#migrate), these commands accept a `--tenants` option.
## Migrate fresh {#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:
```none
php artisan tenants:migrate-fresh --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
## Run {#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 {#tenant-list}
```none
php artisan tenants:list
Listing all tenants.
[Tenant] id: dbe0b330-1a6e-11e9-b4c3-354da4b4f339 @ localhost
[Tenant] id: 49670df0-1a87-11e9-b7ba-cf5353777957 @ dev.localhost
```
## Create tenant {#create-tenant}
This command lets you create tenants from the command line. You may find this useful if you need to create tenants from some service that's separate from your app.
You may specify any amount of domains using `-d <domain>`. To set data during the creation process, add arguments of the `<key>=<value>` format to the end of the call.
For example:
```none
php artisan tenants:create -d aaa.localhost -d bbb.localhost plan=free email=foo@test.local
5f6dbfb8-41da-4398-a361-5342a98d81a0
```
The command returns the created tenant's id.
## Selectively clearing tenant cache {#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,38 @@
---
title: Creating Tenants
description: Creating tenants
extends: _layouts.documentation
section: content
---
# Creating Tenants {#creating-tenants}
> **Make sure your database is correctly [configured]({{ $page->link('configuration/#database') }}) before creating tenants.**
To create a tenant, you can use
```php
use Stancl\Tenancy\Tenant;
Tenant::new()
->withDomains(['tenant1.yourapp.com', 'tenant1.com'])
->withData(['plan' => 'free'])
->save();
```
> Tip: All domains under `.localhost` are routed to 127.0.0.1 on most operating systems. This is useful for development.
The `withDomains()` and `withData()` methods are optional.
You can also create a tenant using a single method: `Tenant::create`:
```php
$domains = ['tenant1.myapp.com', 'tenant1.com'];
Tenant::create($domains, [
'plan' => 'free',
]);
```
`Tenant::create()` works with both `Stancl\Tenancy\Tenant` and the facade, `\Tenant`.
> Note: By default, creating a tenant doesn't run [migrations]({{ $page->link('tenant-migrations' )}}) automatically. You may change this behavior using the `migrate_after_creation` [configuration]({{ $page->link('configuration#migrate-after-creation') }}).

View file

@ -0,0 +1,20 @@
---
title: Custom Database Names
description: Custom Database Names..
extends: _layouts.documentation
section: content
---
# Custom Database Names {#custom-database-names}
To set a specific database name for a tenant, set the `_tenancy_db_name` key in the tenant's storage.
You should do this during the tenant creation process, to make sure the right database name is used during database creation.
```php
use Stancl\Tenancy\Tenant;
Tenant::create('example.com', [
'_tenancy_db_name' => 'example_com'
])
```

View file

@ -0,0 +1,26 @@
---
title: Custom Database Connections
description: Custom Database Connections
extends: _layouts.documentation
section: content
---
# Custom Database Connections {#custom-database-names}
To set a specific database connection for a tenant, set the `_tenancy_db_connection` key in the tenant's storage. The connection's database name will be still replaced by the tenant's database name. You can [customize that]({{ $page->link('custom-database-names') }}) too.
You may want custom connections to be dynamic (rather than adding them to the DB config manually), so can use something like this:
```php
// Make new tenants use your connection "template"
Tenant::new()->withData([
'_tenancy_db_connection' => 'someTenantConnectionTemplate',
]);
// Make tweaks to the connection before bootstrapping tenancy
tenancy()->hook('bootstrapping', function ($tenantManager) {
config(['database.connections.someTenantConnectionTemplate.name' => $tenantManager->getTenant('database_name')]);
config(['database.connections.someTenantConnectionTemplate.password' => $tenantManager->getTenant('database_password')]);
config(['database.connections.someTenantConnectionTemplate.host' => $tenantManager->getTenant('database_host')]);
});
```

View file

@ -0,0 +1,41 @@
---
title: Difference Between This Package And Others
description: Difference Between This Package And Others
extends: _layouts.documentation
section: content
---
# Difference Between This Package And Others
A frequently asked question is the difference between this package and other tenancy packages for Laravel.
## tenancy/multi-tenant (previously hyn/multi-tenant)
tenancy/multi-tenant gives you an API for making your application multi-tenant yourself. It gives you a tenant DB connection, traits to apply on your models, a guide on creating your own tenant-aware cache, etc.
## tenancy/tenancy (currently in alpha)
tenancy/tenancy is even less opinionated and is more of a framework to write your own tenancy implementation. For example, there is no tenant object. There is a tenant interface that you implement on some model in your application.
## stancl/tenancy
This package takes a completely new approach to multi-tenancy.
It makes your application multi-tenant automatically and attempts to make you not have to change anything in your code. The philosophy behind this approach is that you should write your app, not tenancy boilerplate.
We belive that your code will be a lot cleaner if tenancy and the actual app don't mix. Why pollute your code with tons of tenancy implementations, when you can push all of tenancy one layer below your actual application?
Apart from saving you a ton of time, the benefit of going with the automatic approach is that you can adapt easily, since you're not bound to a specific implementation of multi-tenancy. [You can always change how tenancy is bootstrapped.]({{ $page->link('tenancy-bootstrappers') }})
This approach is also more secure. Say you have written a SaaS. The application is finished &mdash; now you just need to make it multi-tenant before releasing it. With the tenancy/\* packages, you will have to rewrite significant portions of your code and hope you did not forget to change each, for example, `Cache::` call to some tenant-aware cache that you implement yourself.
With stancl/tenancy, you just install the package, decide what routes belong to the "central" part of the app and what routes belong to the tenant part of the app, and tell the config file on what domains you host the central app &mdash; the landing page & sign up form.
Everything else happens automatically in the background:
- Database connection is switched
- Cache is made multi-tenant
- Filesystem is made multi-tenant
- Queued jobs are made multi-tenant
- Redis is made multi-tenant
This means that you can also integrate with any packages you would normally use, without any difficulties.

View file

@ -0,0 +1,9 @@
---
title: Digging Deeper
description: Digging Deeper..
extends: _layouts.documentation
section: content
---
# Digging Deeper {#digging-deeper}

View file

@ -0,0 +1,16 @@
---
title: Disabling Database Creation
description: Disabling Database Creation
extends: _layouts.documentation
section: content
---
# Disabling Database Creation {#disabling-database-creation}
DB creation can be disabled for all tenants (`tenancy.create_database` config), or for individual tenants during tenant creation:
```php
Tenant::new()->withData([
'_tenancy_create_database' => false,
])->save();
```

View file

@ -0,0 +1,12 @@
---
title: Optional Features
description: Optional Features
extends: _layouts.documentation
section: content
---
# Optional Features {#optional-features}
Similarly to [TenancyBootstrappers]({{ $page->link('tenancy-bootstrappers') }}), `Features` are classes that provide extra functionality and can be enabled in the config. They're all disabled by default, because they're not critical to the core tenancy scaffolding.
You may enable and disable `Features` in the `tenancy.features` configuration.

View file

@ -0,0 +1,34 @@
---
title: Tenant Config
description: Tenant Config
extends: _layouts.documentation
section: content
---
# Tenant Config {#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 [tenant storage]({{ $page->link('tenant-storage') }}) 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 `tenancy.storage_to_config_map` config.
For example, if your `storage_to_config_map` looked like this:
```php
'storage_to_config_map' => [
'paypal_api_key' => 'services.paypal.api_key',
],
```
the value of `paypal_api_key` in [tenant storage]({{ $page->link('tenant-storage') }}) would be copied to the `services.paypal.api_key` config when tenancy is initialized.

View file

@ -0,0 +1,17 @@
---
title: Tenant Redirect
description: Tenant Redirect
extends: _layouts.documentation
section: content
---
# Tenant Redirect {#tenant-redirect}
> To enable this feature, uncomment the `Stancl\Tenancy\Features\TenantRedirect::class` line in your `tenancy.features` config.
A customer has signed up on your website, you have created a new tenant and now you want to redirect the customer to their website. You can use the `tenant()` method on Redirect, like this:
```php
// tenant sign up controller
return redirect()->route('dashboard')->tenant($domain);
```

View file

@ -0,0 +1,16 @@
---
title: Timestamps
description: Timestamps
extends: _layouts.documentation
section: content
---
# Timestamps {#timestamps-redirect}
> To enable this feature, uncomment the `Stancl\Tenancy\Features\Timestamps::class` line in your `tenancy.features` config.
This `Feature` adds the following timestamps into the tenant storage:
- `created_at`
- `updated_at`
- `deleted_at` - for soft deletes

View file

@ -0,0 +1,69 @@
---
title: Filesystem Tenancy
description: Filesystem Tenancy..
extends: _layouts.documentation
section: content
---
# Filesystem Tenancy {#filesystem-tenancy}
> Note: It's important to differentiate between storage_path() and the Storage facade. The Storage facade is what you use to put files into storage, i.e. `Storage::disk('local')->put()`. `storage_path()` is used to get the path to the storage directory.
The `storage_path()` will be suffixed with a directory named `config('tenancy.filesystem.suffix_base') . $id`.
The root of each disk listed in `tenancy.filesystem.disks` will be suffixed with `config('tenancy.filesystem.suffix_base') . $id`.
**However, this alone would cause unwanted behavior.** It would work for S3 and similar disks, but for local disks Laravel does its own suffixing. For local storage we need the second of these examples:
```
/path_to_your_application/storage/app/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/
/path_to_your_application/storage/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/app/
```
Why? Because `storage_path()` returns:
`/path_to_your_application/storage/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/`
so Laravel's `storage_path('app')` means appending `app` to that.
That's what the `root_override` section is for. `%storage_path%` gets replaced by `storage_path()` *after* tenancy has been initialized. The roots of disks listed in the `root_override` section of the config will be replaced accordingly. All other disks will be simply suffixed with `tenancy.filesystem.suffix_base` + the tenant id.
Since `storage_path()` will be suffixed, your folder structure will look like this:
![The folder structure](https://i.imgur.com/GAXQOnN.png)
If you write to these directories, you will need to create them after you create the tenant. See the docs for [PHP's mkdir](http://php.net/function.mkdir).
Logs will be saved to `storage/logs` regardless of any changes to `storage_path()`, and regardless of tenant.
## Assets {#assets}
Laravel's `asset()` helper has two different paths of execution:
- If `config('app.asset_url')` has been set, it will simply append `tenant$id` to the end of the configured asset URL. This is useful if you use Laravel Vapor. Vapor sets the asset URL to something like `https://abcdefghijkl.cloudfrount.net/123-456-789`. That is the root for your assets. This package will append that with something like `tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd`.
- If `config('app.asset_url')` is null, as it is by default, the helper will return a URL (`/tenancy/assets/...`) to a controller provided by this package. That controller returns a file response from `storage_path("app/public/$path")`. This means that you need to store your assets in the public directory.
> Note: In 1.x, the `asset()` helper was not tenant-aware, and there was a `tenant_asset()` helper that followed the second option in the list above (a link to a controller). For backwards compatibility, that helper remains intact.
> If you have some non-tenant-specific assets, you may use the package's `global_asset()` helper.
Note that all tenant assets have to be in the `app/public/` subdirectory of the tenant's storage directory, as shown in the image above.
This is what the backend of `tenant_asset()` (and `asset()` when no asset URL is configured) returns:
```php
// TenantAssetsController
return response()->file(storage_path('app/public/' . $path));
```
With default filesystem configuration, these two commands are equivalent:
```php
Storage::disk('public')->put($filename, $data);
Storage::disk('local')->put("public/$filename", $data);
```
Note that every request for a tenant asset requires a full framework boot and tenancy initialization. This is not ideal if you have some assets that occur on each page (like logos). So for non-private assets, you may want to create a disk and use URLs from that disk instead. For example:
```php
Storage::disk('app-public')->url('tenants/logos/' . tenant()->id . '.png');
```
If you want to store something globally, simply create a new disk and *don't* add it to the `tenancy.filesystem.disks` config.

View file

@ -0,0 +1,34 @@
---
title: Getting Started
description: Getting started.
extends: _layouts.documentation
section: content
---
# Getting Started {#getting-started}
[**stancl/tenancy**](https://github.com/stancl/tenancy) is a Laravel multi-database tenancy package. It makes your app multi-tenant in a way that requires no changes to the codebase. Instead of applying traits on models and replacing every single reference to cache by a reference to a tenant-aware cache, the package lets you write your app without thinking about tenancy. It handles tenancy automatically in the background.
## How does it work? {#how-does-it-work}
A user visits `client1.yourapp.com`. The package identifies the tenant who this domain belongs to, and automatically does the following:
- switches database connection
- replaces the default cache manager
- switches Redis connection
- changes filesystem root paths
- makes jobs automatically tenant-aware
The benefits of this being taken care of by the package are:
- separation of concerns: you should write your app, not tenancy implementations
- reliability: you won't have to fear that you forgot to replace a reference to cache by a tenant-aware cache call. This is something you might worry about if you're implementing tenancy into an existing application.
## What is multi-tenancy? {#what-is-multi-tenancy}
Multi-tenancy is the ability to provide your application to multiple customers (who have their own users and other resources) from a single instance of your application. Think Slack, Shopify, etc.
Multi-tenancy can be single-database and multi-database.
**Single-database tenancy** means that your application uses only a single database. The way this is usually implemented is that instead of having the `id`, `title`, `user_id` and `body` columns in your `posts` table, you will also have a `tenant_id` column. This approach works until you need custom databases for your clients. It's also easy to implement, it basically boils down to having your models use a trait which adds a [global scope](https://laravel.com/docs/master/eloquent#global-scopes).
Again, to be clear: This package does not provide single-database tenancy features.
**Multi-database tenancy**, the type that this package provides, lets you use a separate database for each tenant. The benefits of this approach are scalability, compliance (some clients need to have the database on their server) and mitigation of risks such as showing the wrong tenant's data to a user.

View file

@ -0,0 +1,86 @@
---
title: Hooks / The Event System
description: Hooks / The Event System
extends: _layouts.documentation
section: content
---
# Hooks / The Event System
You can use event hooks to change the behavior of the package.
All hook callbacks receive the `TenantManager` as the first argument.
## Tenant events
A common use case for these events is seeding the tenant data during creation:
```php
// AppServiceProvider::boot()
tenancy()->hook('tenant.creating', function (TenantManager $tm, Tenant $tenant) {
$tenant->put([
'posts_per_page' => '15',
]);
});
```
The following events are available:
- `tenant.creating`
- `tenant.created`
- `tenant.updating`
- `tenant.updated`
- `tenant.deleting`
- `tenant.deleted`
- `tenant.softDeleting`
- `tenant.softDeleted`
Callbacks for these events may accept the following arguments:
```php
TenantManager $tenantManager, Tenant $tenant
```
## Database events
A use case for these events is executing something after the tenant database is created (& migrated/seeded) without running into race conditions.
Say you have a `AfterCreatingTenant` job that creates a superadmin user. You may use the `database.creating` event to add this job into the queue chain of the job that creates the tenant's database.
```php
tenancy()->hook('database.creating', function (TenantManager $tm, string $db, Tenant $tenant) {
return [
new AfterCreatingTenant($tenant->id);
]
});
```
The following events are available:
- `database.creating`
- `database.created`
- `database.deleting`
- `database.deleted`
Callbacks for these events may accept the following arguments:
```php
TenantManager $tenantManager, string $db, Tenant $tenant
```
## Bootstrapping/ending events
The following events are available:
- `bootstrapping`
- `bootstrapped`
- `ending`
- `ended`
You may use the `bootstrapping` & `ending` events to prevent some bootstrappers from being executed.
The following actions can be prevented:
- database connection switch: `database`
- Redis prefix: `redis`
- CacheManager switch: `cache`
- Filesystem changes: `filesystem`
- Queue tenancy: `queue`
- and anything else listed in the [`tenancy.bootstrappers` config]({{ $page->link('configuration#bootstrappers') }})
Callbacks for these events may accept the following arguments:
```php
TenantManager $tenantManager, Tenant $tenant
```

View file

@ -0,0 +1,18 @@
---
title: Horizon Integration
description: Horizon Integration..
extends: _layouts.documentation
section: content
---
# Horizon Integration
> Make sure your queue is [correctly configured]({{ $page->link('jobs-queues') }}) before using Horizon.
Jobs are automatically tagged with the tenant's id and domain:
![id and domain tags](https://i.imgur.com/K2oWTJc.png)
You can use these tags to monitor specific tenants' jobs:
![Monitoring tags](https://i.imgur.com/qB6veK7.png)

View file

@ -0,0 +1,40 @@
---
title: HTTPS Certificates
description: HTTPS Certificates..
extends: _layouts.documentation
section: content
---
# HTTPS Certificates
HTTPS certificates are very easy to deal with if you use the `yourclient1.yourapp.com`, `yourclient2.yourapp.com` model. You can use a wildcard HTTPS certificate.
If you use the model where second level domains are used, there are multiple ways you can solve this.
This guide focuses on nginx.
### 1. Use nginx with the lua module
Specifically, you're interested in the [`ssl_certificate_by_lua_block`](https://github.com/openresty/lua-nginx-module#ssl_certificate_by_lua_block) directive. Nginx doesn't support using variables such as the hostname in the `ssl_certificate` directive, which is why the lua module is needed.
This approach lets you use one server block for all tenants.
### 2. Add a simple server block for each tenant
You can store most of your config in a file, such as `/etc/nginx/includes/tenant`, and include this file into tenant server blocks.
```nginx
server {
include includes/tenant;
server_name foo.bar;
# ssl_certificate /etc/foo/...;
}
```
### Generating certificates
You can generate a certificate using certbot. If you use the `--nginx` flag, you will need to run certbot as root. If you use the `--webroot` flag, you only need the user that runs it to have write access to the webroot directory (or perhaps webroot/.well-known is enough) and some certbot files (you can specify these using --work-dir, --config-dir and --logs-dir).
Creating this config dynamically from PHP is not easy, but is probably feasible. Giving `www-data` write access to `/etc/nginx/sites-available/tenants.conf` should work.
However, you still need to reload nginx configuration to apply the changes to configuration. This is problematic and I'm not sure if there is a simple and secure way to do this from PHP.

View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=getting-started">
<title>stancl/tenancy</title>
</head>
</html>

View file

@ -0,0 +1,77 @@
---
title: Installation
description: Installing stancl/tenancy
extends: _layouts.documentation
section: content
---
# Installation {#getting-started}
Laravel 6.0 or higher is needed.
### Require the package via composer
First you need to require the package using composer:
```
composer require stancl/tenancy
```
### Automatic installation {#automatic-installation}
To install the package, simply run
```
php artisan tenancy:install
```
This will do all the steps listed in the [Manual installation](#manual-installation) section for you.
You will be asked if you want to store your data in a relational database or Redis. Continue to the next page ([Storage Drivers]({{ $page->link('storage-drivers') }})) to know what that means.
### Manual installation {#manual-installation}
If you prefer installing the package manually, you can do that too. It shouldn't take more than a minute either way.
#### Setting up middleware
Now open `app/Http/Kernel.php` and make the package's middleware classes top priority, so that they get executed before anything else, making sure things like the database switch connections soon enough:
```php
protected $middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
// ...
];
```
Add the `\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class` middleware to all route groups you use, so that's probably `'web'` and possibly `'api'`:
```php
protected $middlewareGroups = [
'web' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
// ...
],
// ...
]
```
#### Creating routes
The package lets you have tenant routes and "exempt" routes. Tenant routes are your application's routes. Exempt routes are routes exempt from tenancy — landing pages, sign up forms, and routes for managing tenants.
Routes in `routes/web.php` are exempt, while routes in `routes/tenant.php` have the tenancy middleware automatically applied to them.
So, to create tenant routes, put those routes in a new file called `routes/tenant.php`.
#### Configuration
Run the following:
```
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config
```
This creates a `config/tenancy.php`. You can use it to configure how the package works.
Configuration is explained in detail on the [Configuration]({{ $page->link('configuration') }}) page.

View file

@ -0,0 +1,14 @@
---
title: Integrations
description: Integrating stancl/tenancy
extends: _layouts.documentation
section: content
---
# Integrations {#integrations}
This package naturally integrates well with Laravel packages, since it does not rely on you explicitly specifying database connections.
There are some exceptions, though. [Telescope integration]({{ $page->link('telescope') }}), for example, requires you to change the database connection in `config/telescope.php` to a non-default one, because the default connection is switched to the tenant connection. Some packages should use a central connection for data storage.
> You may be thinking, why does the DB storage driver work with the default, central DB connection, but Telescope doesn't? It's because the DB storage driver intelligently uses the **original** default DB connection, if it has been changed.

View file

@ -0,0 +1,25 @@
---
title: Jobs & Queues
description: Jobs & Queues..
extends: _layouts.documentation
section: content
---
# Jobs & Queues {#jobs-queues}
Jobs are automatically multi-tenant, which means that if a job is dispatched while tenant A is initialized, the job will operate with tenant A's database, cache, filesystem, and Redis.
**However**, if you're using the `database` or `redis` queue driver, you have to make a small tweak to your queue configuration.
Open `config/queue.php` and make sure your queue driver has an explicitly set connection. Otherwise it would use the default one, which would cause issues, since `database.default` is changed by the package and Redis connections are prefixed.
**If you're using `database`, add a new line to `queue.connections.database` and `queue.failed`:**
```php
'connection' => 'mysql',
```
where `'mysql'` is the name of your non-tenant database connection with a `jobs` table.
Also make sure you run the queue migrations **for the central database**, not your tenants.
**If you're using Redis, make sure its `'connection'` is not in `tenancy.redis.prefixed_connections`.**

View file

@ -0,0 +1,21 @@
---
title: Livewire Integration
description: Livewire Integration
extends: _layouts.documentation
section: content
---
# Livewire Integration {#livewire-integration}
Open the `config/livewire.php` file and change this line:
```php
'middleware_group' => ['web'],
```
to this:
```php
'middleware_group' => ['web', 'universal'],
```
Now you can use Livewire both in the central app and the tenant app.

View file

@ -0,0 +1,46 @@
---
title: Middleware Configuration
description: Middleware Configuration..
extends: _layouts.documentation
section: content
---
# Middleware Configuration {#middleware-configuration}
## Header or query parameter based identification {#header-or-query-parameter-based-identification}
To identify tenants using request headers or query parameters, you may use the `InitializeTenancyByRequestData` middleware.
Create a **central** route (don't apply the `tenancy` middleware group on it and don't put it into `routes/tenant.php`) and apply this middleware on the route:
```php
\Stancl\Tenancy\Middleware\InitializeTenancyByRequestData::class
```
To customize the header, query parameter, and `onFail` logic, you may do this in your `AppServiceProvider::boot()`:
```php
// use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData::class;
$this->app->bind(InitializeTenancyByRequestData::class, function ($app) {
return new InitializeTenancyByRequestData('header name', 'query parameter', function ($exception) {
// return redirect()->route('foo');
});
});
```
To disable identification using header or query parameter, set the respective parameter to `null`.
## Customizing the onFail logic {#customizing-the-onfail-logic}
When a tenant route is visited and the tenant can't be identified, an exception is thrown. If you want to change this behavior, to a redirect for example, add this to your `app/Providers/AppServiceProvider.php`'s `boot()` method:
```php
// use Stancl\Tenancy\Middleware\InitializeTenancy;
$this->app->bind(InitializeTenancy::class, function ($app) {
return new InitializeTenancy(function ($exception, $request, $next) {
// return redirect()->route('foo');
});
});
```

View file

@ -0,0 +1,28 @@
---
title: Miscellaneous Tips
description: Miscellaneous Tips..
extends: _layouts.documentation
section: content
---
# Miscellaneous Tips {#misc-tips}
## Custom ID scheme
If you don't want to use UUIDs and want to use something more human-readable (even domain concatenated with uuid, for example), you can create a custom class for this:
```php
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
class MyUniqueIDGenerator implements UniqueIdentifierGenerator
{
public static function generate(array $domains, array $data = []): string
{
return $domains[0] . \Ramsey\Uuid\Uuid::uuid4()->toString();
}
}
```
and then set the `tenancy.unique_id_generator` config to the full path to your class.
Note that you may have to make the `id` column on the `tenants` table larger, as it's set to the exact length of uuids by default.

View file

@ -0,0 +1,36 @@
---
title: Nova Integration
description: Nova Integration
extends: _layouts.documentation
section: content
---
# Nova Integration {#nova-integration}
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.
```none
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('universal-routes') }}) of the app.)
- Add the `'tenancy'` middleware group to your `nova.middleware` config. Example:
```php
'middleware' => [
'tenancy',
'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,41 @@
---
title: Passport Integration
description: Passport Integration
extends: _layouts.documentation
section: content
---
# Passport Integration {#passport-integration}
> 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' => 'tenancy']);
```
- `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/#seed-after-migration') }}).
## 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 [Tenant Storage]({{ $page->link('tenant-storage') }}) 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
- Add these keys to your `tenancy.storage_to_config_map` config:
```php
'storage_to_config_map' => [
'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.

View file

@ -0,0 +1,15 @@
---
title: PostgreSQL schema separation
description: PostgreSQL schema separation
extends: _layouts.documentation
section: content
---
# PostgreSQL schema separation {#postgresql-schema-separation}
If you're using PostgreSQL, you can separate tenant databases by *schemas* instead of *databases*.
To enable this, set the config like this:
- `tenancy.database.separate_by`: `'schema'` to tell the package how we're separating databases
- `tenancy.database_managers.pgsql`: `Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class` to tell the package to use the schema creator/deleter instead of database creator/deleter for pgsql databases.

View file

@ -0,0 +1,36 @@
---
title: Integration with Spatie packages
description: Integration with Spatie packages
extends: _layouts.documentation
section: content
---
# Integration with Spatie packages {#integration-with-spatie-packages}
## laravel-activitylog {#activitylog}
### For the tenant app: {#activitylog-tenant}
- 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: {#activitylog-central}
- Set the `database_connection` key in `config/activitylog.php` to the name of your central database connection.
## laravel-permission {#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
tenancy()->hook('bootstrapped', function (TenantManager $tenantManager) {
\Spatie\Permission\PermissionRegistrar::$cacheKey = 'spatie.permission.cache.tenant.' . $tenantManager->getTenant('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,17 @@
---
title: Stay Updated
description: Stay Updated..
extends: _layouts.documentation
section: content
---
# Stay Updated {#stay-updated}
If you'd like to be notified about new versions, you can [sign up for e-mail notifications](https://stancl.github.io/tenancy/#stay-updated) or join our [Telegram channel](https://t.me/joinchat/AAAAAFjdrbSJg0ZCHTzxLA).
You can choose whether you want to receive emails about major versions and/or minor versions.
- Major versions include breaking changes. Composer won't know about these versions and won't update to them. Major versions will be released about once every 6 months.
- Minor versions include backwards-compatible features and bug fixes.
<!-- todo mailchimp dialog -->

View file

@ -0,0 +1,63 @@
---
title: Storage Drivers
description: Storage Drivers
extends: _layouts.documentation
section: content
---
# Storage Drivers {#storage-drivers}
Storage drivers are used to store a list of all tenants, their domains and any extra information you store about your tenants (their plan, API keys, etc).
Currently, database and Redis storage drivers are available as part of the package. However, you can [write your own]({{ $page->link('writing-storage-drivers') }}) (and contribute ❤️) storage drivers.
## Database {#database}
The database storage driver lets you store tenant information in a relational database like MySQL, PostgreSQL and SQLite.
The benefit of this storage driver is that you don't have to use both Redis and a database for your data. Also you don't have to do as much configuration.
To use this driver, you need to have a `tenants` table and a `domains` table. You may also use a custom database connection. By default, `tenancy.storage_drivers.db.connection` is set to `null`, which means that your app's default database connection will be used to store tenants.
If you wish to use a different connection, e.g. `central`, you may create it in the `config/database.php` file and set `tenancy.storage_drivers.db.connection` to the name of that connection. It's recommended to do this for easier changes in the future.
To create the `tenants` and `domains` tables, you can use the migrations that come with this package. If you haven't published them during installation, publish them now:
```
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=migrations
```
By default, all of your data will be stored in the JSON column `data`. If you want to store some data in a dedicated column (to leverage indexing, for example), add the column to the migration and to the `tenancy.custom_columns` config.
> If you have existing migrations related to your app in `database/migrations`, move them to `database/migrations/tenant`. You can read more about tenant migrations [here]({{ $page->link('tenant-migrations') }}).
Finally, run the migrations:
```
php artisan migrate
```
> If you use a non-default connection, such as `central`, you have to specify which DB to migrate using the `--database` option.
## Redis {#redis}
The Redis storage driver lets you store tenant information in Redis, a high-performance key-value store.
The benefit of this storage driver is its performance.
**Note that you need to configure persistence on your Redis instance if you don't want to lose all information about tenants.**
Read the [Redis documentation page on persistence](https://redis.io/topics/persistence). You should definitely use AOF and if you want to be even more protected from data loss, you can use RDB **in conjunction with AOF**.
If your cache driver is Redis and you don't want to use AOF with it, run two Redis instances. Otherwise, just make sure you use a different database (number) for tenancy and another for anything else.
To use this driver, create a new Redis connection in the `database.redis` configuration (`config/database.php`) called `tenancy` (or if you use another name, be sure to update it in the `tenancy.storage_drivers.redis.connection` configuration (`config/tenancy.php`)).
```php
'tenancy' => [
'host' => env('TENANCY_REDIS_HOST', '127.0.0.1'),
'password' => env('TENANCY_REDIS_PASSWORD', null),
'port' => env('TENANCY_REDIS_PORT', 6380), // different port = separate Redis instance
'database' => env('TENANCY_REDIS_DB', 3), // alternatively, different database number
],
```
> Note: You need phpredis. Predis support will dropped by Laravel in version 7.

View file

@ -0,0 +1,31 @@
---
title: Telescope Integration
description: Telescope Integration..
extends: _layouts.documentation
section: content
---
# Telescope Integration
> To enable this feature, uncomment the `Stancl\Tenancy\Features\TelescopeTags::class` line in your `tenancy.features` config.
Requests in Telescope are automatically tagged with the tenant id and domain:
![Telescope Request with tags](https://i.imgur.com/CEEluYj.png)
This lets you filter requests by id and domain:
![Filtering by id](https://i.imgur.com/SvbOa7S.png)
![Filtering by domain](https://i.imgur.com/dCJuEr1.png)
If you'd like to set Telescope tags in your own code, e.g. in your `AppServiceProvider`, replace your `Telescope::tag()` call like this:
```php
\Tenancy::integrationEvent('telescope', function ($entry) {
return ['abc']; // your logic
});
```
![Tenancy tags merged with tag abc](https://i.imgur.com/4p1wOiM.png)
Once Telescope 3 is released, you won't have to do this.
To have Telescope working, make sure your `telescope.storage.database.connection` points to a non-tenant connection. It's that way by default, so for most projects, Telescope should work out of the box.

View file

@ -0,0 +1,18 @@
---
title: Tenancy Bootstrappers
description: Tenancy Bootstrappers
extends: _layouts.documentation
section: content
---
# Tenancy Bootstrappers {#tenancy-bootstrappers}
These are the classes that do the magic. When tenancy is initialized, TenancyBootstrappers are executed, making Laravel tenant-aware.
All Tenancy Bootstrappers must implement the `Stancl\Tenancy\Contracts\TenancyBootstrapper` interface.
When tenancy is [initialized]({{ $page->link('tenancy-initialization') }}), the `start()` method on the [enabled bootstrappers]({{ $page->link('configuration#bootstrappers') }}) is called.
Conversely, when tenancy is ended, the `end()` method is called.
In the [`tenancy.bootstrappers` configuration]( {{ $page->link('configuration#bootstrappers') }} ), bootstrappers have an alias configured (e.g. `database`) that is used by [events]({{ $page->link('hooks') }}) to say which bootstrappers are prevented.

View file

@ -0,0 +1,21 @@
---
title: Tenancy Initialization
description: Tenancy Initialization..
extends: _layouts.documentation
section: content
---
# Tenancy Initialization {#tenancy-initialization}
Tenancy can be initialized using the following methods on `Stancl\Tenancy\TenantManager`:
- `initializeTenancy($tenant)`
- `initialize($tenant)`
- `init($domain)`
Similarly, tenancy can be ended using:
- `endTenancy()`
- `end()`
You can use these methods in `php artisan tinker`.
[Tenant Routes]({{ $page->link('tenant-routes') }}) have the `InitializeTenancy` middleware applied to them. That middleware automatically initializes tenancy for the current hostname.

View file

@ -0,0 +1,61 @@
---
title: Tenant-Aware Commands
description: Tenant-Aware Commands
extends: _layouts.documentation
section: content
---
# Tenant-Aware Commands {#tenant-aware-commands}
Even though [`tenants:run`]({{ $page->link('console-commands#run') }}) lets you run arbitrary artisan commands for tenants, you may want to have strictly tenant commands.
To make a command tenant-aware, utilize the `TenantAwareCommand` trait:
```php
class MyCommand extends Command
{
use TenantAwareCommand;
}
```
However, this trait requires you to implement a `getTenants()` method that returns an array of `Tenant` instances.
If you don't want to implement the options/arguments yourself, you may use one of these two traits:
- `HasATenantsOption` - accepts multiple tenant ids, optional -- by default the command is executed for all tenants
- `HasATenantArgument` - accepts a single tenant id, required argument
These traits implement the `getTenants()` method needed by `TenantAwareCommand`.
> Note: If you're using a custom constructor for your command, you need to add `$this->specifyParameters()` at the end for the option/argument traits to take effect.
So if you use these traits in combination with `TenantAwareCommand`, you won't have to change a thing in your command:
```php
class FooCommand extends Command
{
use TenantAwareCommand, HasATenantsOption;
public function handle()
{
//
}
}
class BarCommand extends Command
{
use TenantAwareCommand, HasATenantArgument;
public function handle()
{
//
}
}
```
### Custom implementation
If you want more control, you may implement this functionality yourself by simply accepting a `tenant_id` argument and then inside `handle()` doing something like this:
```php
tenancy()->find($this->argument('tenant_id'))->run(function () {
// your actual command code
});
```

View file

@ -0,0 +1,140 @@
---
title: Tenant Manager
description: Tenant Manager
extends: _layouts.documentation
section: content
---
# Tenant Manager {#tenant-manager}
This page documents a couple of `TenantManager` methods you may find useful.
To call methods on `TenantManager`, you may use the `tenancy()` helper or the `Tenancy` facade.
### Find tenant by id {#find-by-id}
```php
>>> \Tenancy::find('b07aa3b0-dc68-11e9-9352-9159b2055c42')
=> Stancl\Tenancy\Tenant {#3099
+data: [
"id" => "b07aa3b0-dc68-11e9-9352-9159b2055c42",
"plan" => "free",
],
+domains: [
"foo.localhost",
],
}
```
### Find tenant by domain {#find-by-domain}
```php
>>> tenancy()->findByDomain('bar.localhost')
=> Stancl\Tenancy\Tenant {#3091
+data: [
"id" => "b38b2bd0-dc68-11e9-adfc-ede94ab3b264",
],
+domains: [
"bar.localhost",
],
}
```
### Find tenant by arbitrary key {#find-by-arbitrary-key}
> Note: Only the DB storage driver implements this feature.
```php
tenancy()->findBy('email', $email);
tenancy()->findByEmail($email);
```
### Getting the current tenant
One more way to get the current [tenant]({{ $page->link('tenants') }}) is to call `getTenant()` on `TenantManager`:
```php
tenancy()->getTenant()
```
If you want to get the value of a specific key from the array, you can an argument with the key.
```php
tenancy()->getTenant('id') // Does the same thing as tenant('id')
```
### Getting all tenants
This method returns a collection of arrays.
```php
>>> tenancy()->all()
=> Illuminate\Support\Collection {#3080
all: [
Stancl\Tenancy\Tenant {#3076
+data: [
"id" => "b07aa3b0-dc68-11e9-9352-9159b2055c42",
],
+domains: [
"foo.localhost",
],
},
Stancl\Tenancy\Tenant {#3075
+data: [
"id" => "b38b2bd0-dc68-11e9-adfc-ede94ab3b264",
],
+domains: [
"bar.localhost",
],
},
],
}
>>> tenancy()->all()->pluck('domains')
=> Illuminate\Support\Collection {#3108
all: [
[
"foo.localhost",
],
[
"bar.localhost",
],
],
}
```
### Deleting a tenant {#deleting-a-tenant}
```php
>>> $tenant = tenancy()->findByDomain('foo.localhost');
=> Stancl\Tenancy\Tenant {#3119
+data: [
"id" => "b07aa3b0-dc68-11e9-9352-9159b2055c42",
"plan" => "free",
],
+domains: [
"foo.localhost",
],
}
>>> $tenant->delete();
=> true
```
NOTE: This doesn't delete the tenant's database.
If you want to delete it, get the database name prior to deleting the tenant using `getDatabaseName()`.
```php
>>> $tenant->getDatabaseName()
=> "tenant67412a60-1c01-11e9-a9e9-f799baa56fd9"
```
If you want tenant databases to be deleted automatically, you may use the [`delete_database_after_tenant_deletion` configuration]({{ $page->link('configuration#delete-database-after-tenant-deletion') }})
### Soft deleting a tenant {#soft-deleting-a-tenant}
You may also "soft delete" tenants. The `softDelete()` method detaches all domains from a tenant:
```php
$tenant->softDelete();
```
The list of original domains will be accessible under the `_tenancy_original_domains` key in the tenant storage.

View file

@ -0,0 +1,30 @@
---
title: Tenant Migrations
description: Tenant Migrations
extends: _layouts.documentation
section: content
---
# Tenant Migrations {#tenant-migrations}
You can run tenant migrations using the `php artisan tenants:migrate` command.
You may specify the tenant(s) using the `--tenants` option.
```
php artisan tenants:migrate --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
> Note: Tenant migrations must be located in `database/migrations/tenant`.
You can run migrations from outside the command line as well. To run migrations for a tenant in your code, use `Artisan::call()`:
```php
$tenant = \Tenant::create('tenant1.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant->id];
]);
```
You may also [configure]({{ $page->link('configuration#migrate-after-creation') }}) the package to run migrations automatically, after creating tenants.

View file

@ -0,0 +1,99 @@
---
title: Tenant Routes
description: Tenant Routes
extends: _layouts.documentation
section: content
---
# Tenant Routes {#tenant-routes}
Routes within `routes/tenant.php` will have the `web` and `tenancy` middleware groups automatically applied on them.
Just like `routes/web.php`, these routes use the `App\Http\Controllers` namespace (you can [configure this]({{ $page->link('configuration#tenant-route-namespace') }}))
> If a tenant cannot be identified, an exception will be thrown. If you want to change this behavior (to a redirect, for example) read the [Middleware Configuration]({{ $page->link('middleware-configuration') }}) page.
## Middleware {#middleware}
The package automatically adds the `InitializeTenancy` middleware to the global middleware stack. This middleware checks if the current domain is not part of `tenancy.exempt_domains`. If not, it attempts to identify the tenant based on the current hostname. Once the tenant is identified, the database connection, cache, filesystem root paths and, optionally, Redis connection, will be switched.
After the *global* middleware is executed, the controllers are constructed.
After that, the *route* middleware is executed.
All route groups in your application should have the `\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains` middleware applied on them, to prevent access from tenant domains to central routes and vice versa. See below for more detail about the `PreventAccessFromTenantDomains` middleware.
All tenant routes in your application should have the `tenancy` middleware group applied on them.
The `tenancy` middleware group marks the route as a tenant route. That middleware functions as a "flag" for the `PreventAccessFromTenantDomains`, telling it that the route is a tenant route, since the middleware has no other way of distingushing central from tenant routes.
In previous versions, the `InitializeTenancy` middleware was applied only on tenant routes. However, that lead to tenancy not being initialized in controller constructors, which could cause bugs. So from 2.1.0 on, tenancy is initialized on all routes on non-exempt domains, and if the route is not tenant, the request gets aborted by the `PreventAccessFromTenantDomains` once Laravel reaches the route middleware step.
## Central routes {#central-routes}
Routes in files other than `routes/tenant.php` will not have the `tenancy` middleware automatically applied on them, so they will be central routes. If you want these routes to be tenant routes, you can apply the `tenancy` middleware manually, as described in custom route groups below.
## API routes / custom route groups {#custom-groups}
If you want certain routes (perhaps API routes) to be multi-tenant, wrap them in a Route group with this middleware:
```php
Route::middleware('tenancy')->group(function () {
// Route::get('/', 'HelloWorld');
});
```
and make sure the `Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains` middleware is applied on the *entire* group:
```php
// app/Http/Kernel.php
protected $middlewareGroups = [
// ...
'api' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
// ...
]
];
```
## The `PreventAccess...` middleware {#prevent-access-middleware}
The `Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains` middleware prevents access to non-tenant routes from tenant domains by returning a redirect to the tenant app's home page ([`tenancy.home_url`]({{ $page->link('configuration#home-url') }})). Conversely, it returns a 404 when a user attempts to visit a tenant route on a web (exempt) domain.
The `tenancy:install` command applies this middleware to the `web` and `api` groups. To apply it for another route group, add this middleware manually to that group. You can do this in `app/Http/Kernel.php`.
## Conflicting routes {#conflicting-routes}
By default, you cannot have conflicting routes in `web.php` and `tenant.php`. It would break `php artisan route:list` and route caching.
**However**, tenant routes are loaded after the web/api routes, so if you register your central routes only for domains listed in the `tenancy.exempt_domains` config, you **can use the same URLs for central and tenant routes**.
Here's an example implementation:
```php
// RouteServiceProvider
protected function mapWebRoutes()
{
foreach (config('tenancy.exempt_domains', []) as $domain) {
Route::middleware('web')
->domain($domain)
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
}
protected function mapApiRoutes()
{
foreach (config('tenancy.exempt_domains', []) as $domain) {
Route::prefix('api')
->middleware('api')
->domain($domain)
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
}
```
One thing to keep in mind though: If you use multiple exempt domains, you cannot use route names. They can be used only once, so the name would link to the URL on the first exempt domain.
If you don't need conflicting routes, you may want to do the following: Since you probably want cleaner URLs on your non-tenant part of the application (landing page, etc), prefix your tenant routes with something like `/app`.

View file

@ -0,0 +1,55 @@
---
title: Tenant Storage
description: Tenant storage..
extends: _layouts.documentation
section: content
---
# Tenant Storage {#tenant-storage}
Tenant storage is where tenants' ids and domains are stored. You can store things like the tenant's plan, subscription information, and tenant-specific application configuration in tenant storage. You may use these functions on `Tenant` objects:
```php
get (string|array $key)
put (string|array $key, mixed $value = null) // if $key is array, make sure $value is null
```
To put something into the tenant storage, you can use `put()` or `set()`.
```php
$tenant->put($key, $value);
$tenant->set($key, $value); // alias for put()
$tenant->put(['key1' => 'value1', 'key2' => 'value2']);
```
> **Note:** Don't start any keys with `_tenancy` unless instructed to by the docs. It is a reserved namespace used to store internal data by this package.
To get something from the storage, you can use `get()`:
```php
tenant()->get($key);
tenant()->get(['key1', 'key2']);
```
> In this example, we're calling the methods on the current tenant &mdash; `tenant()`.
> Note: `get(['key1', 'key2'])` returns an associative array.
Note that $key has to be a string or an array with string keys. The value(s) can be of any data type. Example with arrays:
```php
>>> tenant()->put('foo', ['a' => 'b', 'c' => 'd']);
=> [ // put() returns the supplied value(s)
"a" => "b",
"c" => "d",
]
>>> tenant()->get('foo');
=> [
"a" => "b",
"c" => "d",
]
```
To delete keys from the storage, you may use these methods:
```php
$tenant->deleteKey($key)
$tenant->deleteKeys(['foo', 'bar']);
```

View file

@ -0,0 +1,128 @@
---
title: Tenants
description: Tenants
extends: _layouts.documentation
section: content
---
# Tenants {#tenants}
This page covers the `Stancl\Tenancy\Tenant` object. Both [creating tenants]({{ $page->link('creating-tenants') }}) and interacting with the [tenant storage]({{ $page->link('tenant-storage') }}) are covered on separate pages. This page focuses on advanced usage, which can help you with writing nicer code.
## `$data` {#data}
An associative array that mirrors the tenant's data in the actual storage. It acts as a "cache" for [tenant storage methods]({{ $page->link('tenant-storage') }}). When you call `$tenant->get('foo')`, the `$data` property is checked for `'foo'` before trying to read from the storage. Similarly, when you call `$tenant->put()`, you write both to the actual storage and to the `$data` array.
If you try to access the tenant's properties using `$tenant->foo` or `$tenant['foo']`, those calls are redirected to the `$data` array.
The `put()` call always writes to the actual storage. If you do just:
```php
$tenant->foo = 'bar';
```
The data will not be persisted until you `->save()` the `$tenant`.
## `$domains` {#domains}
An array of domains that belong to the tenant.
You may add and remove domains using the following methods:
```php
$tenant->addDomains(['foo.yourapp.com'])->save();
$tenant->addDomains('foo.yourapp.com')->save();
$tenant->removeDomains(['foo.yourapp.com'])->save();
$tenant->removeDomains('foo.yourapp.com')->save();
```
> Don't forget to `->save()` after modifying the domains!
## `run()` {#run}
The `$tenant->run()` command lets you execute a closure inside a tenant's "environment".
```php
$tenant->run(function ($tenant) {
User::create(['name' => 'Admin', 'email' => 'admin@yourapp.com', ...]);
});
```
It also lets you get data from the tenant's environment:
```php
$tenantsUserCount = $tenant->run(function ($tenant) {
return User::count();
});
```
If you need access to the tenant within the closure, it's passed as the first argument.
This feature is a safe alternative to:
```php
tenancy()->initialize($tenant);
// make some changes
tenancy()->end();
```
and it also checks if tenancy was initialized. If it was, it returns to the original tenant after running the closure.
## `$persisted` {#persisted}
This property says whether the model has saved to the storage yet. In other words, if it's `false`, it's a new instance that has not been `->save()`d yet.
## `->save()` {#save}
If no ID is set in the tenant's `$data`, it will be generated using the `UniqueIDGenerator` specified in `config('tenancy.unique_id_generator')`.
Then, if the object has been persisted, it's updated in storage. Otherwise, it's written to storage.
## `->with()` {#with}
You may fluently change the tenant's `$data` using `->with()`:
```php
$tenant->with('foo', 'bar'); // equivalent to $tenant->foo = $bar
```
Don't forget to `->save()` when you use `->with()`:
```php
$tenant->with('foo', 'bar')->with('abc', 'xyz')->save();
```
You may also use `->with<PropertyNameInPascalCase>` and it will be stored in snake case:
```php
>>> \Tenancy::find('b07aa3b0-dc68-11e9-9352-9159b2055c42')
=> Stancl\Tenancy\Tenant {#3060
+data: [
"id" => "b07aa3b0-dc68-11e9-9352-9159b2055c42",
],
+domains: [
"foo.localhost",
],
}
>>> \Tenancy::find('b07aa3b0-dc68-11e9-9352-9159b2055c42')
->withPaypalApiKey('foobar')
->save();
=> Stancl\Tenancy\Tenant {#3087
+data: [
"id" => "b07aa3b0-dc68-11e9-9352-9159b2055c42",
"paypal_api_key" => "foobar",
],
+domains: [
"foo.localhost",
],
}
```
These methods make the most sense (= sound the best) during tenant creation:
```php
// $domains = ['foo.yourapp.com', 'foo.com'];
$primary_domain = $domains[0];
$id = $primary_domain . $this->randomString(24);
Tenant::new()
->withDomains($domains)
->withPaypalApiKey('defaultApiKey');
->withId($id)
->save();
```

View file

@ -0,0 +1,12 @@
---
title: Universal Routes
description: Universal Routes
extends: _layouts.documentation
section: content
---
# Universal Routes {#universal-routes}
You may want to use some routes, such as `Auth::routes()`, in both your tenant app and your central app. To do this, use the `universal` middleware group on the route(s).
Another example of when you might want to do this is if you want to use [Laravel Nova in the tenant app]({{ $page->link('nova') }}) as well as in the central app.

View file

@ -0,0 +1,56 @@
---
title: Upgrading
description: Upgrading
extends: _layouts.documentation
section: content
---
# Upgrading from 1.x {#upgrading}
The 2.0.0 release is essentialy a ~60% rewrite, with 3,187 additions and 1,896 deletions (lines of code). Version 2 introduces Laravel 6 support and drops Laravel 5.8 support.
This guide attempts to cover the main changes that were made to the package. The rewrite was mainly:
- an internal thing: much better code quality means much better maintainability and much more features in the future :)
- to provide a nicer API for working with tenants
Even though new syntax was one of the main goals, the rewrite was made with backwards compatibility in mind, so many old methods still work.
If you're coming from 1.x, it's recommended to read (or at least skim through) the entire documentation again.
## Main changes
- `Tenant` objects are now used, instead of arrays, to represent tenants. See the [Tenants]({{ $page->link('tenants') }}) page.
- Tenants can now have multiple domains, so a new `domains` table is used by the DB storage driver.
- The `uuid` property on tenants is now `id`.
- `tenancy()` helper now returns an instance of `TenantManager` while the `tenant()` helper returns an instance of the current `Tenant`. If no `Tenant` has been identified, `null` is returned. Same with the `Tenancy` and `Tenant` facades.
- Event listeners/hooks have a new syntax: `Tenancy::eventListener('bootstrapping', function () {})`
- The tenancy bootstrapping logic has been extracted into separate classes, such as `DatabaseTenancyBootstrapper`, `CacheTenancyBootstrapper` etc.
- A concept of `Feature`s was introduced. They're classes that provide additional functionality - functionality that is not necessary to bootstrap tenancy.
- predis support was dropped. Laravel will drop predis support in 7.x.
- There is new syntax for [creating]({{ $page->link('creating-tenants') }}) and [interacting]({{ $page->link('tenants') }}) with tenants, be sure to read those documentation pages again.
- The `_tenancy` namespace for keys in tenant storage is reserved by the package and should not be used unless instructed to by the documentation.
- The config was changed *a lot*, so you should publish and configure it again.
You can publish the configuration like this:
```none
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config
```
## DB storage driver
- The `uuid` column in the `tenants` table was renamed to `id`. The `domain` column was dropped.
- A new migration was added to create the `domains` table. **The old migration was renamed**, so if you publish migrations again, be sure to delete the old migration, to avoid creating the table twice.
You can publish migrations like this:
```none
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=migrations
```
## Redis storage driver
- The `uuid` keys are now `id`.
- The `domain` key was dropped.
- The `_tenancy_domains` key is used to store an array of domains that belong to the tenant.
## New Features
- [Tenant Config]({{ $page->link('features/tenant-config') }})
- [Migrate Fresh]({{ $page->link('console-commands#migrate-fresh') }})
- [`tenants:create`]({{ $page->link('console-commands#create-tenant') }})

View file

@ -0,0 +1,16 @@
---
title: Usage
description: Usage..
extends: _layouts.documentation
section: content
---
# Usage {#usage}
This chapter describes usage of the package. That includes creating tenants, deleting tenants, storing data in the tenant storage.
The package comes with two helpers - `tenancy()` and `tenant()`.
- `tenancy()` returns an instance of [`TenantManager`]({{ $page->link('tenant-manager') }})
- `tenant()` returns an instance of the current tenant, or null if no tenant hs been identified yet. You can pass an argument to this helper, to get a value from the tenant storage: `tenant('plan')` is identical to [`tenant()->get('plan')`]({{ $page->link('tenant-storage') }}).
The package also comes with two facades. `Tenancy` -- for `TenantManager` -- and `Tenant` -- for the current `Tenant`.

View file

@ -0,0 +1,86 @@
---
title: Writing Storage Drivers
description: Writing Storage Drivers
extends: _layouts.documentation
section: content
---
# Writing Storage Drivers
If you don't want to use the provided DB/Redis storage drivers, you can write your own driver.
To create a driver, create a class that implements the `Stancl\Tenancy\Contracts\StorageDriver` interface.
Here's an example:
```php
<?php
namespace App\StorageDrivers\MongoDBStorageDriver;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Contracts\StorageDriver;
class MongoDBStorageDriver implements StorageDriver
{
public function createTenant(Tenant $tenant): void
{
//
}
public function updateTenant(Tenant $tenant): void
{
//
}
public function deleteTenant(Tenant $tenant): void
{
//
}
public function findById(string $id): Tenant
{
//
}
public function findByDomain(string $domain): Tenant
{
//
}
public function all(array $ids = []): array
{
//
}
public function ensureTenantCanBeCreated(Tenant $tenant): void
{
//
}
public function withDefaultTenant(Tenant $tenant)
{
//
}
public function get(string $key, Tenant $tenant = null)
{
//
}
public function getMany(array $keys, Tenant $tenant = null)
{
//
}
public function put(string $key, $value, Tenant $tenant = null): void
{
//
}
public function putMany(array $kvPairs, Tenant $tenant = null): void
{
//
}
}
```