Merge branch 'master' into patch-1

This commit is contained in:
lukinovec 2022-06-02 11:50:07 +02:00 committed by GitHub
commit ee6025e0f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1479 additions and 181421 deletions

View file

@ -36,6 +36,7 @@ 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`
3. update the `domains` table migration `tenant_id` column to the same type as `tenants` id
### Domain model {#domain-model}
@ -80,7 +81,7 @@ See this section in the config, it's documented with comments.
`tenancy.filesystem.*`
This section is relevant to cache separation, specifically, to the `FilesystemTenancyBootstrapper`.
This section is relevant to storage separation, specifically, to the `FilesystemTenancyBootstrapper`.
See this section in the config, it's documented with comments.

View file

@ -51,6 +51,8 @@ Note that you don't want to grant the users the ability to grant themselves more
## Specifying template connections {#specifying-the-template-connections}
> **Important:** there should be no `tenant` connection in `config/database.php`. If you create a template connection for tenants, name it something like `tenant_template`. The `tenant` connection is entirely managed by the package and gets reset to `null` when tenancy is ended.
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 {#specifyng-other-connection-details}
@ -62,4 +64,4 @@ You may also set specific connection details without necessarily creating a new
- 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.
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

@ -16,7 +16,9 @@ $tenant->domains()->create([
]);
```
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.
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, you should use both `acme` as a subdomain and `acme.com` for the domain.
Note that starting with Laravel 8 and up, Laravel TrustHost middleware is enabled by default (see [laravel/laravel#5477](https://github.com/laravel/laravel/pull/5477)). This blocks Domain-based tenant identification since these requests will be treated as 'untrusted' unless added as a trusted host. You can either comment out this middleware in your App\Http\Kernel.php, or you can add the custom tenant domains in the App\Http\Middleware\TrustHosts.php `hosts()` method.
To retrieve the current domain model (when using domain identification), you may access the `$currentDomain` public static property on `DomainTenantResolver`.
@ -24,4 +26,4 @@ To retrieve the current domain model (when using domain identification), you may
For local development, you may use `*.localhost` domains (like `foo.localhost`) for tenants. On many operating systems, these work the same way as `localhost`.
If you're using Valet, you may want to use e.g. `saas.test` for the central domain and `foo.saas.test`, `bar.saas.test` etc for tenant domains.
If you're using Valet, you may want to use e.g. `saas.test` for the central domain and `foo.saas.test`, `bar.saas.test` etc for tenant domains. Alternatively, if you want to use **multiple second-level domains**, you can use the `valet link` command to attach additional domains to the project. For example: `valet link bar.test`

View file

@ -12,4 +12,10 @@ Sometimes you may want to redirect the user to a specific route on a different d
```php
return redirect()->route('home')->domain($domain);
```
```
You can also use the `tenant_route()` helper to redirect users to another domain.
```php
return redirect(tenant_route($domain, 'home'));
```

View file

@ -23,14 +23,14 @@ Uncomment the following line in your `tenancy.features` config:
## **Configuring the mappings** {#configuring-the-mappings}
This feature maps keys in the tenant storage to config keys based on the `$storageToConfigMap` public property.
This feature maps keys in the tenant storage to config keys based on the `$storageToConfigMap` [static property]({{ $page->link('configuration#static-properties') }}).
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.
@ -45,5 +45,5 @@ Sometimes you may want to copy the value to multiple config keys. To do that, sp
'app.locale',
'locales.default',
],
],
];
```

View file

@ -6,6 +6,8 @@ section: content
# Installation {#installation}
Laravel 6.0 or higher is needed.
Require the package using composer:
```php
@ -39,4 +41,4 @@ 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.
And finally, if you want to use a different central database than the one defined by `DB_CONNECTION` in the file `.env`, 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

@ -22,6 +22,8 @@ to this:
],
```
(Don't forget to import the middleware class.)
Now you can use Livewire both in the central app and the tenant app.
Also make sure to enable [universal routes]({{ $page->link('features/universal-routes') }}).

View file

@ -24,6 +24,7 @@ To use Nova inside of the tenant part of your application, do the following:
- 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

View file

@ -0,0 +1,54 @@
---
title: Laravel Orchid integration
extends: _layouts.documentation
section: content
---
# Laravel Orchid {#laravel-orchid}
The instruction is written for a newly installed Orchid platform, it is proposed to use the platform as a central application and the tenant app.
## Both in the central app and the tenant app
Laravel Orchid has already been installed according to the [documentation Orchid](https://orchid.software/en/docs/installation/). All the steps have also been completed [Quickstart Tutorial](/docs/v3/quickstart).
- To use Orchid both in the central & tenant parts you need to enable [Universal Routes]({{ $page->link('features/universal-routes') }}).
- Add the tenancy middleware to your `config\platform.php`
```php
'middleware' => [
'public' => ['web', 'universal', \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class],
'private' => ['web', 'platform', 'universal', \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class],
],
```
- Add a route to `routes\platform.php`
```php
Route::screen('/', PlatformScreen::class)
->name('platform.index')
->breadcrumbs(function (Trail $trail) {
return $trail->push(__('Home'), route('platform.index'));
});
```
- In the file `app\Providers\RouteServiceProvider.php`, if necessary, change the path to the "home" route for your central application. By default for Orchid it will be `'admin/main'`
```php
public const HOME = 'admin/main';
```
- Tenant Routes `routes\tenant.php`
```php
Route::middleware([
'web',
'platform',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->prefix(Orchid\Platform\Dashboard::prefix('/'))->group(function () {
Route::get('/', function () {
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});
});
```

View file

@ -22,14 +22,50 @@ To use Passport inside the tenant part of your application, you may do the follo
PreventAccessFromCentralDomains::class,
]]);
```
- Add this to `boot` method in your `AppServiceProvider`:
```php
Passport::loadKeysFrom(base_path(config('passport.key_path')));
```
- `php artisan vendor:publish --tag=passport-migrations` & move to `database/migrations/tenant/` directory
- Create `passport.php` file in your config directory and add database connection and key path config. This makes passport use the default connection.
```php
<?php
return [
'storage' => [
'database' => [
'connection' => null,
],
],
'key_path' => env('OAUTH_KEY_PATH', 'storage')
];
```
You may set the OAUTH_KEY_PATH in your .env, but by default `passport:keys` puts them in `storage/` 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') }}).
- 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') }}), like this:
```php
public function run()
{
$client = new ClientRepository();
$client->createPasswordGrantClient(null, 'Default password grant client', 'http://your.redirect.path');
$client->createPersonalAccessClient(null, 'Default personal access client', 'http://your.redirect.path');
}
```
## **Tenant-specific keys** {#tenant-specific-keys}
@ -51,7 +87,25 @@ And again, you need to create clients in your tenant database seeding process.
## Using Passport in both the central & tenant app {#using-passport-in-both-the-central-and-tenant-app}
![Passport for both central & tenant app](/assets/images/passport_universal.png)
To use Passport on central and tenant application, you may apply the following changes.
- Remove this from the `register` method in your `AppServiceProvider` if you added it previously:
```php
Passport::ignoreMigrations();
```
- Configure `Passport routes` on the `register` method in your `AppServiceProvider` as follows:
```php
Passport::routes(null, ['middleware' => [
'universal',
InitializeTenancyByDomain::class
]]);
```
- Make a copy of `Passport migrations` to `database/migrations/tenant/` directory
And make sure you enable the *Universal Routes* feature.

View file

@ -12,7 +12,7 @@ Multi-tenancy is the ability to provide your service to multiple users (tenants)
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.
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 form 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.

View file

@ -10,8 +10,6 @@ If you're using the `QueueTenancyBootstrapper`, queued jobs dispatched from the
**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 {#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:
@ -35,11 +33,11 @@ To dispatch a job such that it will run centrally under all circumstances, creat
```jsx
// queue.connections
'central' => [
'driver' => 'database',
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'central' => true, // <---
'central' => true, // <---
],
```
@ -47,4 +45,4 @@ And use this connection like this:
```jsx
dispatch(new SomeJob(...))->onConnection('central');
```
```

View file

@ -48,12 +48,12 @@ App\Providers\TenancyServiceProvider::class, // <-- here
## Creating a tenant model {#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:
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 the file `app/Models/Tenant.php` like this:
```php
<?php
namespace App;
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
@ -66,12 +66,12 @@ class Tenant extends BaseTenant implements TenantWithDatabase
}
```
Now we need to tell the package to use this custom model:
*Please note: if you have the models anywhere else, you should adjust the code and commands of this tutorial accordingly.*
Now we need to tell the package to use this custom model. Open the `config/tenancy.php` file and modify the line below:
```php
// config/tenancy.php
'tenant_model' => \App\Tenant::class,
'tenant_model' => \App\Models\Tenant::class,
```
## Events {#events}
@ -112,7 +112,7 @@ protected function centralDomains(): array
}
```
If you're using Laravel 8, call these methods manually from your `RouteServiceProvider`'s `boot()` method, instead of the `$this->routes()` calls.
Call these methods manually from your `RouteServiceProvider`'s `boot()` method, instead of the `$this->routes()` calls.
```php
public function boot()
@ -134,6 +134,15 @@ Now we need to actually specify the central domains. A central domain is a domai
],
```
If you're using Laravel Sail, no changes are needed, default values are good to go:
```php
'central_domains' => [
'127.0.0.1',
'localhost',
],
```
## Tenant routes {#tenant-routes}
Your tenant routes will look like this by default:
@ -152,18 +161,18 @@ Route::middleware([
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.
Let's make a small change to dump all the users in the database, so that we can actually see multi-tenancy working. Open the file `routes/tenant.php` and apply the modification below:
```php
Route::get('/', function () {
dd(\App\User::all());
dd(\App\Models\User::all());
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});
```
## Migrations {#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.
To have users in tenant databases, let's move the `users` table migration (the file `database/migrations/2014_10_12_000000_create_users_table.php` or similar) 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 {#creating-tenants}
@ -171,23 +180,15 @@ For testing purposes, we'll create a tenant in `tinker` — no need to waste tim
```php
$ php artisan tinker
>>> $tenant1 = Tenant::create(['id' => 'foo']);
>>> $tenant1 = App\Models\Tenant::create(['id' => 'foo']);
>>> $tenant1->domains()->create(['domain' => 'foo.localhost']);
>>>
>>> $tenant2 = Tenant::create(['id' => 'bar']);
>>> $tenant2 = App\Models\Tenant::create(['id' => 'bar']);
>>> $tenant2->domains()->create(['domain' => 'bar.localhost']);
```
Now we'll create a user inside each tenant's database:
```php
App\Tenant::all()->runForEach(function () {
factory(App\User::class)->create();
});
```
If you use Laravel 8, the the command is slightly different:
```php
App\Models\Tenant::all()->runForEach(function () {
App\Models\User::factory()->create();
@ -196,4 +197,4 @@ App\Models\Tenant::all()->runForEach(function () {
## Trying it out {#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.
Now we visit `foo.localhost` in our browser, replace `localhost` with one of the values of `central_domains` in the file `config/tenancy.php` as modified previously. 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

@ -26,6 +26,8 @@ You will need two models for the resource. One for the tenant database and one f
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`.
It is important to note that when a model is updated or created as a result of being synchronised, that model is called with `withoutEvents` and as such if you rely on the saving or creating events you will need to implement this in some other way.
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.
@ -213,10 +215,12 @@ Attaching a tenant to a user will copy even the unsynced columns (they act as de
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.
Also note that if you create a user in the tenant's database, the global id will be created using the ID generator. If you disable the ID generator for [incrementing tenant ids]({{ $page->link('tenants') }}#incrementing-ids), you'll need to make some changes.
## Queueing {#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

@ -33,9 +33,23 @@ Note that you must use a cache store that supports tagging, e.g. Redis.
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
- Suffixes `storage_path()` (useful if you're using the local disk for storing tenant files)
- Makes `asset()` calls use the TenantAssetController to retrieve tenant-specific files
- 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()`.
- Because the `asset()` helper uses the TenantAssetController to retrieve tenant-specific files. Be aware this requires the tenant to be identified and initialized to function. Accordingly, you should ensure that the tenant identification middleware used is appropriate for your use case (defaults to InitializeTenancyByDomain). For example, if you are only using subdomain identification, you should update the middleware used by the TenantAssetController to the InitializeTenancyBySubdomain middleware. To do this, you can follow the steps outlined in [configuration]({{ $page->link('configuration') }}) to update the public static property which defined the middleware used, e.g. in your TenancyServiceProvider:
```php
use Stancl\Tenancy\Controllers\TenantAssetsController;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
// other methods in the TenancyServiceProvider
public function boot()
{
// update the middleware used by the asset controller
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomainOrSubdomain::class;
}
```
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/)

View file

@ -24,6 +24,11 @@ This will let you use the following method on each tenant object:
$tenant->putDownForMaintenance();
```
To remove specific tenant from maintainance mode:
```php
$tenant->update(['maintenance_mode' => null]);
```
## Middleware {#middleware}
You will also need to use the `Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode` middleware on your tenant routes.