1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 14:14:04 +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

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