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

Tenant-specific connections, some work to get tests running

This commit is contained in:
Samuel Štancl 2019-09-15 17:44:26 +02:00
parent e25a01a997
commit c65b6839ff
13 changed files with 162 additions and 67 deletions

View file

@ -3,9 +3,9 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'storage_driver' => 'Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver', 'storage_driver' => 'Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver',
'storage' => [ 'storage' => [
'db' => [ // Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver 'db' => [ // Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver
'data_column' => 'data', 'data_column' => 'data',
'custom_columns' => [ 'custom_columns' => [
// 'plan', // 'plan',
@ -60,6 +60,7 @@ return [
'cache' => 'Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper', 'cache' => 'Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper',
'filesystem' => 'Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper', 'filesystem' => 'Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper',
'redis' => 'Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper', 'redis' => 'Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper',
'queue' => 'Stancl\Tenancy\TenancyBoostrappers\QueueTenancyBootstrapper',
], ],
'features' => [ 'features' => [
// Features are classes that provide additional functionality // Features are classes that provide additional functionality

View file

@ -16,10 +16,8 @@ class CreateTenantsTable extends Migration
public function up() public function up()
{ {
Schema::create('tenants', function (Blueprint $table) { Schema::create('tenants', function (Blueprint $table) {
$table->string('uuid', 36)->primary(); // don't change this $table->string('id', 36)->primary(); // 36 characters is the default uuid length
$table->string('domain', 255)->index(); // don't change this // your custom, indexed columns go here
// your indexed columns go here
$table->json('data'); $table->json('data');
}); });

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDomainsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('domains', function (Blueprint $table) {
$table->string('tenant_id', 36)->primary(); // 36 characters is the default uuid length
$table->string('domain', 255)->index(); // don't change this
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('domains');
}
}

View file

@ -48,6 +48,14 @@ interface StorageDriver
*/ */
public function ensureTenantCanBeCreated(Tenant $tenant): void; public function ensureTenantCanBeCreated(Tenant $tenant): void;
/**
* Set default tenant (will be used for get/put when no tenant is supplied).
*
* @param Tenant $tenant
* @return self
*/
public function withDefaultTenant(Tenant $tenant);
/** /**
* Get a value from storage. * Get a value from storage.
* *

View file

@ -6,6 +6,7 @@ namespace Stancl\Tenancy;
use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator; use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator;
@ -23,6 +24,7 @@ class DatabaseManager
public function __construct(Application $app, BaseDatabaseManager $database) public function __construct(Application $app, BaseDatabaseManager $database)
{ {
$this->app = $app;
$this->database = $database; $this->database = $database;
$this->originalDefaultConnectionName = $app['config']['database.default']; $this->originalDefaultConnectionName = $app['config']['database.default'];
} }
@ -35,9 +37,8 @@ class DatabaseManager
*/ */
public function connect(Tenant $tenant) public function connect(Tenant $tenant)
{ {
$connection = 'tenant'; // todo tenant-specific connections $this->createTenantConnection($tenant);
$this->createTenantConnection($tenant->getDatabaseName(), $connection); $this->switchConnection($tenant->getConnectionName());
$this->switchConnection($connection);
} }
/** /**
@ -53,13 +54,13 @@ class DatabaseManager
/** /**
* Create the tenant database connection. * Create the tenant database connection.
* *
* @param string $databaseName * @param Tenant $tenant
* @param string $connectionName
* @return void * @return void
*/ */
public function createTenantConnection(string $databaseName, string $connectionName = null) public function createTenantConnection(Tenant $tenant)
{ {
$connectionName = $connectionName ?? 'tenant'; // todo $databaseName = $tenant->getDatabaseName();
$connectionName = $tenant->getConnectionName();
// Create the database connection. // Create the database connection.
$based_on = $this->app['config']['tenancy.database.based_on'] ?? $this->originalDefaultConnectionName; $based_on = $this->app['config']['tenancy.database.based_on'] ?? $this->originalDefaultConnectionName;
@ -108,9 +109,9 @@ class DatabaseManager
$manager = $this->getTenantDatabaseManager($tenant); $manager = $this->getTenantDatabaseManager($tenant);
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
QueuedTenantDatabaseCreator::dispatch($this->app[$manager], $database, 'create'); QueuedTenantDatabaseCreator::dispatch($manager, $database, 'create');
} else { } else {
return $this->app[$manager]->createDatabase($database); return $manager->createDatabase($database);
} }
} }
@ -120,16 +121,16 @@ class DatabaseManager
$manager = $this->getTenantDatabaseManager($tenant); $manager = $this->getTenantDatabaseManager($tenant);
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
QueuedTenantDatabaseCreator::dispatch($this->app[$manager], $database, 'delete'); QueuedTenantDatabaseCreator::dispatch($manager, $database, 'delete');
} else { } else {
return $this->app[$manager]->deleteDatabase($database); return $manager->deleteDatabase($database);
} }
} }
protected function getTenantDatabaseManager(Tenant $tenant) protected function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
{ {
$connection = $tenant->getConnectionName(); // todo $this->createTenantConnection($tenant);
$driver = $this->getDriver($connection); $driver = $this->getDriver($tenant->getConnectionName());
$databaseManagers = $this->app['config']['tenancy.database_managers']; $databaseManagers = $this->app['config']['tenancy.database_managers'];
@ -137,6 +138,6 @@ class DatabaseManager
throw new DatabaseManagerNotRegisteredException($driver); throw new DatabaseManagerNotRegisteredException($driver);
} }
return $databaseManagers[$driver]; return $this->app[$databaseManagers[$driver]];
} }
} }

