mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 05:14:05 +00:00
Initial commit
This commit is contained in:
commit
deb3ad77f5
19 changed files with 1283 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
composer.lock
|
||||
vendor/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Samuel Štancl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
343
README.md
Normal file
343
README.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# Tenancy
|
||||
|
||||
### *A Laravel multi-database tenancy implementation that respects your code.*
|
||||
|
||||
You won't have to change a thing in your application's code.
|
||||
|
||||
- :white_check_mark: No model traits to change database connection
|
||||
- :white_check_mark: No replacing of Laravel classes (`Cache`, `Storage`, ...) with tenancy-aware classes
|
||||
- :white_check_mark: Built-in tenant identification based on hostname
|
||||
|
||||
## Installation
|
||||
|
||||
### Installing the package
|
||||
|
||||
```
|
||||
composer require stancl/tenancy
|
||||
```
|
||||
|
||||
### Adding the `InitializeTenancy` middleware
|
||||
|
||||
Open `app/Http/Kernel.php` and make the following changes:
|
||||
|
||||
First, you want to create middleware groups so that we can apply this middleware on routes.
|
||||
- Create a new middleware group in `$middlewareGroups`:
|
||||
```php
|
||||
'tenancy' => [
|
||||
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
|
||||
],
|
||||
```
|
||||
- Create a new middleware group in `$routeMiddleware`:
|
||||
```php
|
||||
'tenancy' => \Stancl\Tenancy\Middleware\InitializeTenancy::class,
|
||||
```
|
||||
- Make the middleware top priority, so that it gets executed before anything else, thus making sure things like the database switch connections soon enough.
|
||||
```php
|
||||
protected $middlewarePriority = [
|
||||
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
|
||||
```
|
||||
|
||||
#### Configuring the middleware
|
||||
|
||||
When a tenant route is visited, but the tenant can't be identified, an exception can be 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) {
|
||||
// redirect
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Creating tenant routes
|
||||
|
||||
Add this method into `app/Providers/RouteServiceProvider.php`:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Define the "tenant" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapTenantRoutes()
|
||||
{
|
||||
Route::middleware(['web', 'tenancy'])
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/tenant.php'));
|
||||
}
|
||||
```
|
||||
|
||||
And add this line to `map()`:
|
||||
|
||||
```php
|
||||
$this->mapTenantRoutes();
|
||||
```
|
||||
|
||||
Now rename the `routes/web.php` file to `routes/tenant.php`. This file will contain routes accessible only with tenancy.
|
||||
|
||||
Create an empty `routes/web.php` file. This file will contain routes accessible without tenancy (such as the landing page.)
|
||||
|
||||
### Publishing the configuration file
|
||||
|
||||
```
|
||||
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config
|
||||
```
|
||||
|
||||
You should see something along the lines of `Copied File [...] to [/config/tenancy.php]`.
|
||||
|
||||
#### `database`
|
||||
|
||||
Databases will be named like this:
|
||||
|
||||
```php
|
||||
config('tenancy.database.prefix') . $uuid . config('tenancy.database.suffix')
|
||||
```
|
||||
|
||||
They will use a connection based on the connection specified using the `based_on` setting. Using `mysql` or `sqlite` is fine, but if you need to change more things than just the database name, you can create a new `tenant` connection and set `tenancy.database.based_on` to `tenant`.
|
||||
|
||||
#### `redis`
|
||||
|
||||
Keys will be prefixed with:
|
||||
|
||||
```php
|
||||
config('tenancy.redis.prefix_base') . $uuid
|
||||
```
|
||||
|
||||
These changes will only apply for connections listed in `prefixed_connections`.
|
||||
|
||||
#### `cache`
|
||||
|
||||
Cache keys will be tagged with a tag:
|
||||
|
||||
```php
|
||||
config('tenancy.cache.prefix_base') . $uuid
|
||||
```
|
||||
|
||||
### `filesystem`
|
||||
|
||||
Filesystem paths will be suffixed with:
|
||||
|
||||
```php
|
||||
config('tenancy.filesystem.suffix_base') . $uuid
|
||||
```
|
||||
|
||||
These changes will only apply for disks listen in `disks`.
|
||||
|
||||
You can see an example in the [Filesystem](#Filesystem) section of the documentation.
|
||||
|
||||
# Usage
|
||||
|
||||
## Obtaining a `TenantManager` instance
|
||||
|
||||
You can use the `tenancy()` and `tenant()` helpers to resolve `Stancl\Tenancy\TenantManager` out of the service container. These two helpers are exactly the same, the only reason there are two is nice syntax. `tenancy()->init()` sounds better than `tenant()->init()` and `tenant()->create()` sounds better than `tenancy()->create()`.
|
||||
|
||||
### Creating a new tenant
|
||||
|
||||
```php
|
||||
>>> tenant()->create('dev.localhost')
|
||||
=> [
|
||||
"uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957",
|
||||
"domain" => "dev.localhost",
|
||||
]
|
||||
```
|
||||
|
||||
### Starting a session as a tenant
|
||||
|
||||
This runs `TenantManager::bootstrap()` which switches the DB connection, prefixes Redis, changes filesystem root paths, etc.
|
||||
|
||||
```php
|
||||
tenancy()->init();
|
||||
// The domain will be autodetected unless specified as an argument
|
||||
tenancy()->init('dev.localhost');
|
||||
```
|
||||
|
||||
### Getting tenant information based on his UUID
|
||||
|
||||
You can use `find()`, which is an alias for `getTenantById()`.
|
||||
You may use the second argument to specify the key(s) as a string/array.
|
||||
|
||||
```php
|
||||
>>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339');
|
||||
=> [
|
||||
"uuid" => "dbe0b330-1a6e-11e9-b4c3-354da4b4f339",
|
||||
"domain" => "localhost",
|
||||
"foo" => "bar",
|
||||
]
|
||||
>>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339', 'foo');
|
||||
=> [
|
||||
"foo" => "bar",
|
||||
]
|
||||
>>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339', ['foo', 'domain']);
|
||||
=> [
|
||||
"foo" => "bar",
|
||||
"domain" => "localhost",
|
||||
]
|
||||
```
|
||||
|
||||
### Getting tenant UUID based on his domain
|
||||
|
||||
```php
|
||||
>>> tenant()->getTenantIdByDomain('localhost');
|
||||
=> "b3ce3f90-1a88-11e9-a6b0-038c6337ae50"
|
||||
>>> tenant()->getIdByDomain('localhost');
|
||||
=> "b3ce3f90-1a88-11e9-a6b0-038c6337ae50"
|
||||
```
|
||||
|
||||
### Getting tenant information based on his domain
|
||||
|
||||
You may use the second argument to specify the key(s) as a string/array.
|
||||
|
||||
```php
|
||||
>>> tenant()->findByDomain('localhost');
|
||||
=> [
|
||||
"uuid" => "b3ce3f90-1a88-11e9-a6b0-038c6337ae50",
|
||||
"domain" => "localhost",
|
||||
]
|
||||
```
|
||||
|
||||
### Getting current tenant information
|
||||
|
||||
You can access the public array `tenant` of `TenantManager` like this:
|
||||
|
||||
```php
|
||||
tenancy()->tenant
|
||||
```
|
||||
|
||||
which returns an array. If you want to get the value of a specific key from the array, you can use one of the helpers with an argument --- the key on the `tenant` array.
|
||||
|
||||
```php
|
||||
tenant('uuid'); // Does the same thing as tenant()->tenant['uuid']
|
||||
```
|
||||
|
||||
### Listing all tenants
|
||||
|
||||
```php
|
||||
>>> tenant()->all();
|
||||
=> Illuminate\Support\Collection {#2980
|
||||
all: [
|
||||
[
|
||||
"uuid" => "32e20780-1a88-11e9-a051-4b6489a7edac",
|
||||
"domain" => "localhost",
|
||||
],
|
||||
[
|
||||
"uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957",
|
||||
"domain" => "dev.localhost",
|
||||
],
|
||||
],
|
||||
}
|
||||
>>> tenant()->all()->pluck('domain');
|
||||
=> Illuminate\Support\Collection {#2983
|
||||
all: [
|
||||
"localhost",
|
||||
"dev.localhost",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Deleting a tenant
|
||||
|
||||
```php
|
||||
>>> tenant()->delete('dbe0b330-1a6e-11e9-b4c3-354da4b4f339');
|
||||
=> true
|
||||
>>> tenant()->delete(tenant()->getTenantIdByDomain('dev.localhost'));
|
||||
=> true
|
||||
>>> tenant()->delete(tenant()->findByDomain('localhost')['uuid']);
|
||||
=> true
|
||||
```
|
||||
|
||||
## Storage driver
|
||||
|
||||
Currently, only Redis is supported, but you're free to code your own storage driver which follows the `Stancl\Tenancy\Interfaces\StorageDriver` interface. Just point the `tenancy.storage_driver` setting at your driver.
|
||||
|
||||
**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 for anything else.
|
||||
|
||||
### Storing custom data
|
||||
|
||||
Along with the tenant and database info, you can store your own data in the storage. You can use:
|
||||
|
||||
```php
|
||||
tenancy()->get($key);
|
||||
tenancy()->put($key, $value);
|
||||
tenancy()->set($key, $value); // alias for put()
|
||||
```
|
||||
|
||||
Note that `$key` has to be a string.
|
||||
|
||||
## Database
|
||||
|
||||
The entire application will use a new database connection. The connection will be based on the connection specified in `tenancy.database.based_on`. A database name of `tenancy.database.prefix` + tenant UUID + `tenancy.database.suffix` will be used. You can set the suffix to `.sqlite` if you're using sqlite and want the files to be in the sqlite format and you can leave the suffix empty if you're using MySQL (for example).
|
||||
|
||||
## Redis
|
||||
|
||||
Connections listed in the `tenancy.redis.prefixed_connections` config array use a prefix based on the `tenancy.redis.prefix_base` and the tenant UUID.
|
||||
|
||||
**Note: You *must* use phpredis for prefixes to work. Predis doesn't support prefixes.**
|
||||
|
||||
## Cache
|
||||
|
||||
Both `cache()` and `Cache` will use `Stancl\Tenancy\CacheManager`, which adds a tag (`prefix_base` + tenant UUID) to all methods called on it.
|
||||
|
||||
|
||||
## Filesystem
|
||||
|
||||
Assuming the following tenancy config:
|
||||
|
||||
```php
|
||||
'filesystem' => [
|
||||
'suffix_base' => 'tenant',
|
||||
// Disks which should be suffixed with the prefix_base + tenant UUID.
|
||||
'disks' => [
|
||||
'local',
|
||||
// 's3',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
The `local` filesystem driver will be suffixed with a directory containing `tenant` and the tenant UUID.
|
||||
|
||||
```php
|
||||
>>> Storage::disk('local')->getAdapter()->getPathPrefix()
|
||||
=> "/var/www/laravel/multitenancy/storage/app/"
|
||||
>>> tenancy()->init()
|
||||
=> [
|
||||
"uuid" => "dbe0b330-1a6e-11e9-b4c3-354da4b4f339",
|
||||
"domain" => "localhost",
|
||||
]
|
||||
>>> Storage::disk('local')->getAdapter()->getPathPrefix()
|
||||
=> "/var/www/laravel/multitenancy/storage/app/tenantdbe0b330-1a6e-11e9-b4c3-354da4b4f339/"
|
||||
```
|
||||
|
||||
## Artisan commands
|
||||
|
||||
```
|
||||
Available commands for the "tenants" namespace:
|
||||
tenants:list List tenants.
|
||||
tenants:migrate Run migrations for tenant(s)
|
||||
tenants:rollback Rollback migrations for tenant(s).
|
||||
tenants:seed Seed tenant database(s).
|
||||
```
|
||||
|
||||
#### `tenants:list`
|
||||
|
||||
```
|
||||
$ artisan tenants:list
|
||||
Listing all tenants.
|
||||
[Tenant] uuid: dbe0b330-1a6e-11e9-b4c3-354da4b4f339 @ localhost
|
||||
[Tenant] uuid: 49670df0-1a87-11e9-b7ba-cf5353777957 @ dev.localhost
|
||||
```
|
||||
|
||||
#### `tenants:migrate`, `tenants:rollback`, `tenants:seed`
|
||||
|
||||
- You may specify the tenant(s) UUIDs using the `--tenants` option.
|
||||
|
||||
## Some tips
|
||||
|
||||
- If you create a tenant using the interactive console (`artisan tinker`) and use sqlite, you might need to change the database's permissions and/or ownership (`chmod`/`chown`) so that the web application can access it.
|
||||
31
composer.json
Normal file
31
composer.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "stancl/tenancy",
|
||||
"description": "A Laravel multi-database tenancy implementation that respects your code.",
|
||||
"keywords": ["laravel", "multi-tenancy", "multi-database", "tenancy"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Samuel Štancl",
|
||||
"email": "samuel.stancl@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"illuminate/support": "^5.7",
|
||||
"webpatser/laravel-uuid": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Stancl\\Tenancy\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Stancl\\Tenancy\\TenancyServiceProvider"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/CacheManager.php
Normal file
19
src/CacheManager.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Cache\CacheManager as BaseCacheManager;
|
||||
|
||||
class CacheManager extends BaseCacheManager
|
||||
{
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
$tags = [config('tenancy.cache.prefix_base') . tenant('uuid')];
|
||||
|
||||
if ($method === "tags") {
|
||||
return $this->store()->tags(array_merge($tags, ...$parameters));
|
||||
}
|
||||
|
||||
return $this->store()->tags($tags)->$method(...$parameters);
|
||||
}
|
||||
}
|
||||
69
src/Commands/Migrate.php
Normal file
69
src/Commands/Migrate.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||
|
||||
class Migrate extends MigrateCommand
|
||||
{
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Run migrations for tenant(s)';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Migrator $migrator, DatabaseManager $database)
|
||||
{
|
||||
parent::__construct($migrator);
|
||||
$this->database = $database;
|
||||
|
||||
$this->setName('tenants:migrate');
|
||||
$this->specifyParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->confirmToProceed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->input->setOption('database', 'tenant');
|
||||
|
||||
tenant()->all($this->option('tenants'))->each(function ($tenant) {
|
||||
$this->line("Tenant: {$tenant['uuid']} ({$tenant['domain']})");
|
||||
$this->database->connectToTenant($tenant);
|
||||
|
||||
// Migrate
|
||||
parent::handle();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
return array_merge([
|
||||
['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null]
|
||||
], parent::getOptions());
|
||||
}
|
||||
|
||||
protected function getMigrationPaths()
|
||||
{
|
||||
return [database_path('migrations/tenant')];
|
||||
}
|
||||
}
|
||||
69
src/Commands/Rollback.php
Normal file
69
src/Commands/Rollback.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Illuminate\Database\Console\Migrations\RollbackCommand;
|
||||
|
||||
class Rollback extends RollbackCommand
|
||||
{
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Rollback migrations for tenant(s).';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Migrator $migrator, DatabaseManager $database)
|
||||
{
|
||||
parent::__construct($migrator);
|
||||
$this->database = $database;
|
||||
|
||||
$this->setName('tenants:rollback');
|
||||
$this->specifyParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->confirmToProceed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->input->setOption('database', 'tenant');
|
||||
|
||||
tenant()->all($this->option('tenants'))->each(function ($tenant) {
|
||||
$this->line("Tenant: {$tenant['uuid']} ({$tenant['domain']})");
|
||||
$this->database->connectToTenant($tenant);
|
||||
|
||||
// Migrate
|
||||
parent::handle();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
return array_merge([
|
||||
['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null]
|
||||
], parent::getOptions());
|
||||
}
|
||||
|
||||
protected function getMigrationPaths()
|
||||
{
|
||||
return [database_path('migrations/tenant')];
|
||||
}
|
||||
}
|
||||
70
src/Commands/Seed.php
Normal file
70
src/Commands/Seed.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Illuminate\Database\Console\Seeds\SeedCommand;
|
||||
use Illuminate\Database\ConnectionResolverInterface;
|
||||
|
||||
class Seed extends SeedCommand
|
||||
{
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Seed tenant database(s).';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ConnectionResolverInterface $resolver, DatabaseManager $database)
|
||||
{
|
||||
parent::__construct($resolver);
|
||||
$this->database = $database;
|
||||
|
||||
$this->setName('tenants:seed');
|
||||
$this->specifyParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->confirmToProceed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->input->setOption('database', 'tenant');
|
||||
|
||||
tenant()->all($this->option('tenants'))->each(function ($tenant) {
|
||||
$this->line("Tenant: {$tenant['uuid']} ({$tenant['domain']})");
|
||||
$this->database->connectToTenant($tenant);
|
||||
|
||||
// Migrate
|
||||
parent::handle();
|
||||
});
|
||||
}
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
return array_merge([
|
||||
['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null]
|
||||
], parent::getOptions());
|
||||
}
|
||||
|
||||
protected function getMigrationPaths()
|
||||
{
|
||||
return [database_path('migrations/tenant')];
|
||||
}
|
||||
}
|
||||
45
src/Commands/TenantList.php
Normal file
45
src/Commands/TenantList.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class TenantList extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'tenants:list';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'List tenants.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info("Listing all tenants.");
|
||||
tenancy()->all()->each(function ($tenant) {
|
||||
$this->line("[Tenant] uuid: {$tenant['uuid']} @ {$tenant['domain']}");
|
||||
});
|
||||
}
|
||||
}
|
||||
64
src/DatabaseManager.php
Normal file
64
src/DatabaseManager.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
|
||||
|
||||
class DatabaseManager
|
||||
{
|
||||
public function __construct(BaseDatabaseManager $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function connect(string $database)
|
||||
{
|
||||
$this->createTenantConnection($database);
|
||||
$this->database->setDefaultConnection('tenant');
|
||||
$this->database->reconnect('tenant');
|
||||
}
|
||||
|
||||
public function connectToTenant($tenant)
|
||||
{
|
||||
$this->connect(tenant()->getDatabaseName($tenant));
|
||||
}
|
||||
|
||||
public function disconnect()
|
||||
{
|
||||
$this->database->reconnect('default');
|
||||
$this->database->setDefaultConnection('default');
|
||||
}
|
||||
|
||||
public function create(string $name, string $driver = null)
|
||||
{
|
||||
$this->createTenantConnection($name);
|
||||
$driver = $driver ?: $this->getDriver();
|
||||
if ($driver === "sqlite") {
|
||||
$f = fopen(database_path($name), 'w');
|
||||
fclose($f);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return DB::statement("CREATE DATABASE `$name`");
|
||||
}
|
||||
|
||||
public function getDriver(): ?string
|
||||
{
|
||||
return config("database.connections.tenant.driver");
|
||||
}
|
||||
|
||||
public function createTenantConnection(string $database_name)
|
||||
{
|
||||
// Create the `tenancy` database connection.
|
||||
$based_on = config('tenancy.database.based_on') ?: config('database.default');
|
||||
config()->set([
|
||||
'database.connections.tenant' => config('database.connections.' . $based_on)
|
||||
]);
|
||||
|
||||
// Change DB name
|
||||
$database_name = $this->getDriver() === "sqlite" ? database_path($database_name) : $database_name;
|
||||
config()->set(['database.connections.tenant.database' => $database_name]);
|
||||
}
|
||||
}
|
||||
15
src/Interfaces/StorageDriver.php
Normal file
15
src/Interfaces/StorageDriver.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Interfaces;
|
||||
|
||||
interface StorageDriver
|
||||
{
|
||||
public function identifyTenant(string $domain): array;
|
||||
public function getAllTenants(array $uuids = []): array;
|
||||
public function getTenantById(string $uuid, $fields = []): array;
|
||||
public function getTenantIdByDomain(string $domain): ?string;
|
||||
public function createTenant(string $domain, string $uuid): array;
|
||||
public function deleteTenant(string $uuid): bool;
|
||||
public function get(string $uuid, string $key);
|
||||
public function put(string $uuid, string $key, $value);
|
||||
}
|
||||
37
src/Middleware/InitializeTenancy.php
Normal file
37
src/Middleware/InitializeTenancy.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class InitializeTenancy
|
||||
{
|
||||
public function __construct(Closure $onFail = null)
|
||||
{
|
||||
$this->onFail = $onFail ?: function ($e) { throw $e; };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
try {
|
||||
tenancy()->init();
|
||||
} catch (\Exception $e) {
|
||||
// Pass the exception to the onFail function if it takes any parameters.
|
||||
$callback = $this->onFail;
|
||||
if ((new \ReflectionFunction($callback))->getNumberOfParameters() > 0) {
|
||||
$callback($e);
|
||||
} else {
|
||||
$callback();
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
84
src/StorageDrivers/RedisStorageDriver.php
Normal file
84
src/StorageDrivers/RedisStorageDriver.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers;
|
||||
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Stancl\Tenancy\Interfaces\StorageDriver;
|
||||
|
||||
class RedisStorageDriver implements StorageDriver
|
||||
{
|
||||
private $redis;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->redis = Redis::connection('tenancy');
|
||||
}
|
||||
|
||||
public function identifyTenant(string $domain): array
|
||||
{
|
||||
$id = $this->getTenantIdByDomain($domain);
|
||||
if (! $id) {
|
||||
throw new \Exception("Tenant could not be identified on domain {$domain}");
|
||||
}
|
||||
return array_merge(['uuid' => $id], $this->getTenantById($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the tenant based on his uuid.
|
||||
*
|
||||
* @param string $uuid
|
||||
* @param array|string $fields
|
||||
* @return array
|
||||
*/
|
||||
public function getTenantById(string $uuid, $fields = []): array
|
||||
{
|
||||
if (! $fields) {
|
||||
return $this->redis->hgetall("tenants:$uuid");
|
||||
}
|
||||
|
||||
return array_combine($fields, $this->redis->hmget("tenants:$uuid", $fields));
|
||||
}
|
||||
|
||||
public function getTenantIdByDomain(string $domain): ?string
|
||||
{
|
||||
return $this->redis->hget("domains:$domain", 'tenant_id') ?: null;
|
||||
}
|
||||
|
||||
public function createTenant(string $domain, string $uuid): array
|
||||
{
|
||||
$this->redis->hmset("domains:$domain", 'tenant_id', $uuid);
|
||||
$this->redis->hmset("tenants:$uuid", 'uuid', $uuid, 'domain', $domain);
|
||||
return $this->redis->hgetall("tenants:$uuid");
|
||||
}
|
||||
|
||||
public function deleteTenant(string $id): bool
|
||||
{
|
||||
$domain = $this->getTenantById($id)['domain'];
|
||||
$this->redis->del("domains:$domain");
|
||||
return (bool) $this->redis->del("tenants:$id");
|
||||
}
|
||||
|
||||
public function getAllTenants(array $uuids = []): array
|
||||
{
|
||||
$hashes = array_map(function ($hash) {
|
||||
return "tenants:{$hash}";
|
||||
}, $uuids);
|
||||
|
||||
$hashes = $hashes ?: $this->redis->scan(null, 'tenants:*');
|
||||
|
||||
return array_map(function ($tenant) {
|
||||
return $this->redis->hgetall($tenant);
|
||||
}, $hashes);
|
||||
}
|
||||
|
||||
public function get(string $uuid, string $key)
|
||||
{
|
||||
return $this->redis->hget("tenants:$uuid", $key);
|
||||
}
|
||||
|
||||
public function put(string $uuid, string $key, $value)
|
||||
{
|
||||
$this->redis->hset("tenants:$uuid", $key, $value);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
63
src/TenancyServiceProvider.php
Normal file
63
src/TenancyServiceProvider.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Stancl\Tenancy\Commands\Seed;
|
||||
use Stancl\Tenancy\TenantManager;
|
||||
use Stancl\Tenancy\DatabaseManager;
|
||||
use Stancl\Tenancy\Commands\Migrate;
|
||||
use Stancl\Tenancy\Commands\Rollback;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Commands\TenantList;
|
||||
use Stancl\Tenancy\Interfaces\StorageDriver;
|
||||
use Stancl\Tenancy\StorageDrivers\RedisStorageDriver;
|
||||
|
||||
class TenancyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
Migrate::class,
|
||||
Rollback::class,
|
||||
Seed::class,
|
||||
TenantList::class,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/config/tenancy.php' => config_path('tenancy.php'),
|
||||
], 'config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->mergeConfigFrom(__DIR__ . '/config/tenancy.php', 'tenancy');
|
||||
|
||||
$this->app->bind(StorageDriver::class, $this->app['config']['tenancy.storage_driver']);
|
||||
$this->app->singleton(DatabaseManager::class);
|
||||
$this->app->singleton(TenantManager::class, function ($app) {
|
||||
return new TenantManager($app, $app[StorageDriver::class], $app[DatabaseManager::class]);
|
||||
});
|
||||
|
||||
$this->app->singleton(Migrate::class, function ($app) {
|
||||
return new Migrate($app['migrator'], $app[DatabaseManager::class]);
|
||||
});
|
||||
$this->app->singleton(Rollback::class, function ($app) {
|
||||
return new Rollback($app['migrator'], $app[DatabaseManager::class]);
|
||||
});
|
||||
$this->app->singleton(Seed::class, function ($app) {
|
||||
return new Seed($app['db'], $app[DatabaseManager::class]);
|
||||
});
|
||||
}
|
||||
}
|
||||
30
src/Tenant.php
Normal file
30
src/Tenant.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
class Tenant
|
||||
{
|
||||
public $uuid;
|
||||
public $domain;
|
||||
public $databaseName;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array|string $data
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$data = is_string($data) ? json_decode($data, true) : (array) $data;
|
||||
|
||||
$this->uuid = $data['uuid'];
|
||||
$this->domain = $data['domain'] ?? tenancy()->getTenantById($data['uuid'], 'domain');
|
||||
$this->databaseName = $data['database_name'] ?? $this->getDatabaseName($data);
|
||||
}
|
||||
|
||||
public function getDatabaseName($uuid = null)
|
||||
{
|
||||
$uuid = $uuid ?: $this->uuid;
|
||||
return config('tenancy.database._prefix_base') . $uuid . config('tenancy.database._suffix');
|
||||
}
|
||||
}
|
||||
226
src/TenantManager.php
Normal file
226
src/TenantManager.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Stancl\Tenancy\BootstrapsTenancy;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Stancl\Tenancy\Interfaces\StorageDriver;
|
||||
|
||||
class TenantManager
|
||||
{
|
||||
use BootstrapsTenancy;
|
||||
|
||||
/**
|
||||
* The application instance.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Foundation\Application|\Illuminate\Foundation\Application
|
||||
*/
|
||||
private $app;
|
||||
|
||||
/**
|
||||
* Storage driver for tenant metadata.
|
||||
*
|
||||
* @var StorageDriver
|
||||
*/
|
||||
public $storage;
|
||||
|
||||
/**
|
||||
* Database manager.
|
||||
*
|
||||
* @var DatabaseManager
|
||||
*/
|
||||
public $database;
|
||||
|
||||
/**
|
||||
* Current tenant.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $tenant;
|
||||
|
||||
public function __construct($app, StorageDriver $storage, DatabaseManager $database)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->storage = $storage;
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function init(string $domain = null): array
|
||||
{
|
||||
$this->setTenant($this->identify($domain));
|
||||
$this->bootstrap();
|
||||
return $this->tenant;
|
||||
}
|
||||
|
||||
public function identify(string $domain = null): array
|
||||
{
|
||||
$domain = $domain ?: $this->currentDomain();
|
||||
|
||||
if (! $domain) {
|
||||
throw new \Exception("No domain supplied nor detected.");
|
||||
}
|
||||
|
||||
$tenant = $this->storage->identifyTenant($domain);
|
||||
|
||||
if (! $tenant || ! array_key_exists('uuid', $tenant) || ! $tenant['uuid']) {
|
||||
throw new \Exception("Tenant could not be identified on domain {$domain}.");
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public function create(string $domain = null): array
|
||||
{
|
||||
$domain = $domain ?: $this->currentDomain();
|
||||
|
||||
if ($id = $this->storage->getTenantIdByDomain($domain)) {
|
||||
throw new \Exception("Domain $domain is already occupied by tenant $id.");
|
||||
}
|
||||
|
||||
$tenant = $this->storage->createTenant($domain, \Uuid::generate(1, $domain));
|
||||
$this->database->create($this->getDatabaseName($tenant));
|
||||
\Artisan::call('tenants:migrate', [
|
||||
'--tenants' => [$tenant['uuid']]
|
||||
]);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public function delete(string $uuid): bool
|
||||
{
|
||||
return $this->storage->deleteTenant($uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array with information about a tenant based on his uuid.
|
||||
*
|
||||
* @param string $uuid
|
||||
* @param array|string $fields
|
||||
* @return array
|
||||
*/
|
||||
public function getTenantById(string $uuid, $fields = [])
|
||||
{
|
||||
$fields = (array) $fields;
|
||||
return $this->storage->getTenantById($uuid, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for getTenantById().
|
||||
*
|
||||
* @param string $uuid
|
||||
* @param array|string $fields
|
||||
* @return array
|
||||
*/
|
||||
public function find(string $uuid, $fields = [])
|
||||
{
|
||||
return $this->getTenantById($uuid, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tenant uuid based on the domain that belongs to him.
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTenantIdByDomain(string $domain = null): ?string
|
||||
{
|
||||
$domain = $domain ?: $this->currentDomain();
|
||||
|
||||
return $this->storage->getTenantIdByDomain($domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for getTenantIdByDomain().
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string|null
|
||||
*/
|
||||
public function getIdByDomain(string $domain = null)
|
||||
{
|
||||
return $this->getTenantIdByDomain($domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tenant information based on his domain.
|
||||
*
|
||||
* @param string $domain
|
||||
* @param mixed $fields
|
||||
* @return array
|
||||
*/
|
||||
public function findByDomain(string $domain = null, $fields = [])
|
||||
{
|
||||
$domain = $domain ?: $this->currentDomain();
|
||||
|
||||
return $this->find($this->getIdByDomain($domain), $fields);
|
||||
}
|
||||
|
||||
public static function currentDomain(): ?string
|
||||
{
|
||||
return request()->getHost() ?? null;
|
||||
}
|
||||
|
||||
public function getDatabaseName($tenant = []): string
|
||||
{
|
||||
$tenant = $tenant ?: $this->tenant;
|
||||
return config('tenancy.database.prefix') . $tenant['uuid'] . config('tenancy.database.suffix');
|
||||
}
|
||||
|
||||
public function setTenant(array $tenant): array
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tenants.
|
||||
*
|
||||
* @param array|string $uuids
|
||||
* @return array
|
||||
*/
|
||||
public function all($uuids = [])
|
||||
{
|
||||
$uuid = (array) $uuids;
|
||||
return collect($this->storage->getAllTenants($uuids));
|
||||
}
|
||||
|
||||
public function actAsId(string $uuid): array
|
||||
{
|
||||
return $this->setTenant($this->storage->getTenantById($uuid));
|
||||
}
|
||||
|
||||
public function actAsDomain(string $domain): string
|
||||
{
|
||||
return $this->init($domain);
|
||||
}
|
||||
|
||||
public function get(string $key)
|
||||
{
|
||||
return $this->storage->get($this->tenant['uuid'], $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a value into the storage for the current tenant.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function put(string $key, $value)
|
||||
{
|
||||
// Todo allow $value to be null and $key to be an array.
|
||||
return $this->tenant[$key] = $this->storage->put($this->tenant['uuid'], $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for put().
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function set(string $key, $value)
|
||||
{
|
||||
return $this->put($this->put($key, $value));
|
||||
}
|
||||
}
|
||||
46
src/Traits/BootstrapsTenancy.php
Normal file
46
src/Traits/BootstrapsTenancy.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
trait BootstrapsTenancy
|
||||
{
|
||||
public function bootstrap()
|
||||
{
|
||||
$this->switchDatabaseConnection();
|
||||
$this->setPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']);
|
||||
$this->tagCache();
|
||||
$this->suffixFilesystemRootPaths();
|
||||
}
|
||||
|
||||
public function switchDatabaseConnection()
|
||||
{
|
||||
$this->database->connect($this->getDatabaseName());
|
||||
}
|
||||
|
||||
public function setPhpRedisPrefix($connections = ['default'])
|
||||
{
|
||||
return;
|
||||
foreach ($connections as $connection) {
|
||||
$prefix = config('tenancy.redis.prefix_base') . $this->tenant['uuid'];
|
||||
$client = Redis::connection($connection)->client();
|
||||
$client->setOption($client::OPT_PREFIX, $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
public function tagCache()
|
||||
{
|
||||
$this->app->extend('cache', function () {
|
||||
return new CacheManager($this->app);
|
||||
});
|
||||
}
|
||||
|
||||
public function suffixFilesystemRootPaths()
|
||||
{
|
||||
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . tenant('uuid');
|
||||
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
||||
\Storage::disk($disk)->getAdapter()->setPathPrefix(
|
||||
$this->app['config']["filesystems.disks.{$disk}.root"] . "/{$suffix}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/config/tenancy.php
Normal file
28
src/config/tenancy.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'storage_driver' => 'Stancl\Tenancy\StorageDrivers\RedisStorageDriver',
|
||||
'database' => [
|
||||
'based_on' => 'sqlite',
|
||||
'prefix' => 'tenant',
|
||||
'suffix' => '.sqlite',
|
||||
],
|
||||
'redis' => [
|
||||
'prefix_base' => 'tenant',
|
||||
'prefixed_connections' => [
|
||||
'default',
|
||||
],
|
||||
],
|
||||
'cache' => [
|
||||
'prefix_base' => 'tenant',
|
||||
],
|
||||
|
||||
'filesystem' => [
|
||||
'suffix_base' => 'tenant',
|
||||
// Disks which should be suffixed with the suffix_base + tenant UUID.
|
||||
'disks' => [
|
||||
// 'local',
|
||||
// 's3',
|
||||
],
|
||||
],
|
||||
];
|
||||
21
src/helpers.php
Normal file
21
src/helpers.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
use Stancl\Tenancy\TenantManager;
|
||||
|
||||
if (! function_exists('tenancy')) {
|
||||
function tenancy($key = null)
|
||||
{
|
||||
if ($key) {
|
||||
return app(TenantManager::class)->tenant[$key];
|
||||
}
|
||||
|
||||
return app(TenantManager::class);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('tenant')) {
|
||||
function tenant($key = null)
|
||||
{
|
||||
return tenancy($key);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue