1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 18:54:03 +00:00

Merge branch '1.x' into telescope-tags

This commit is contained in:
Samuel Štancl 2019-08-17 22:22:52 +02:00
commit a00113b3d1
36 changed files with 558 additions and 707 deletions

View file

@ -18,7 +18,7 @@ class CacheManager extends BaseCacheManager
$names = $parameters[0];
$names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/5.7/cache#removing-tagged-cache-items
return $this->store()->tags(array_merge($tags, $names));
return $this->store()->tags(\array_merge($tags, $names));
}
return $this->store()->tags($tags)->$method(...$parameters);

View file

@ -34,15 +34,16 @@ class Install extends Command
]);
$this->info('✔️ Created config/tenancy.php');
file_put_contents(app_path('Http/Kernel.php'), str_replace(
\file_put_contents(app_path('Http/Kernel.php'), \str_replace(
'protected $middlewarePriority = [',
"protected \$middlewarePriority = [\n \Stancl\Tenancy\Middleware\InitializeTenancy::class,",
file_get_contents(app_path('Http/Kernel.php'))
\file_get_contents(app_path('Http/Kernel.php'))
));
$this->info('✔️ Set middleware priority');
file_put_contents(base_path('routes/tenant.php'),
"<?php
\file_put_contents(
base_path('routes/tenant.php'),
"<?php
/*
|--------------------------------------------------------------------------
@ -58,7 +59,8 @@ class Install extends Command
Route::get('/your/application/homepage', function () {
return 'This is your multi-tenant application. The uuid of the current tenant is ' . tenant('uuid');
});
");
"
);
$this->info('✔️ Created routes/tenant.php');
$this->line('');
@ -71,6 +73,11 @@ Route::get('/your/application/homepage', function () {
$this->info('✔️ Created migration.');
}
if (! \is_dir(database_path('migrations/tenant'))) {
\mkdir(database_path('migrations/tenant'));
$this->info('✔️ Created database/migrations/tenant folder.');
}
$this->comment('✨️ stancl/tenancy installed successfully.');
}
}

View file

@ -47,11 +47,14 @@ class Migrate extends MigrateCommand
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);
// See Illuminate\Database\Migrations\DatabaseMigrationRepository::getConnection.
// Database connections are cached by Illuminate\Database\ConnectionResolver.
$connectionName = "tenant{$tenant['uuid']}";
$this->input->setOption('database', $connectionName);
$this->database->connectToTenant($tenant, $connectionName);
// Migrate
parent::handle();

View file

@ -40,7 +40,7 @@ class Run extends Command
$callback = function ($prefix = '') {
return function ($arguments, $argument) use ($prefix) {
[$key, $value] = explode('=', $argument, 2);
[$key, $value] = \explode('=', $argument, 2);
$arguments[$prefix . $key] = $value;
return $arguments;
@ -48,13 +48,13 @@ class Run extends Command
};
// Turns ['foo=bar', 'abc=xyz=zzz'] into ['foo' => 'bar', 'abc' => 'xyz=zzz']
$arguments = array_reduce($this->option('argument'), $callback(), []);
$arguments = \array_reduce($this->option('argument'), $callback(), []);
// Turns ['foo=bar', 'abc=xyz=zzz'] into ['--foo' => 'bar', '--abc' => 'xyz=zzz']
$options = array_reduce($this->option('option'), $callback('--'), []);
$options = \array_reduce($this->option('option'), $callback('--'), []);
// Run command
$this->call($this->argument('commandname'), array_merge($arguments, $options));
$this->call($this->argument('commandname'), \array_merge($arguments, $options));
tenancy()->end();
});

View file

@ -10,21 +10,23 @@ final class DatabaseManager
{
public $originalDefaultConnection;
protected $defaultTenantConnectionName = 'tenant';
public function __construct(BaseDatabaseManager $database)
{
$this->originalDefaultConnection = config('database.default');
$this->database = $database;
}
public function connect(string $database)
public function connect(string $database, string $connectionName = null)
{
$this->createTenantConnection($database);
$this->useConnection('tenant');
$this->createTenantConnection($database, $connectionName);
$this->useConnection($connectionName);
}
public function connectToTenant($tenant)
public function connectToTenant($tenant, string $connectionName = null)
{
$this->connect(tenant()->getDatabaseName($tenant));
$this->connect(tenant()->getDatabaseName($tenant), $connectionName);
}
public function disconnect()
@ -50,7 +52,7 @@ final class DatabaseManager
$databaseManagers = config('tenancy.database_managers');
if (! array_key_exists($driver, $databaseManagers)) {
if (! \array_key_exists($driver, $databaseManagers)) {
throw new \Exception("Database could not be created: no database manager for driver $driver is registered.");
}
@ -76,7 +78,7 @@ final class DatabaseManager
$databaseManagers = config('tenancy.database_managers');
if (! array_key_exists($driver, $databaseManagers)) {
if (! \array_key_exists($driver, $databaseManagers)) {
throw new \Exception("Database could not be deleted: no database manager for driver $driver is registered.");
}
@ -87,26 +89,32 @@ final class DatabaseManager
}
}
public function getDriver(): ?string
public function getDriver($connectionName = null): ?string
{
return config('database.connections.tenant.driver');
$connectionName = $connectionName ?: $this->defaultTenantConnectionName;
return config("database.connections.$connectionName.driver");
}
public function createTenantConnection(string $database_name)
public function createTenantConnection(string $databaseName, string $connectionName = null)
{
// Create the `tenancy` database connection.
$connectionName = $connectionName ?: $this->defaultTenantConnectionName;
// Create the database connection.
$based_on = config('tenancy.database.based_on') ?: config('database.default');
config()->set([
'database.connections.tenant' => config('database.connections.' . $based_on),
"database.connections.$connectionName" => 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]);
$databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName;
config()->set(["database.connections.$connectionName.database" => $databaseName]);
}
public function useConnection(string $connection)
public function useConnection(string $connection = null)
{
$connection = $connection ?: $this->defaultTenantConnectionName;
$this->database->setDefaultConnection($connection);
$this->database->reconnect($connection);
}

View file

@ -6,6 +6,7 @@ interface StorageDriver
{
public function identifyTenant(string $domain): array;
/** @return array[] */
public function getAllTenants(array $uuids = []): array;
public function getTenantById(string $uuid, array $fields = []): array;

View file

@ -0,0 +1,87 @@
<?php
namespace Stancl\Tenancy\StorageDrivers;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Interfaces\StorageDriver;
class DatabaseStorageDriver implements StorageDriver
{
public $useJson = false;
// todo use an instance of tenant model?
// todo write tests verifying that data is decoded and added to the array
public function identifyTenant(string $domain): array
{
$id = $this->getTenantIdByDomain($domain);
if (! $id) {
throw new \Exception("Tenant could not be identified on domain {$domain}");
}
return $this->getTenantById($id);
}
/**
* Get information about the tenant based on his uuid.
*
* @param string $uuid
* @param array $fields
* @return array
*/
public function getTenantById(string $uuid, array $fields = []): array
{
if ($fields) {
return Tenant::decodeData(Tenant::find($uuid)->only($fields));
} else {
return Tenant::find($uuid)->decoded();
}
}
public function getTenantIdByDomain(string $domain): ?string
{
return Tenant::where('domain', $domain)->first()->uuid ?? null;
}
public function createTenant(string $domain, string $uuid): array
{
$tenant = Tenant::create(['uuid' => $uuid, 'domain' => $domain, 'data' => '{}'])->toArray();
unset($tenant['data']);
return $tenant;
}
public function deleteTenant(string $id): bool
{
return Tenant::find($id)->delete();
}
public function getAllTenants(array $uuids = []): array
{
return Tenant::getAllTenants($uuids)->toArray();
}
public function get(string $uuid, string $key)
{
return Tenant::find($uuid)->get($key);
}
public function getMany(string $uuid, array $keys): array
{
return Tenant::find($uuid)->getMany($keys);
}
public function put(string $uuid, string $key, $value)
{
return Tenant::find($uuid)->put($key, $value);
}
public function putMany(string $uuid, array $values): array
{
foreach ($values as $key => $value) {
Tenant::find($uuid)->put($key, $value);
}
return $values;
}
}

View file

@ -11,7 +11,7 @@ class RedisStorageDriver implements StorageDriver
public function __construct()
{
$this->redis = Redis::connection('tenancy');
$this->redis = Redis::connection(config('tenancy.redis.connection', 'tenancy'));
}
public function identifyTenant(string $domain): array
@ -33,13 +33,11 @@ class RedisStorageDriver implements StorageDriver
*/
public function getTenantById(string $uuid, array $fields = []): array
{
$fields = (array) $fields;
if (! $fields) {
return $this->redis->hgetall("tenants:$uuid");
}
return array_combine($fields, $this->redis->hmget("tenants:$uuid", $fields));
return \array_combine($fields, $this->redis->hmget("tenants:$uuid", $fields));
}
public function getTenantIdByDomain(string $domain): ?string
@ -50,7 +48,7 @@ class RedisStorageDriver implements StorageDriver
public function createTenant(string $domain, string $uuid): array
{
$this->redis->hmset("domains:$domain", 'tenant_id', $uuid);
$this->redis->hmset("tenants:$uuid", 'uuid', json_encode($uuid), 'domain', json_encode($domain));
$this->redis->hmset("tenants:$uuid", 'uuid', \json_encode($uuid), 'domain', \json_encode($domain));
return $this->redis->hgetall("tenants:$uuid");
}
@ -65,7 +63,7 @@ class RedisStorageDriver implements StorageDriver
public function deleteTenant(string $id): bool
{
try {
$domain = json_decode($this->getTenantById($id)['domain']);
$domain = \json_decode($this->getTenantById($id)['domain']);
} catch (\Throwable $th) {
throw new \Exception("No tenant with UUID $id exists.");
}
@ -77,7 +75,7 @@ class RedisStorageDriver implements StorageDriver
public function getAllTenants(array $uuids = []): array
{
$hashes = array_map(function ($hash) {
$hashes = \array_map(function ($hash) {
return "tenants:{$hash}";
}, $uuids);
@ -88,18 +86,17 @@ class RedisStorageDriver implements StorageDriver
if (config('database.redis.client') === 'phpredis') {
$redis_prefix = $this->redis->getOption($this->redis->client()::OPT_PREFIX) ?? $redis_prefix;
$all_keys = $this->redis->scan(null, $redis_prefix . 'tenants:*');
} else {
$all_keys = $this->redis->scan(null, 'MATCH', $redis_prefix . 'tenants:*')[1];
}
$hashes = array_map(function ($key) use ($redis_prefix) {
$all_keys = $this->redis->keys('tenants:*');
$hashes = \array_map(function ($key) use ($redis_prefix) {
// Left strip $redis_prefix from $key
return substr($key, strlen($redis_prefix));
return \substr($key, \strlen($redis_prefix));
}, $all_keys);
}
return array_map(function ($tenant) {
return \array_map(function ($tenant) {
return $this->redis->hgetall($tenant);
}, $hashes);
}

View file

@ -34,9 +34,13 @@ class TenancyServiceProvider extends ServiceProvider
]);
$this->publishes([
__DIR__ . '/config/tenancy.php' => config_path('tenancy.php'),
__DIR__ . '/../assets/config.php' => config_path('tenancy.php'),
], 'config');
$this->publishes([
__DIR__ . '/../assets/migrations/' => database_path('migrations'),
], 'migrations');
$this->loadRoutesFrom(__DIR__ . '/routes.php');
Route::middlewareGroup('tenancy', [
@ -75,7 +79,7 @@ class TenancyServiceProvider extends ServiceProvider
*/
public function register()
{
$this->mergeConfigFrom(__DIR__ . '/config/tenancy.php', 'tenancy');
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
$this->app->bind(StorageDriver::class, $this->app['config']['tenancy.storage_driver']);
$this->app->bind(ServerConfigManager::class, $this->app['config']['tenancy.server.manager']);

102
src/Tenant.php Normal file
View file

@ -0,0 +1,102 @@
<?php
namespace Stancl\Tenancy;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
protected $guarded = [];
protected $primaryKey = 'uuid';
public $incrementing = false;
public $timestamps = false;
/**
* Decoded data from the data column.
*
* @var object
*/
private $dataObject;
public static function dataColumn()
{
return config('tenancy.storage.db.data_column', 'data');
}
public static function customColumns()
{
return config('tenancy.storage.db.custom_columns', []);
}
public function getConnectionName()
{
return config('tenancy.storage.db.connection') ?: config('database.default');
}
public static function getAllTenants(array $uuids)
{
$tenants = $uuids ? static::findMany($uuids) : static::all();
return $tenants->map([__CLASS__, 'decodeData'])->toBase();
}
public function decoded()
{
return static::decodeData($this);
}
/**
* Return a tenant array with data decoded into separate keys.
*
* @param Tenant|array $tenant
* @return array
*/
public static function decodeData($tenant)
{
$tenant = $tenant instanceof self ? (array) $tenant->attributes : $tenant;
$decoded = \json_decode($tenant[$dataColumn = static::dataColumn()], true);
foreach ($decoded as $key => $value) {
$tenant[$key] = $value;
}
// If $tenant[$dataColumn] has been overriden by a value, don't delete the key.
if (! \array_key_exists($dataColumn, $decoded)) {
unset($tenant[$dataColumn]);
}
return $tenant;
}
public function getFromData(string $key)
{
$this->dataArray = $this->dataArray ?? \json_decode($this->{$this->dataColumn()}, true);
return $this->dataArray[$key] ?? null;
}
public function get(string $key)
{
return $this->attributes[$key] ?? $this->getFromData($key) ?? null;
}
/** @todo In v2, this should return an associative array. */
public function getMany(array $keys): array
{
return \array_map([$this, 'get'], $keys);
}
public function put(string $key, $value)
{
if (\array_key_exists($key, $this->customColumns())) {
$this->update([$key => $value]);
} else {
$obj = \json_decode($this->{$this->dataColumn()});
$obj->$key = $value;
$this->update([$this->dataColumn() => \json_encode($obj)]);
}
return $value;
}
}

View file

@ -9,7 +9,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
public function createDatabase(string $name): bool
{
try {
return fclose(fopen(database_path($name), 'w'));
return \fclose(\fopen(database_path($name), 'w'));
} catch (\Throwable $th) {
return false;
}
@ -18,7 +18,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
public function deleteDatabase(string $name): bool
{
try {
return unlink(database_path($name));
return \unlink(database_path($name));
} catch (\Throwable $th) {
return false;
}

View file

@ -23,7 +23,7 @@ final class TenantManager
*
* @var StorageDriver
*/
protected $storage;
public $storage;
/**
* Database manager.
@ -37,7 +37,7 @@ final class TenantManager
*
* @var array
*/
public $tenant;
public $tenant = [];
public function __construct(Application $app, StorageDriver $storage, DatabaseManager $database)
{
@ -64,7 +64,7 @@ final class TenantManager
$tenant = $this->storage->identifyTenant($domain);
if (! $tenant || ! array_key_exists('uuid', $tenant) || ! $tenant['uuid']) {
if (! $tenant || ! \array_key_exists('uuid', $tenant) || ! $tenant['uuid']) {
throw new \Exception("Tenant could not be identified on domain {$domain}.");
}
@ -86,12 +86,15 @@ final class TenantManager
throw new \Exception("Domain $domain is already occupied by tenant $id.");
}
$tenant = $this->jsonDecodeArrayValues($this->storage->createTenant($domain, (string) \Webpatser\Uuid\Uuid::generate(1, $domain)));
$tenant = $this->storage->createTenant($domain, (string) \Webpatser\Uuid\Uuid::generate(1, $domain));
if ($this->useJson()) {
$tenant = $this->jsonDecodeArrayValues($tenant);
}
if ($data) {
$this->put($data, null, $tenant['uuid']);
$tenant = array_merge($tenant, $data);
$tenant = \array_merge($tenant, $data);
}
$this->database->create($this->getDatabaseName($tenant));
@ -115,7 +118,12 @@ final class TenantManager
{
$fields = (array) $fields;
return $this->jsonDecodeArrayValues($this->storage->getTenantById($uuid, $fields));
$tenant = $this->storage->getTenantById($uuid, $fields);
if ($this->useJson()) {
$tenant = $this->jsonDecodeArrayValues($tenant);
}
return $tenant;
}
/**
@ -167,7 +175,7 @@ final class TenantManager
$uuid = $this->getIdByDomain($domain);
if (is_null($uuid)) {
if (\is_null($uuid)) {
throw new \Exception("Tenant with domain $domain could not be identified.");
}
@ -200,7 +208,9 @@ final class TenantManager
*/
public function setTenant(array $tenant): array
{
$tenant = $this->jsonDecodeArrayValues($tenant);
if ($this->useJson()) {
$tenant = $this->jsonDecodeArrayValues($tenant);
}
$this->tenant = $tenant;
@ -227,10 +237,15 @@ final class TenantManager
public function all($uuids = [])
{
$uuids = (array) $uuids;
$tenants = $this->storage->getAllTenants($uuids);
return collect(array_map(function ($tenant_array) {
return $this->jsonDecodeArrayValues($tenant_array);
}, $this->storage->getAllTenants($uuids)));
if ($this->useJson()) {
$tenants = \array_map(function ($tenant_array) {
return $this->jsonDecodeArrayValues($tenant_array);
}, $tenants);
}
return collect($tenants);
}
/**
@ -258,11 +273,16 @@ final class TenantManager
{
$uuid = $uuid ?: $this->tenant['uuid'];
if (\array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] &&
! \is_array($key) && \array_key_exists($key, $this->tenant)) {
return $this->tenant[$key];
}
if (\is_array($key)) {
return $this->jsonDecodeArrayValues($this->storage->getMany($uuid, $key));
}
return json_decode($this->storage->get($uuid, $key), true);
return \json_decode($this->storage->get($uuid, $key), true);
}
/**
@ -275,10 +295,10 @@ final class TenantManager
*/
public function put($key, $value = null, string $uuid = null)
{
if (in_array($key, ['uuid', 'domain'], true) || (
is_array($key) && (
in_array('uuid', array_keys($key), true) ||
in_array('domain', array_keys($key), true)
if (\in_array($key, ['uuid', 'domain'], true) || (
\is_array($key) && (
\in_array('uuid', \array_keys($key), true) ||
\in_array('domain', \array_keys($key), true)
)
)) {
throw new CannotChangeUuidOrDomainException;
@ -299,7 +319,7 @@ final class TenantManager
}
if (! \is_null($value)) {
return $target[$key] = json_decode($this->storage->put($uuid, $key, json_encode($value)), true);
return $target[$key] = \json_decode($this->storage->put($uuid, $key, \json_encode($value)), true);
}
if (! \is_array($key)) {
@ -308,7 +328,7 @@ final class TenantManager
foreach ($key as $k => $v) {
$target[$k] = $v;
$key[$k] = json_encode($v);
$key[$k] = \json_encode($v);
}
return $this->jsonDecodeArrayValues($this->storage->putMany($uuid, $key));
@ -329,18 +349,28 @@ final class TenantManager
protected function jsonDecodeArrayValues(array $array)
{
array_walk($array, function (&$value, $key) {
$value = json_decode($value, true);
\array_walk($array, function (&$value, $key) {
$value = \json_decode($value, true);
});
return $array;
}
public function useJson()
{
if (\property_exists($this->storage, 'useJson') && $this->storage->useJson === false) {
return false;
}
return true;
}
/**
* Return the identified tenant's attribute(s).
*
* @param string $attribute
* @return mixed
* @todo Deprecate this in v2.
*/
public function __invoke($attribute)
{
@ -348,6 +378,6 @@ final class TenantManager
return $this->tenant;
}
return $this->tenant[(string) $attribute];
return $this->get((string) $attribute);
}
}

View file

@ -10,7 +10,7 @@ class TenantRouteServiceProvider extends RouteServiceProvider
public function map()
{
if (! \in_array(request()->getHost(), $this->app['config']['tenancy.exempt_domains'] ?? [])
&& file_exists(base_path('routes/tenant.php'))) {
&& \file_exists(base_path('routes/tenant.php'))) {
Route::middleware(['web', 'tenancy'])
->namespace($this->app['config']['tenant_route_namespace'] ?? 'App\Http\Controllers')
->group(base_path('routes/tenant.php'));

View file

@ -138,7 +138,7 @@ trait BootstrapsTenancy
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
$old['disks'][$disk] = Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($root = str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) {
if ($root = \str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) {
Storage::disk($disk)->getAdapter()->setPathPrefix($root);
} else {
$root = $this->app['config']["filesystems.disks.{$disk}.root"];

View file

@ -8,7 +8,7 @@ trait HasATenantsOption
{
protected function getOptions()
{
return array_merge([
return \array_merge([
['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null],
], parent::getOptions());
}

View file

@ -78,7 +78,7 @@ trait TenantManagerEvents
*/
public function event(string $name): Collection
{
return array_reduce($this->listeners[$name], function ($prevents, $listener) {
return \array_reduce($this->listeners[$name], function ($prevents, $listener) {
return $prevents->merge($listener($this) ?? []);
}, collect([]));
}

View file

@ -1,47 +0,0 @@
<?php
return [
'storage_driver' => 'Stancl\Tenancy\StorageDrivers\RedisStorageDriver',
'tenant_route_namespace' => 'App\Http\Controllers',
'exempt_domains' => [
// 'localhost',
],
'database' => [
'based_on' => 'mysql',
'prefix' => 'tenant',
'suffix' => '',
],
'redis' => [
'tenancy' => false,
'prefix_base' => 'tenant',
'prefixed_connections' => [
'default',
'cache',
],
],
'cache' => [
'tag_base' => 'tenant',
],
'filesystem' => [
'suffix_base' => 'tenant',
// Disks which should be suffixed with the suffix_base + tenant UUID.
'disks' => [
'local',
'public',
// 's3',
],
'root_override' => [
// Disks whose roots should be overriden after storage_path() is suffixed.
'local' => '%storage_path%/app/',
'public' => '%storage_path%/app/public/',
],
],
'database_managers' => [
'sqlite' => 'Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager',
'mysql' => 'Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager',
'pgsql' => 'Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager',
],
'queue_database_creation' => false,
'queue_database_deletion' => false,
'database_name_key' => null,
];

View file

@ -2,7 +2,7 @@
use Stancl\Tenancy\TenantManager;
if (! function_exists('tenancy')) {
if (! \function_exists('tenancy')) {
function tenancy($key = null)
{
if ($key) {
@ -13,14 +13,14 @@ if (! function_exists('tenancy')) {
}
}
if (! function_exists('tenant')) {
if (! \function_exists('tenant')) {
function tenant($key = null)
{
return tenancy($key);
}
}
if (! function_exists('tenant_asset')) {
if (! \function_exists('tenant_asset')) {
function tenant_asset($asset)
{
return route('stancl.tenancy.asset', ['asset' => $asset]);