View file

@ -4,18 +4,31 @@ declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database; namespace Stancl\Tenancy\StorageDrivers\Database;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException; use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException; use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains; use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains;
use Stancl\Tenancy\StorageDrivers\Database\Tenants as Tenants; use Stancl\Tenancy\StorageDrivers\Database\TenantModel as Tenants;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
class DatabaseStorageDriver implements StorageDriver class DatabaseStorageDriver implements StorageDriver
{ {
// todo write tests verifying that data is decoded and added to the array // todo write tests verifying that data is decoded and added to the array
/** @var Application */
protected $app;
/** @var Tenant The default tenant. */
protected $tenant;
public function __construct(Application $app)
{
$this->app = $app;
}
public function findByDomain(string $domain): Tenant public function findByDomain(string $domain): Tenant
{ {
$id = $this->getTenantIdByDomain($domain); $id = $this->getTenantIdByDomain($domain);
@ -23,16 +36,16 @@ class DatabaseStorageDriver implements StorageDriver
throw new TenantCouldNotBeIdentifiedException($domain); throw new TenantCouldNotBeIdentifiedException($domain);
} }
return $this->find($id); return $this->findById($id);
} }
public function findById(string $id): Tenant public function findById(string $id): Tenant
{ {
return Tenant::fromStorage(Tenants::find($id)->decoded()) return Tenant::fromStorage(Tenants::find($id)->decoded())
->withDomains(Domains::where('tenant_id', $id)->all()->only('domain')->toArray()); ->withDomains(Domains::where('tenant_id', $id)->get()->only('domain')->toArray());
} }
public function ensureTenantCanBeCreated(Tenant $tenant) public function ensureTenantCanBeCreated(Tenant $tenant): void
{ {
// todo test this // todo test this
if (Tenants::find($tenant->id)) { if (Tenants::find($tenant->id)) {
@ -44,6 +57,13 @@ class DatabaseStorageDriver implements StorageDriver
} }
} }
public function withDefaultTenant(Tenant $tenant): self
{
$this->tenant = $tenant;
return $this;
}
public function getTenantIdByDomain(string $domain): ?string public function getTenantIdByDomain(string $domain): ?string
{ {
return Domains::where('domain', $domain)->first()->tenant_id ?? null; return Domains::where('domain', $domain)->first()->tenant_id ?? null;
@ -64,9 +84,8 @@ class DatabaseStorageDriver implements StorageDriver
public function updateTenant(Tenant $tenant): void public function updateTenant(Tenant $tenant): void
{ {
// todo Tenant::find($tenant->id)->putMany($tenant->data);
// 1. update storage // todo update domains
// 2. update domains
} }
public function deleteTenant(Tenant $tenant): void public function deleteTenant(Tenant $tenant): void
@ -93,7 +112,7 @@ class DatabaseStorageDriver implements StorageDriver
*/ */
protected function tenant() protected function tenant()
{ {
return $this->app[Tenant::class]; return $this->tenant ?? $this->app[Tenant::class];
} }
public function get(string $key, Tenant $tenant = null) public function get(string $key, Tenant $tenant = null)

View file

@ -15,6 +15,7 @@ class DomainModel extends Model
protected $primaryKey = 'id'; protected $primaryKey = 'id';
public $incrementing = false; public $incrementing = false;
public $timestamps = false; public $timestamps = false;
public $table = 'domains';
public function getConnectionName() public function getConnectionName()
{ {

View file

@ -15,6 +15,7 @@ class TenantModel extends Model
protected $primaryKey = 'id'; protected $primaryKey = 'id';
public $incrementing = false; public $incrementing = false;
public $timestamps = false; public $timestamps = false;
public $table = 'tenants';
public static function dataColumn() public static function dataColumn()
{ {

View file

@ -21,6 +21,9 @@ class RedisStorageDriver implements StorageDriver
/** @var Redis */ /** @var Redis */
protected $redis; protected $redis;
/** @var Tenant The default tenant. */
protected $tenant;
public function __construct(Application $app, Redis $redis) public function __construct(Application $app, Redis $redis)
{ {
$this->app = $app; $this->app = $app;
@ -34,10 +37,17 @@ class RedisStorageDriver implements StorageDriver
*/ */
protected function tenant() protected function tenant()
{ {
return $this->app[Tenant::class]; return $this->tenant ?? $this->app[Tenant::class];
} }
public function ensureTenantCanBeCreated(Tenant $tenant) public function withDefaultTenant(Tenant $tenant): self
{
$this->tenant = $tenant;
return $this;
}
public function ensureTenantCanBeCreated(Tenant $tenant): void
{ {
// todo // todo
} }

View file

@ -64,14 +64,13 @@ class TenancyServiceProvider extends ServiceProvider
$this->app->singleton($bootstrapper); $this->app->singleton($bootstrapper);
} }
// todo are these necessary? $this->app->singleton(Commands\Migrate::class, function ($app) {
$this->app->singleton(Migrate::class, function ($app) {
return new Commands\Migrate($app['migrator'], $app[DatabaseManager::class]); return new Commands\Migrate($app['migrator'], $app[DatabaseManager::class]);
}); });
$this->app->singleton(Rollback::class, function ($app) { $this->app->singleton(Commands\Rollback::class, function ($app) {
return new Commands\Rollback($app['migrator'], $app[DatabaseManager::class]); return new Commands\Rollback($app['migrator'], $app[DatabaseManager::class]);
}); });
$this->app->singleton(Seed::class, function ($app) { $this->app->singleton(Commands\Seed::class, function ($app) {
return new Commands\Seed($app['db'], $app[DatabaseManager::class]); return new Commands\Seed($app['db'], $app[DatabaseManager::class]);
}); });

View file

@ -5,10 +5,13 @@ declare(strict_types=1);
namespace Stancl\Tenancy; namespace Stancl\Tenancy;
use ArrayAccess; use ArrayAccess;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
use Stancl\Tenancy\Exceptions\TenantStorageException; use Stancl\Tenancy\Exceptions\TenantStorageException;
// todo write tests for updating the tenant
/** /**
* @internal Class is subject to breaking changes in minor and patch versions. * @internal Class is subject to breaking changes in minor and patch versions.
*/ */
@ -30,6 +33,9 @@ class Tenant implements ArrayAccess
*/ */
public $domains = []; public $domains = [];
/** @var Application */
protected $app;
/** @var StorageDriver */ /** @var StorageDriver */
protected $storage; protected $storage;
@ -46,16 +52,24 @@ class Tenant implements ArrayAccess
*/ */
protected $persisted = false; protected $persisted = false;
public function __construct(StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator) public function __construct(Application $app, StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator)
{ {
$this->storage = $storage; $this->app = $app;
$this->storage = $storage->withDefaultTenant($this);
$this->manager = $tenantManager; $this->manager = $tenantManager;
$this->idGenerator = $idGenerator; $this->idGenerator = $idGenerator;
} }
public static function new(): self public static function new(Application $app = null): self
{ {
return app(static::class); $app = $app ?? app();
return new static(
$app,
$app[StorageDriver::class],
$app[TenantManager::class],
$app[UniqueIdentifierGenerator::class]
);
} }
public static function fromStorage(array $data): self public static function fromStorage(array $data): self
@ -132,14 +146,14 @@ class Tenant implements ArrayAccess
public function save(): self public function save(): self
{ {
if (! $this->id) { if (! isset($this->data['id'])) {
$this->generateId(); $this->generateId();
} }
if ($this->persisted) { if ($this->persisted) {
$this->manager->createTenant($this);
} else {
$this->manager->updateTenant($this); $this->manager->updateTenant($this);
} else {
$this->manager->createTenant($this);
} }
$this->persisted = true; $this->persisted = true;
@ -178,7 +192,12 @@ class Tenant implements ArrayAccess
public function getDatabaseName() public function getDatabaseName()
{ {
return $this['_tenancy_db_name'] ?? $this->app['config']['tenancy.database.prefix'] . $this->uuid . $this->app['config']['tenancy.database.suffix']; return $this['_tenancy_db_name'] ?? ($this->app['config']['tenancy.database.prefix'] . $this->id . $this->app['config']['tenancy.database.suffix']);
}
public function getConnectionName()
{
return $this['_tenancy_db_connection'] ?? 'tenant';
} }
/** /**
@ -190,9 +209,11 @@ class Tenant implements ArrayAccess
public function get($keys) public function get($keys)
{ {
if (is_array($keys)) { if (is_array($keys)) {
if (array_intersect(array_keys($this->data), $keys)) { // if all keys are present in cache if ((array_intersect(array_keys($this->data), $keys) === $keys) ||
! $this->persisted) { // if all keys are present in cache
return array_reduce($keys, function ($pairs, $key) { return array_reduce($keys, function ($pairs, $key) {
$pairs[$key] = $this->data[$key]; $pairs[$key] = $this->data[$key] ?? null;
return $pairs; return $pairs;
}, []); }, []);
@ -201,11 +222,14 @@ class Tenant implements ArrayAccess
return $this->storage->getMany($keys); return $this->storage->getMany($keys);
} }
if (! isset($this->data[$keys])) { // single key
$this->data[$keys] = $this->storage->get($keys); $key = $keys;
if (! isset($this->data[$key]) && $this->persisted) {
$this->data[$key] = $this->storage->get($key);
} }
return $this->data[$keys]; return $this->data[$key];
} }
public function put($key, $value = null): self public function put($key, $value = null): self
@ -227,8 +251,13 @@ class Tenant implements ArrayAccess
return $this; return $this;
} }
public function __get($name) public function __get($key)
{ {
return $this->get($name); return $this->get($key);
}
public function __set($key, $value)
{
$this->data[$key] = $value;
} }
} }

View file

@ -210,9 +210,10 @@ class TenantManager
protected function bootstrapFeatures(): self protected function bootstrapFeatures(): self
{ {
foreach ($this->app['config']['tenancy.features'] as $feature) { // todo this doesn't work
$this->app[$feature]->bootstrap($this); // foreach ($this->app['config']['tenancy.features'] as $feature) {
} // $this->app[$feature]->bootstrap($this);
// }
return $this; return $this;
} }
@ -225,7 +226,7 @@ class TenantManager
*/ */
public function tenancyBootstrappers($except = []): array public function tenancyBootstrappers($except = []): array
{ {
return array_key_diff($this->app['config']['tenancy.bootstrappers'], $except); return array_diff_key($this->app['config']['tenancy.bootstrappers'], $except);
} }
public function shouldMigrateAfterCreation(): bool public function shouldMigrateAfterCreation(): bool

View file

@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver; use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
use Stancl\Tenancy\StorageDrivers\RedisStorageDriver; use Stancl\Tenancy\StorageDrivers\RedisStorageDriver;
use Stancl\Tenancy\Tenant;
abstract class TestCase extends \Orchestra\Testbench\TestCase abstract class TestCase extends \Orchestra\Testbench\TestCase
{ {
@ -40,19 +41,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
} }
} }
protected function tearDown(): void public function createTenant($domains = ['test.localhost'])
{ {
// config(['database.default' => 'central']); Tenant::new()->withDomains($domains)->save();
parent::tearDown();
} }
public function createTenant($domain = 'localhost') public function initTenancy($domain = 'test.localhost')
{
tenant()->create($domain);
}
public function initTenancy($domain = 'localhost')
{ {
return tenancy()->init($domain); return tenancy()->init($domain);
} }
@ -112,13 +106,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'tenancy.storage_driver' => RedisStorageDriver::class, 'tenancy.storage_driver' => RedisStorageDriver::class,
]); ]);
tenancy()->storage = $app->make(RedisStorageDriver::class); // tenancy()->storage = $app->make(RedisStorageDriver::class); // todo this shouldn't be necessary
} elseif (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'db') { } elseif (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'db') {
$app['config']->set([ $app['config']->set([
'tenancy.storage_driver' => DatabaseStorageDriver::class, 'tenancy.storage_driver' => DatabaseStorageDriver::class,
]); ]);
tenancy()->storage = $app->make(DatabaseStorageDriver::class); // tenancy()->storage = $app->make(DatabaseStorageDriver::class); // todo this shouldn't be necessary
} }
} }