1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 03:54:03 +00:00

Initial commit

This commit is contained in:
Samuel Štancl 2019-01-17 22:24:12 +01:00
commit deb3ad77f5
19 changed files with 1283 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
composer.lock
vendor/

21
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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')];
}
}

View 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
View 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]);
}
}

View 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);
}

View 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);
}
}

View 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;
}
}

View 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
View 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
View 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));
}
}

View 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
View 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
View 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);
}
}