diff --git a/assets/config.php b/assets/config.php index 564d3d55..7183d2b7 100644 --- a/assets/config.php +++ b/assets/config.php @@ -3,9 +3,9 @@ declare(strict_types=1); return [ - 'storage_driver' => 'Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver', + 'storage_driver' => 'Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver', 'storage' => [ - 'db' => [ // Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver + 'db' => [ // Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver 'data_column' => 'data', 'custom_columns' => [ // 'plan', @@ -60,6 +60,7 @@ return [ 'cache' => 'Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper', 'filesystem' => 'Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper', 'redis' => 'Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper', + 'queue' => 'Stancl\Tenancy\TenancyBoostrappers\QueueTenancyBootstrapper', ], 'features' => [ // Features are classes that provide additional functionality diff --git a/assets/migrations/2019_08_08_000000_create_tenants_table.php b/assets/migrations/2019_08_08_000000_create_tenants_table.php index fc9aa876..8d4b7b9e 100644 --- a/assets/migrations/2019_08_08_000000_create_tenants_table.php +++ b/assets/migrations/2019_08_08_000000_create_tenants_table.php @@ -16,10 +16,8 @@ class CreateTenantsTable extends Migration public function up() { Schema::create('tenants', function (Blueprint $table) { - $table->string('uuid', 36)->primary(); // don't change this - $table->string('domain', 255)->index(); // don't change this - - // your indexed columns go here + $table->string('id', 36)->primary(); // 36 characters is the default uuid length + // your custom, indexed columns go here $table->json('data'); }); diff --git a/assets/migrations/2019_09_15_000000_create_domains_table.php b/assets/migrations/2019_09_15_000000_create_domains_table.php new file mode 100644 index 00000000..815acd76 --- /dev/null +++ b/assets/migrations/2019_09_15_000000_create_domains_table.php @@ -0,0 +1,33 @@ +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'); + } +} diff --git a/src/Contracts/StorageDriver.php b/src/Contracts/StorageDriver.php index a974fe04..834afab5 100644 --- a/src/Contracts/StorageDriver.php +++ b/src/Contracts/StorageDriver.php @@ -48,6 +48,14 @@ interface StorageDriver */ 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. * diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 9ddc1dc3..5b0db28d 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Illuminate\Foundation\Application; +use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator; @@ -23,6 +24,7 @@ class DatabaseManager public function __construct(Application $app, BaseDatabaseManager $database) { + $this->app = $app; $this->database = $database; $this->originalDefaultConnectionName = $app['config']['database.default']; } @@ -35,9 +37,8 @@ class DatabaseManager */ public function connect(Tenant $tenant) { - $connection = 'tenant'; // todo tenant-specific connections - $this->createTenantConnection($tenant->getDatabaseName(), $connection); - $this->switchConnection($connection); + $this->createTenantConnection($tenant); + $this->switchConnection($tenant->getConnectionName()); } /** @@ -53,13 +54,13 @@ class DatabaseManager /** * Create the tenant database connection. * - * @param string $databaseName - * @param string $connectionName + * @param Tenant $tenant * @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. $based_on = $this->app['config']['tenancy.database.based_on'] ?? $this->originalDefaultConnectionName; @@ -108,9 +109,9 @@ class DatabaseManager $manager = $this->getTenantDatabaseManager($tenant); if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { - QueuedTenantDatabaseCreator::dispatch($this->app[$manager], $database, 'create'); + QueuedTenantDatabaseCreator::dispatch($manager, $database, 'create'); } else { - return $this->app[$manager]->createDatabase($database); + return $manager->createDatabase($database); } } @@ -120,16 +121,16 @@ class DatabaseManager $manager = $this->getTenantDatabaseManager($tenant); if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { - QueuedTenantDatabaseCreator::dispatch($this->app[$manager], $database, 'delete'); + QueuedTenantDatabaseCreator::dispatch($manager, $database, 'delete'); } 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 - $driver = $this->getDriver($connection); + $this->createTenantConnection($tenant); + $driver = $this->getDriver($tenant->getConnectionName()); $databaseManagers = $this->app['config']['tenancy.database_managers']; @@ -137,6 +138,6 @@ class DatabaseManager throw new DatabaseManagerNotRegisteredException($driver); } - return $databaseManagers[$driver]; + return $this->app[$databaseManagers[$driver]]; } } diff --git a/src/StorageDrivers/Database/DatabaseStorageDriver.php b/src/StorageDrivers/Database/DatabaseStorageDriver.php index 80984781..a7396260 100644 --- a/src/StorageDrivers/Database/DatabaseStorageDriver.php +++ b/src/StorageDrivers/Database/DatabaseStorageDriver.php @@ -4,18 +4,31 @@ declare(strict_types=1); namespace Stancl\Tenancy\StorageDrivers\Database; +use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException; 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; class DatabaseStorageDriver implements StorageDriver { // 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 { $id = $this->getTenantIdByDomain($domain); @@ -23,16 +36,16 @@ class DatabaseStorageDriver implements StorageDriver throw new TenantCouldNotBeIdentifiedException($domain); } - return $this->find($id); + return $this->findById($id); } public function findById(string $id): Tenant { 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 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 { return Domains::where('domain', $domain)->first()->tenant_id ?? null; @@ -64,9 +84,8 @@ class DatabaseStorageDriver implements StorageDriver public function updateTenant(Tenant $tenant): void { - // todo - // 1. update storage - // 2. update domains + Tenant::find($tenant->id)->putMany($tenant->data); + // todo update domains } public function deleteTenant(Tenant $tenant): void @@ -93,7 +112,7 @@ class DatabaseStorageDriver implements StorageDriver */ protected function tenant() { - return $this->app[Tenant::class]; + return $this->tenant ?? $this->app[Tenant::class]; } public function get(string $key, Tenant $tenant = null) diff --git a/src/StorageDrivers/Database/DomainModel.php b/src/StorageDrivers/Database/DomainModel.php index b4351248..1282ae05 100644 --- a/src/StorageDrivers/Database/DomainModel.php +++ b/src/StorageDrivers/Database/DomainModel.php @@ -15,6 +15,7 @@ class DomainModel extends Model protected $primaryKey = 'id'; public $incrementing = false; public $timestamps = false; + public $table = 'domains'; public function getConnectionName() { diff --git a/src/StorageDrivers/Database/TenantModel.php b/src/StorageDrivers/Database/TenantModel.php index 036b07a6..f1360b63 100644 --- a/src/StorageDrivers/Database/TenantModel.php +++ b/src/StorageDrivers/Database/TenantModel.php @@ -15,6 +15,7 @@ class TenantModel extends Model protected $primaryKey = 'id'; public $incrementing = false; public $timestamps = false; + public $table = 'tenants'; public static function dataColumn() { diff --git a/src/StorageDrivers/RedisStorageDriver.php b/src/StorageDrivers/RedisStorageDriver.php index b8a7ddde..a8192ec3 100644 --- a/src/StorageDrivers/RedisStorageDriver.php +++ b/src/StorageDrivers/RedisStorageDriver.php @@ -21,6 +21,9 @@ class RedisStorageDriver implements StorageDriver /** @var Redis */ protected $redis; + /** @var Tenant The default tenant. */ + protected $tenant; + public function __construct(Application $app, Redis $redis) { $this->app = $app; @@ -34,10 +37,17 @@ class RedisStorageDriver implements StorageDriver */ 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 } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 0cbb6bf0..eb703c3c 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -64,14 +64,13 @@ class TenancyServiceProvider extends ServiceProvider $this->app->singleton($bootstrapper); } - // todo are these necessary? - $this->app->singleton(Migrate::class, function ($app) { + $this->app->singleton(Commands\Migrate::class, function ($app) { 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]); }); - $this->app->singleton(Seed::class, function ($app) { + $this->app->singleton(Commands\Seed::class, function ($app) { return new Commands\Seed($app['db'], $app[DatabaseManager::class]); }); diff --git a/src/Tenant.php b/src/Tenant.php index 9b7538ad..56815cbd 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -5,10 +5,13 @@ declare(strict_types=1); namespace Stancl\Tenancy; use ArrayAccess; +use Illuminate\Foundation\Application; use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Exceptions\TenantStorageException; +// todo write tests for updating the tenant + /** * @internal Class is subject to breaking changes in minor and patch versions. */ @@ -30,6 +33,9 @@ class Tenant implements ArrayAccess */ public $domains = []; + /** @var Application */ + protected $app; + /** @var StorageDriver */ protected $storage; @@ -46,16 +52,24 @@ class Tenant implements ArrayAccess */ 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->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 @@ -132,14 +146,14 @@ class Tenant implements ArrayAccess public function save(): self { - if (! $this->id) { + if (! isset($this->data['id'])) { $this->generateId(); } if ($this->persisted) { - $this->manager->createTenant($this); - } else { $this->manager->updateTenant($this); + } else { + $this->manager->createTenant($this); } $this->persisted = true; @@ -178,7 +192,12 @@ class Tenant implements ArrayAccess 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) { 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) { - $pairs[$key] = $this->data[$key]; + $pairs[$key] = $this->data[$key] ?? null; return $pairs; }, []); @@ -201,11 +222,14 @@ class Tenant implements ArrayAccess return $this->storage->getMany($keys); } - if (! isset($this->data[$keys])) { - $this->data[$keys] = $this->storage->get($keys); + // single key + $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 @@ -227,8 +251,13 @@ class Tenant implements ArrayAccess 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; } } diff --git a/src/TenantManager.php b/src/TenantManager.php index 8a52f435..16ddef54 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -210,9 +210,10 @@ class TenantManager protected function bootstrapFeatures(): self { - foreach ($this->app['config']['tenancy.features'] as $feature) { - $this->app[$feature]->bootstrap($this); - } + // todo this doesn't work + // foreach ($this->app['config']['tenancy.features'] as $feature) { + // $this->app[$feature]->bootstrap($this); + // } return $this; } @@ -225,7 +226,7 @@ class TenantManager */ 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 diff --git a/tests/TestCase.php b/tests/TestCase.php index b0657015..521cc7d2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,8 +5,9 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Illuminate\Support\Facades\Redis; -use Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver; +use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver; use Stancl\Tenancy\StorageDrivers\RedisStorageDriver; +use Stancl\Tenancy\Tenant; 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']); - - parent::tearDown(); + Tenant::new()->withDomains($domains)->save(); } - public function createTenant($domain = 'localhost') - { - tenant()->create($domain); - } - - public function initTenancy($domain = 'localhost') + public function initTenancy($domain = 'test.localhost') { return tenancy()->init($domain); } @@ -112,13 +106,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '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') { $app['config']->set([ 'tenancy.storage_driver' => DatabaseStorageDriver::class, ]); - tenancy()->storage = $app->make(DatabaseStorageDriver::class); + // tenancy()->storage = $app->make(DatabaseStorageDriver::class); // todo this shouldn't be necessary } }