diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php new file mode 100644 index 00000000..f3e230b4 --- /dev/null +++ b/assets/TenancyServiceProvider.stub.php @@ -0,0 +1,78 @@ + [ + JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, // triggers DatabaseMigrated event + SeedDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->queue(true), + ], + DatabaseCreated::class => [], + DatabaseMigrated::class => [], + DatabaseSeeded::class => [], + TenantDeleted::class => [ + JobPipeline::make([ + DeleteDatabase::class, + ])->send(function (TenantDeleted $event) { + return $event->tenant; + })->queue(true), + // DeleteStorage::class, + ], + DatabaseDeleted::class => [], + ]; + } + + public function register() + { + // + } + + public function boot() + { + $this->bootEvents(); + + // + } + + protected function bootEvents() + { + foreach ($this->events() as $event => $listeners) { + foreach (array_unique($listeners) as $listener) { + // Technically, the string|Closure typehint is not enforced by + // Laravel, but for type correctness, we wrap callables in + // simple Closures, to match Laravel's docblock typehint. + if (is_callable($listener) && !$listener instanceof Closure) { + $listener = function ($event) use ($listener) { + $listener($event); + }; + } + + Event::listen($event, $listener); + } + } + } +} \ No newline at end of file diff --git a/assets/config.php b/assets/config.php index 1ec13a91..8a266b83 100644 --- a/assets/config.php +++ b/assets/config.php @@ -2,68 +2,36 @@ declare(strict_types=1); +use Stancl\Tenancy\Database\Models\Tenant; + return [ - /** - * Storage drivers are used to store information about your tenants. - * They hold the Tenant Storage data and keeps track of domains. - */ - 'storage_driver' => 'db', - 'storage_drivers' => [ - /** - * The majority of applications will want to use this storage driver. - * The information about tenants is persisted in a relational DB - * like MySQL or PostgreSQL. The only downside is performance. - * - * A database connection to the central database has to be established on each - * request, to identify the tenant based on the domain. This takes three DB - * queries. Then, the connection to the tenant database is established. - * - * Note: From v2.3.0, the performance of the DB storage driver can be improved - * by a lot by using Cached Tenant Lookup. Be sure to enable that if you're - * using this storage driver. Enabling that feature can completely avoid - * querying the central database to identify build the Tenant object. - */ - 'db' => [ - 'driver' => Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver::class, - 'data_column' => 'data', - 'custom_columns' => [ - // 'plan', - ], + 'tenant_model' => Tenant::class, + 'internal_prefix' => 'tenancy_', - /** - * Your central database connection. Set to null to use the default one. - * - * Note: It's recommended to create a designated central connection, - * to let you easily use it in your app, e.g. via the DB facade. - */ - 'connection' => null, + 'central_connection' => 'central', + 'template_tenant_connection' => null, - 'table_names' => [ - 'tenants' => 'tenants', - 'domains' => 'domains', - ], + 'id_generator' => Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator::class, - /** - * Here you can enable the Cached Tenant Lookup. - * - * You can specify what cache store should be used to cache the tenant resolution. - * Set to string with a specific cache store name, or to null to disable cache. - */ - 'cache_store' => null, - 'cache_ttl' => 3600, // seconds + 'custom_columns' => [ + // + ], + + + 'storage' => [ + 'data_column' => 'data', + 'custom_columns' => [ + // 'plan', ], /** - * The Redis storage driver is much more performant than the database driver. - * However, by default, Redis is a not a durable data storage. It works well for ephemeral data - * like cache, but to hold critical data, it needs to be configured in a way that guarantees - * that data will be persisted permanently. Specifically, you want to enable both AOF and - * RDB. Read this here: https://tenancy.samuelstancl.me/docs/v2/storage-drivers/#redis. + * Here you can enable the Cached Tenant Lookup. + * + * You can specify what cache store should be used to cache the tenant resolution. + * Set to string with a specific cache store name, or to null to disable cache. */ - 'redis' => [ - 'driver' => Stancl\Tenancy\StorageDrivers\RedisStorageDriver::class, - 'connection' => 'tenancy', - ], + 'cache_store' => null, // env('CACHE_DRIVER') + 'cache_ttl' => 3600, // seconds ], /** @@ -108,6 +76,7 @@ return [ */ 'prefix' => 'tenant', 'suffix' => '', + // todo get rid of this stuff, just set the closure instead ], /** @@ -237,55 +206,30 @@ return [ */ 'home_url' => '/app', - /** - * Automatically create a database when creating a tenant. - */ - 'create_database' => true, - /** * Should tenant databases be created asynchronously in a queued job. */ - 'queue_database_creation' => false, + 'queue_database_creation' => false, // todo make this a static property - /** - * Should tenant migrations be ran after the tenant's database is created. - */ - 'migrate_after_creation' => false, 'migration_parameters' => [ '--force' => true, // Set this to true to be able to run migrations in production // '--path' => [database_path('migrations/tenant')], // If you need to customize paths to tenant migrations ], - /** - * Should tenant databases be automatically seeded after they're created & migrated. - */ - 'seed_after_migration' => false, // should the seeder run after automatic migration 'seeder_parameters' => [ '--class' => 'DatabaseSeeder', // root seeder class, e.g.: 'DatabaseSeeder' // '--force' => true, ], - /** - * Automatically delete the tenant's database after the tenant is deleted. - * - * This will save space but permanently delete data which you might want to keep. - */ - 'delete_database_after_tenant_deletion' => false, - /** * Should tenant databases be deleted asynchronously in a queued job. */ 'queue_database_deletion' => false, - /** - * If you don't supply an id when creating a tenant, this class will be used to generate a random ID. - */ - 'unique_id_generator' => Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator::class, - /** * Middleware pushed to the global middleware stack. */ - 'global_middleware' => [ + 'global_middleware' => [ // todo get rid of this Stancl\Tenancy\Middleware\InitializeTenancy::class, ], ]; diff --git a/assets/migrations/2019_09_15_000010_create_tenants_table.php b/assets/migrations/2019_09_15_000010_create_tenants_table.php index f779856f..37fc22c4 100644 --- a/assets/migrations/2019_09_15_000010_create_tenants_table.php +++ b/assets/migrations/2019_09_15_000010_create_tenants_table.php @@ -18,9 +18,10 @@ class CreateTenantsTable extends Migration Schema::create('tenants', function (Blueprint $table) { $table->string('id', 36)->primary(); // 36 characters is the default uuid length - // (optional) your custom, indexed columns may go here + // your custom columns may go here - $table->json('data'); + $table->timestamps(); + $table->json('data')->default('{}'); }); } diff --git a/composer.json b/composer.json index cac4568d..8c04d02e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "stancl/tenancy", - "description": "A Laravel multi-database tenancy package that respects your code.", + "description": "Automatic multi-tenancy for your Laravel application.", "keywords": ["laravel", "multi-tenancy", "multi-database", "tenancy"], "license": "MIT", "authors": [ @@ -20,7 +20,7 @@ "laravel/framework": "^6.0|^7.0", "orchestra/testbench-browser-kit": "^4.0|^5.0", "league/flysystem-aws-s3-v3": "~1.0", - "phpunit/phpcov": "^6.0|^7.0" + "doctrine/dbal": "^2.10" }, "autoload": { "psr-4": { diff --git a/docker-compose.yml b/docker-compose.yml index d5315fb9..fafd6316 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,9 +16,6 @@ services: DB_PASSWORD: password DB_USERNAME: root DB_DATABASE: main - TENANCY_TEST_REDIS_HOST: redis - TENANCY_TEST_MYSQL_HOST: mysql - TENANCY_TEST_PGSQL_HOST: postgres stdin_open: true tty: true mysql: diff --git a/fulltest b/fulltest index de5d7542..fa5435e3 100755 --- a/fulltest +++ b/fulltest @@ -4,4 +4,3 @@ set -e # for development docker-compose up -d ./test "$@" -docker-compose exec -T test vendor/bin/phpcov merge --clover clover.xml coverage/ diff --git a/phpunit.xml b/phpunit.xml index a1c16a21..cfd22371 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -30,6 +30,5 @@ - \ No newline at end of file diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 4859d047..d10bcd06 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -8,6 +8,7 @@ use Illuminate\Console\Command; use Illuminate\Database\Console\Migrations\MigrateCommand; use Illuminate\Database\Migrations\Migrator; use Stancl\Tenancy\DatabaseManager; +use Stancl\Tenancy\Events\DatabaseMigrated; use Stancl\Tenancy\Traits\DealsWithMigrations; use Stancl\Tenancy\Traits\HasATenantsOption; @@ -62,6 +63,8 @@ class Migrate extends MigrateCommand // Migrate parent::handle(); }); + + event(new DatabaseMigrated($tenant)); }); } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index acc697e0..a31d0045 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\Seeds\SeedCommand; use Stancl\Tenancy\DatabaseManager; +use Stancl\Tenancy\Events\DatabaseSeeded; use Stancl\Tenancy\Traits\HasATenantsOption; class Seed extends SeedCommand @@ -60,6 +61,8 @@ class Seed extends SeedCommand // Seed parent::handle(); }); + + event(new DatabaseSeeded($tenant)); }); } } diff --git a/src/Contracts/UniqueIdentifierGenerator.php b/src/Contracts/UniqueIdentifierGenerator.php index 0dd40eb5..ad560641 100644 --- a/src/Contracts/UniqueIdentifierGenerator.php +++ b/src/Contracts/UniqueIdentifierGenerator.php @@ -4,14 +4,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; +use Stancl\Tenancy\Database\Models\Tenant; + interface UniqueIdentifierGenerator { /** * Generate a unique identifier. - * - * @param string[] $domains - * @param array $data - * @return string */ - public static function generate(array $domains, array $data = []): string; + public static function generate(Tenant $tenant): string; } diff --git a/src/Database/Models/Concerns/CentralConnection.php b/src/Database/Models/Concerns/CentralConnection.php new file mode 100644 index 00000000..1de8d3d5 --- /dev/null +++ b/src/Database/Models/Concerns/CentralConnection.php @@ -0,0 +1,11 @@ +id && config('tenancy.id_generator')) { + $model->id = app(config('tenancy.id_generator'))->generate($model); + } + }); + } +} diff --git a/src/Database/Models/Concerns/HasADataColumn.php b/src/Database/Models/Concerns/HasADataColumn.php new file mode 100644 index 00000000..8f10a407 --- /dev/null +++ b/src/Database/Models/Concerns/HasADataColumn.php @@ -0,0 +1,55 @@ +getAttributes() as $key => $value) { + if (! in_array($key, static::getCustomColums())) { + $current = $model->getAttribute(static::getDataColumn()) ?? []; + + $model->setAttribute(static::getDataColumn(), array_merge($current, [ + $key => $value, + ])); + + unset($model->attributes[$key]); + } + } + }; + + $decode = function (self $model) { + foreach ($model->getAttribute(static::getDataColumn()) ?? [] as $key => $value) { + $model->setAttribute($key, $value); + } + + $model->setAttribute(static::getDataColumn(), null); + }; + + static::saving($encode); + static::saved($decode); + static::retrieved($decode); + } + + public function getCasts() + { + return array_merge(parent::getCasts(), [ + static::getDataColumn() => 'array', + ]); + } + + /** + * Get the name of the column that stores additional data. + */ + public static function getDataColumn(): string + { + return 'data'; + } + + public static function getCustomColums(): array + { + return array_merge(['id'], config('tenancy.custom_columns')); + } +} \ No newline at end of file diff --git a/src/Database/Models/Domain.php b/src/Database/Models/Domain.php new file mode 100644 index 00000000..b9a8dac8 --- /dev/null +++ b/src/Database/Models/Domain.php @@ -0,0 +1,24 @@ +belongsTo(Tenant::class); + } + + protected $dispatchEvents = [ + 'saved' => DomainSaved::class, + 'created' => DomainCreated::class, + 'updated' => DomainUpdated::class, + 'deleted' => DomainDeleted::class, + ]; +} diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php new file mode 100644 index 00000000..f65f414e --- /dev/null +++ b/src/Database/Models/Tenant.php @@ -0,0 +1,93 @@ +dataColumnCasts(), [ + 'id' => $this->getIncrementing() ? 'integer' : 'string', + ]); + } + + public function getIncrementing() + { + return config('tenancy.id_generator') === null; + } + + public $guarded = []; + + public function domains() // todo not required + { + return $this->hasMany(Domain::class); + } + + public static function internalPrefix(): string + { + return config('tenancy.database_prefix'); + } + + /** + * Get an internal key. + * + * @param string $key + * @return mixed + */ + public function getInternal(string $key) + { + return $this->getAttribute(static::internalPrefix() . $key); + } + + /** + * Set internal key. + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function setInternal(string $key, $value) + { + $this->setAttribute($key, $value); + + return $this; + } + + public function database(): DatabaseConfig + { + return new DatabaseConfig($this); + } + + public function run(callable $callback) + { + $originalTenant = $this->manager->getTenant(); + + $this->manager->initializeTenancy($this); + $result = $callback($this); + $this->manager->endTenancy($this); + + if ($originalTenant) { + $this->manager->initializeTenancy($originalTenant); + } + + return $result; + } + + protected $dispatchesEvents = [ + 'saved' => Events\TenantSaved::class, + 'created' => Events\TenantCreated::class, + 'updated' => Events\TenantUpdated::class, + 'deleted' => Events\TenantDeleted::class, + ]; +} diff --git a/src/DatabaseConfig.php b/src/DatabaseConfig.php index 93248427..df081311 100644 --- a/src/DatabaseConfig.php +++ b/src/DatabaseConfig.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\Contracts\ModifiesDatabaseNameForConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; class DatabaseConfig @@ -65,33 +66,33 @@ class DatabaseConfig public function getName(): ?string { - return $this->tenant->data['_tenancy_db_name'] ?? (static::$databaseNameGenerator)($this->tenant); + return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant); } public function getUsername(): ?string { - return $this->tenant->data['_tenancy_db_username'] ?? null; + return $this->tenant->getInternal('db_username') ?? null; } public function getPassword(): ?string { - return $this->tenant->data['_tenancy_db_password'] ?? null; + return $this->tenant->getInternal('db_password') ?? null; } public function makeCredentials(): void { - $this->tenant->data['_tenancy_db_name'] = $this->getName() ?? (static::$databaseNameGenerator)($this->tenant); + $this->tenant->setInternal('db_name', $this->getName() ?? (static::$databaseNameGenerator)($this->tenant)); if ($this->manager() instanceof ManagesDatabaseUsers) { - $this->tenant->data['_tenancy_db_username'] = $this->getUsername() ?? (static::$usernameGenerator)($this->tenant); - $this->tenant->data['_tenancy_db_password'] = $this->getPassword() ?? (static::$passwordGenerator)($this->tenant); + $this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant)); + $this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant)); } } public function getTemplateConnectionName(): string { - return $this->tenant->data['_tenancy_db_connection'] - ?? config('tenancy.database.template_connection') + return $this->tenant->getInternal('db_connection') + ?? config('tenancy.template_tenant_connection') ?? DatabaseManager::$originalDefaultConnectionName; } @@ -120,23 +121,23 @@ class DatabaseConfig */ public function tenantConfig(): array { - $dbConfig = array_filter(array_keys($this->tenant->data), function ($key) { - return Str::startsWith($key, '_tenancy_db_'); + $dbConfig = array_filter(array_keys($this->tenant->getAttributes()), function ($key) { + return Str::startsWith($key, $this->tenant->internalPrefix() . 'db_'); }); // Remove DB name because we set that separately - if (($pos = array_search('_tenancy_db_name', $dbConfig)) !== false) { + if (($pos = array_search($this->tenant->internalPrefix() . 'db_name', $dbConfig)) !== false) { unset($dbConfig[$pos]); } // Remove DB connection because that's not used inside the array - if (($pos = array_search('_tenancy_db_connection', $dbConfig)) !== false) { + if (($pos = array_search($this->tenant->internalPrefix() . 'db_connection', $dbConfig)) !== false) { unset($dbConfig[$pos]); } return array_reduce($dbConfig, function ($config, $key) { return array_merge($config, [ - Str::substr($key, strlen('_tenancy_db_')) => $this->tenant[$key], + Str::substr($key, strlen($this->tenant->internalPrefix() . 'db_')) => $this->tenant->getAttribute($key), ]); }, []); } diff --git a/src/Events/Contracts/DomainEvent.php b/src/Events/Contracts/DomainEvent.php new file mode 100644 index 00000000..5e896afd --- /dev/null +++ b/src/Events/Contracts/DomainEvent.php @@ -0,0 +1,19 @@ +domain = $domain; + } +} diff --git a/src/Events/Contracts/TenantEvent.php b/src/Events/Contracts/TenantEvent.php new file mode 100644 index 00000000..56d43a28 --- /dev/null +++ b/src/Events/Contracts/TenantEvent.php @@ -0,0 +1,19 @@ +tenant = $tenant; + } +} diff --git a/src/Events/DatabaseCreated.php b/src/Events/DatabaseCreated.php new file mode 100644 index 00000000..19cb039f --- /dev/null +++ b/src/Events/DatabaseCreated.php @@ -0,0 +1,7 @@ +jobs = $jobs; + $this->send = $send ?? function ($event) { + // If no $send callback is set, we'll just pass the event through the jobs. + return $event; + }; + $this->shouldQueue = $shouldQueue ?? static::$shouldQueueByDefault; + } + + /** @param callable[]|string[] $jobs */ + public static function make(array $jobs): self + { + return new static($jobs); + } + + public function queue(bool $shouldQueue): self + { + $this->shouldQueue = $shouldQueue; + + return $this; + } + + public function send(callable $send): self + { + $this->send = $send; + + return $this; + } + + /** @return bool|$this */ + public function shouldQueue(bool $shouldQueue = null) + { + if ($shouldQueue !== null) { + $this->shouldQueue = $shouldQueue; + + return $this; + } + + return $this->shouldQueue; + } + + public function handle($event): void + { + /** @var Pipeline $pipeline */ + $pipeline = app(Pipeline::class); + + $pipeline + ->send(($this->send)($event)) + ->through($this->jobs) + ->thenReturn(); + } +} diff --git a/src/Events/Listeners/QueueableListener.php b/src/Events/Listeners/QueueableListener.php new file mode 100644 index 00000000..75bc2a91 --- /dev/null +++ b/src/Events/Listeners/QueueableListener.php @@ -0,0 +1,15 @@ +getBootstrappers() as $bootstrapper) { + $bootstrapper->end(); + } + } +} \ No newline at end of file diff --git a/src/Events/TenancyEnded.php b/src/Events/TenancyEnded.php new file mode 100644 index 00000000..f22080d4 --- /dev/null +++ b/src/Events/TenancyEnded.php @@ -0,0 +1,16 @@ +tenant = $tenant; + } +} diff --git a/src/Events/TenancyInitialized.php b/src/Events/TenancyInitialized.php new file mode 100644 index 00000000..43c53c38 --- /dev/null +++ b/src/Events/TenancyInitialized.php @@ -0,0 +1,16 @@ +tenant = $tenant; + } +} diff --git a/src/Events/TenantCreated.php b/src/Events/TenantCreated.php new file mode 100644 index 00000000..eadde370 --- /dev/null +++ b/src/Events/TenantCreated.php @@ -0,0 +1,7 @@ +tenant = $tenant; + } + + public function handle() + { + if ($this->tenant->getAttribute('_tenancy_create_database') !== false) { + $this->tenant->database()->manager()->createDatabase($this->tenant); + } + } +} diff --git a/src/Jobs/QueuedTenantDatabaseDeleter.php b/src/Jobs/DeleteDatabase.php similarity index 55% rename from src/Jobs/QueuedTenantDatabaseDeleter.php rename to src/Jobs/DeleteDatabase.php index dbd79d21..b897bc5c 100644 --- a/src/Jobs/QueuedTenantDatabaseDeleter.php +++ b/src/Jobs/DeleteDatabase.php @@ -10,31 +10,22 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\TenantDatabaseManager; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Database\Models\Tenant; -class QueuedTenantDatabaseDeleter implements ShouldQueue +class DeleteDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantDatabaseManager */ - protected $databaseManager; - /** @var Tenant */ protected $tenant; - public function __construct(TenantDatabaseManager $databaseManager, Tenant $tenant) + public function __construct(Tenant $tenant) { - $this->databaseManager = $databaseManager; $this->tenant = $tenant; } - /** - * Execute the job. - * - * @return void - */ public function handle() { - $this->databaseManager->deleteDatabase($this->tenant); + $this->tenant->database()->manager()->deleteDatabase($this->tenant); } } diff --git a/src/Jobs/QueuedTenantDatabaseCreator.php b/src/Jobs/MigrateDatabase.php similarity index 57% rename from src/Jobs/QueuedTenantDatabaseCreator.php rename to src/Jobs/MigrateDatabase.php index 066580b9..ea3834c6 100644 --- a/src/Jobs/QueuedTenantDatabaseCreator.php +++ b/src/Jobs/MigrateDatabase.php @@ -9,22 +9,18 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Stancl\Tenancy\Contracts\TenantDatabaseManager; -use Stancl\Tenancy\Tenant; +use Illuminate\Support\Facades\Artisan; +use Stancl\Tenancy\Database\Models\Tenant; -class QueuedTenantDatabaseCreator implements ShouldQueue +class MigrateDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantDatabaseManager */ - protected $databaseManager; - /** @var Tenant */ protected $tenant; - public function __construct(TenantDatabaseManager $databaseManager, Tenant $tenant) + public function __construct(Tenant $tenant) { - $this->databaseManager = $databaseManager; $this->tenant = $tenant; } @@ -35,6 +31,12 @@ class QueuedTenantDatabaseCreator implements ShouldQueue */ public function handle() { - $this->databaseManager->createDatabase($this->tenant); + $migrationParameters = [ + // todo ... + ]; + + Artisan::call('tenants:migrate', [ + '--tenants' => [$this->tenant->id], + ] + $migrationParameters); } } diff --git a/src/Jobs/QueuedTenantDatabaseMigrator.php b/src/Jobs/QueuedTenantDatabaseMigrator.php deleted file mode 100644 index c71696cc..00000000 --- a/src/Jobs/QueuedTenantDatabaseMigrator.php +++ /dev/null @@ -1,42 +0,0 @@ -tenantId = $tenant->id; - $this->migrationParameters = $migrationParameters; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - Artisan::call('tenants:migrate', [ - '--tenants' => [$this->tenantId], - ] + $this->migrationParameters); - } -} diff --git a/src/Jobs/QueuedTenantDatabaseSeeder.php b/src/Jobs/SeedDatabase.php similarity index 73% rename from src/Jobs/QueuedTenantDatabaseSeeder.php rename to src/Jobs/SeedDatabase.php index e1ecea41..6a197cf0 100644 --- a/src/Jobs/QueuedTenantDatabaseSeeder.php +++ b/src/Jobs/SeedDatabase.php @@ -10,18 +10,18 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Artisan; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Database\Models\Tenant; -class QueuedTenantDatabaseSeeder implements ShouldQueue +class SeedDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var string */ - protected $tenantId; + /** @var Tenant */ + protected $tenant; public function __construct(Tenant $tenant) { - $this->tenantId = $tenant->id; + $this->tenant = $tenant; } /** @@ -32,7 +32,7 @@ class QueuedTenantDatabaseSeeder implements ShouldQueue public function handle() { Artisan::call('tenants:seed', [ - '--tenants' => [$this->tenantId], + '--tenants' => [$this->tenant->id], ]); } } diff --git a/src/StorageDrivers/RedisStorageDriver.php b/src/StorageDrivers/RedisStorageDriver.php deleted file mode 100644 index 79852f8c..00000000 --- a/src/StorageDrivers/RedisStorageDriver.php +++ /dev/null @@ -1,241 +0,0 @@ -app = $app; - $this->redis = $redis->connection($app['config']['tenancy.storage_drivers.redis.connection'] ?? 'tenancy'); - } - - /** - * Get the current tenant. - * - * @return Tenant - */ - protected function tenant() - { - return $this->tenant ?? $this->app[Tenant::class]; - } - - public function withDefaultTenant(Tenant $tenant): self - { - $this->tenant = $tenant; - - return $this; - } - - public function ensureTenantCanBeCreated(Tenant $tenant): void - { - // Tenant ID - if ($this->redis->exists("tenants:{$tenant->id}")) { - throw new TenantWithThisIdAlreadyExistsException($tenant->id); - } - - // Domains - if ($this->redis->exists(...array_map(function ($domain) { - return "domains:$domain"; - }, $tenant->domains))) { - throw new DomainsOccupiedByOtherTenantException; - } - } - - public function findByDomain(string $domain): Tenant - { - $id = $this->getTenantIdByDomain($domain); - if (! $id) { - throw new TenantCouldNotBeIdentifiedException($domain); - } - - return $this->findById($id); - } - - public function findById(string $id): Tenant - { - $data = $this->redis->hgetall("tenants:$id"); - - if (! $data) { - throw new TenantDoesNotExistException($id); - } - - return $this->makeTenant($data); - } - - public function getTenantIdByDomain(string $domain): ?string - { - return $this->redis->hget("domains:$domain", 'tenant_id') ?: null; - } - - public function createTenant(Tenant $tenant): void - { - $this->redis->transaction(function ($pipe) use ($tenant) { - foreach ($tenant->domains as $domain) { - $pipe->hmset("domains:$domain", ['tenant_id' => $tenant->id]); - } - - $data = []; - foreach ($tenant->data as $key => $value) { - $data[$key] = json_encode($value); - } - - $pipe->hmset("tenants:{$tenant->id}", array_merge($data, ['_tenancy_domains' => json_encode($tenant->domains)])); - }); - } - - public function updateTenant(Tenant $tenant): void - { - $id = $tenant->id; - - $old_domains = json_decode($this->redis->hget("tenants:$id", '_tenancy_domains'), true); - $deleted_domains = array_diff($old_domains, $tenant->domains); - $domains = $tenant->domains; - - $data = []; - foreach ($tenant->data as $key => $value) { - $data[$key] = json_encode($value); - } - - $this->redis->transaction(function ($pipe) use ($id, $data, $deleted_domains, $domains) { - foreach ($deleted_domains as $deleted_domain) { - $pipe->del("domains:$deleted_domain"); - } - - foreach ($domains as $domain) { - $pipe->hset("domains:$domain", 'tenant_id', $id); - } - - $pipe->hmset("tenants:$id", array_merge($data, ['_tenancy_domains' => json_encode($domains)])); - }); - } - - public function deleteTenant(Tenant $tenant): void - { - $this->redis->transaction(function ($pipe) use ($tenant) { - foreach ($tenant->domains as $domain) { - $pipe->del("domains:$domain"); - } - - $pipe->del("tenants:{$tenant->id}"); - }); - } - - /** - * Return a list of all tenants. - * - * @param string[] $ids - * @return Tenant[] - */ - public function all(array $ids = []): array - { - $hashes = array_map(function ($hash) { - return "tenants:{$hash}"; - }, $ids); - - if (! $hashes) { - // Prefix is applied to all functions except scan(). - // This code applies the correct prefix manually. - $redis_prefix = $this->redis->getOption($this->redis->client()::OPT_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)); - }, $all_keys); - } - - return array_map(function ($tenant) { - return $this->makeTenant($this->redis->hgetall($tenant)); - }, $hashes); - } - - /** - * Make a Tenant instance from low-level array data. - * - * @param array $data - * @return Tenant - */ - protected function makeTenant(array $data): Tenant - { - foreach ($data as $key => $value) { - $data[$key] = json_decode($value, true); - } - - $domains = $data['_tenancy_domains']; - unset($data['_tenancy_domains']); - - return Tenant::fromStorage($data)->withDomains($domains); - } - - public function get(string $key, Tenant $tenant = null) - { - $tenant = $tenant ?? $this->tenant(); - - $json_data = $this->redis->hget("tenants:{$tenant->id}", $key); - if ($json_data === false) { - return; - } - - return json_decode($json_data, true); - } - - public function getMany(array $keys, Tenant $tenant = null): array - { - $tenant = $tenant ?? $this->tenant(); - - $result = []; - $values = $this->redis->hmget("tenants:{$tenant->id}", $keys); - foreach ($keys as $i => $key) { - $result[$key] = json_decode($values[$i], true); - } - - return $result; - } - - public function put(string $key, $value, Tenant $tenant = null): void - { - $tenant = $tenant ?? $this->tenant(); - $this->redis->hset("tenants:{$tenant->id}", $key, json_encode($value)); - } - - public function putMany(array $kvPairs, Tenant $tenant = null): void - { - $tenant = $tenant ?? $this->tenant(); - - foreach ($kvPairs as $key => $value) { - $kvPairs[$key] = json_encode($value); - } - - $this->redis->hmset("tenants:{$tenant->id}", $kvPairs); - } - - public function deleteMany(array $keys, Tenant $tenant = null): void - { - $tenant = $tenant ?? $this->tenant(); - - $this->redis->hdel("tenants:{$tenant->id}", ...$keys); - } -} diff --git a/src/Tenancy.php b/src/Tenancy.php new file mode 100644 index 00000000..24917c24 --- /dev/null +++ b/src/Tenancy.php @@ -0,0 +1,39 @@ +tenant = $tenant; + + event(new Events\TenancyInitialized($tenant)); + } + + public function end(): void + { + event(new Events\TenancyEnded($this->tenant)); + + $this->tenant = null; + } + + /** @return TenancyBootstrapper[] */ + public function getBootstrappers(): array + { + $resolve = static::$getBootstrappers ?? function (Tenant $tenant) { + return config('tenancy.bootstrappers'); + }; + + return $resolve($this->tenant); + } +} diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index b5c41c73..9644576f 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -8,6 +8,9 @@ use Illuminate\Cache\CacheManager; use Illuminate\Contracts\Http\Kernel; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Database\TenantObserver; +use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver; use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper; class TenancyServiceProvider extends ServiceProvider @@ -22,13 +25,13 @@ class TenancyServiceProvider extends ServiceProvider $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy'); $this->app->bind(Contracts\StorageDriver::class, function ($app) { - return $app->make($app['config']['tenancy.storage_drivers'][$app['config']['tenancy.storage_driver']]['driver']); + return $app->make(DatabaseStorageDriver::class); }); $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.unique_id_generator']); $this->app->singleton(DatabaseManager::class); - $this->app->singleton(TenantManager::class); + $this->app->singleton(Tenancy::class); $this->app->bind(Tenant::class, function ($app) { - return $app[TenantManager::class]->getTenant(); + return $app[Tenancy::class]->tenant; }); foreach ($this->app['config']['tenancy.bootstrappers'] as $bootstrapper) { diff --git a/src/Tenant.php b/src/Tenant.php deleted file mode 100644 index 4a9824f8..00000000 --- a/src/Tenant.php +++ /dev/null @@ -1,453 +0,0 @@ -config = $config; - $this->storage = $storage->withDefaultTenant($this); - $this->manager = $tenantManager; - $this->idGenerator = $idGenerator; - } - - /** - * Public constructor. - * - * @param Application $app - * @return self - */ - public static function new(Application $app = null): self - { - $app = $app ?? app(); - - return new static( - $app[Repository::class], - $app[StorageDriver::class], - $app[TenantManager::class], - $app[UniqueIdentifierGenerator::class] - ); - } - - /** - * Used by storage drivers to create persisted instances of Tenant. - * - * @param array $data - * @return self - */ - public static function fromStorage(array $data): self - { - return static::new()->withData($data)->persisted(true); - } - - /** - * Create a tenant in a single call. - * - * @param string|string[] $domains - * @param array $data - * @return self - */ - public static function create($domains, array $data = []): self - { - return static::new()->withDomains((array) $domains)->withData($data)->save(); - } - - /** - * DO NOT CALL THIS METHOD FROM USERLAND UNLESS YOU KNOW WHAT YOU ARE DOING. - * Set $persisted. - * - * @param bool $persisted - * @return self - */ - public function persisted(bool $persisted): self - { - $this->persisted = $persisted; - - return $this; - } - - /** - * Does this model exist in the tenant storage. - * - * @return bool - */ - public function isPersisted(): bool - { - return $this->persisted; - } - - /** - * Assign domains to the tenant. - * - * @param string|string[] $domains - * @return self - */ - public function addDomains($domains): self - { - $domains = (array) $domains; - $this->domains = array_merge($this->domains, $domains); - - return $this; - } - - /** - * Unassign domains from the tenant. - * - * @param string|string[] $domains - * @return self - */ - public function removeDomains($domains): self - { - $domains = (array) $domains; - $this->domains = array_diff($this->domains, $domains); - - return $this; - } - - /** - * Unassign all domains from the tenant. - * - * @return self - */ - public function clearDomains(): self - { - $this->domains = []; - - return $this; - } - - /** - * Set (overwrite) the tenant's domains. - * - * @param string|string[] $domains - * @return self - */ - public function withDomains($domains): self - { - $domains = (array) $domains; - - $this->domains = $domains; - - return $this; - } - - /** - * Set (overwrite) tenant data. - * - * @param array $data - * @return self - */ - public function withData(array $data): self - { - $this->data = $data; - - return $this; - } - - /** - * Generate a random ID. - * - * @return void - */ - public function generateId() - { - $this->id = $this->idGenerator->generate($this->domains, $this->data); - } - - /** - * Write the tenant's state to storage. - * - * @return self - */ - public function save(): self - { - if (! isset($this->data['id'])) { - $this->generateId(); - } - - if ($this->persisted) { - $this->manager->updateTenant($this); - } else { - $this->database()->makeCredentials(); - $this->manager->createTenant($this); - } - - return $this; - } - - /** - * Delete a tenant from storage. - * - * @return self - */ - public function delete(): self - { - if ($this->persisted) { - $this->manager->deleteTenant($this); - $this->persisted = false; - } - - return $this; - } - - /** - * Unassign all domains from the tenant and write to storage. - * - * @return self - */ - public function softDelete(): self - { - $this->manager->event('tenant.softDeleting', $this); - - $this->put([ - '_tenancy_original_domains' => $this->domains, - ]); - $this->clearDomains(); - $this->save(); - - $this->manager->event('tenant.softDeleted', $this); - - return $this; - } - - /** - * Get database config. - * - * @return DatabaseConfig - */ - public function database(): DatabaseConfig - { - return new DatabaseConfig($this); - } - - /** - * Get a value from tenant storage. - * - * @param string|string[] $keys - * @return void - */ - public function get($keys) - { - if (is_array($keys)) { - 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] ?? null; - - return $pairs; - }, []); - } - - return $this->storage->getMany($keys); - } - - // single key - $key = $keys; - - if (! isset($this->data[$key]) && $this->persisted) { - $this->data[$key] = $this->storage->get($key); - } - - return $this->data[$key]; - } - - /** - * Set a value and write to storage. - * - * @param string|array $key - * @param mixed $value - * @return self - */ - public function put($key, $value = null): self - { - $this->manager->event('tenant.updating', $this); - - if ($key === 'id') { - throw new TenantStorageException("Tenant ids can't be changed."); - } - - if (is_array($key)) { - if ($this->persisted) { - $this->storage->putMany($key); - } - - foreach ($key as $k => $v) { // Add to cache - $this->data[$k] = $v; - } - } else { - if ($this->persisted) { - $this->storage->put($key, $value); - } - - $this->data[$key] = $value; - } - - $this->manager->event('tenant.updated', $this); - - return $this; - } - - /** @alias put */ - public function set($key, $value = null): self - { - return $this->put($key, $value); - } - - /** - * Delete a key from the tenant's storage. - * - * @param string $key - * @return self - */ - public function deleteKey(string $key): self - { - return $this->deleteKeys([$key]); - } - - /** - * Delete keys from the tenant's storage. - * - * @param string[] $keys - * @return self - */ - public function deleteKeys(array $keys): self - { - $this->manager->event('tenant.updating', $this); - - if (! $this->storage instanceof CanDeleteKeys) { - throw new NotImplementedException(get_class($this->storage), 'deleteMany', - 'This method was added to storage drivers provided by the package in 2.2.0 and will be part of the StorageDriver contract in 3.0.0.' - ); - } else { - $this->storage->deleteMany($keys); - foreach ($keys as $key) { - unset($this->data[$key]); - } - } - - $this->manager->event('tenant.updated', $this); - - return $this; - } - - /** - * Set a value in the data array without saving into storage. - * - * @param string $key - * @param mixed $value - * @return self - */ - public function with(string $key, $value): self - { - $this->data[$key] = $value; - - return $this; - } - - /** - * Run a closure inside the tenant's environment. - * - * @param Closure $closure - * @return mixed - */ - public function run(Closure $closure) - { - $originalTenant = $this->manager->getTenant(); - - $this->manager->initializeTenancy($this); - $result = $closure($this); - $this->manager->endTenancy($this); - - if ($originalTenant) { - $this->manager->initializeTenancy($originalTenant); - } - - return $result; - } - - public function __get($key) - { - return $this->get($key); - } - - public function __set($key, $value) - { - if ($key === 'id' && isset($this->data['id'])) { - throw new TenantStorageException("Tenant ids can't be changed."); - } - - $this->data[$key] = $value; - } - - public function __call($method, $parameters) - { - if (Str::startsWith($method, 'with')) { - return $this->with(Str::snake(substr($method, 4)), $parameters[0]); - } - - static::throwBadMethodCallException($method); - } -} diff --git a/src/TenantManager.php b/src/TenantManager.php deleted file mode 100644 index 103ecc8c..00000000 --- a/src/TenantManager.php +++ /dev/null @@ -1,471 +0,0 @@ -app = $app; - $this->storage = $storage; - $this->artisan = $artisan; - $this->database = $database->withTenantManager($this); - - $this->bootstrapFeatures(); - } - - /** - * Write a new tenant to storage. - * - * @param Tenant $tenant - * @return self - */ - public function createTenant(Tenant $tenant): self - { - $this->event('tenant.creating', $tenant); - - $this->ensureTenantCanBeCreated($tenant); - - $this->storage->createTenant($tenant); - - $tenant->persisted = true; - - /** @var \Illuminate\Contracts\Queue\ShouldQueue[]|callable[] $afterCreating */ - $afterCreating = []; - - if ($this->shouldMigrateAfterCreation()) { - $afterCreating[] = $this->databaseCreationQueued() - ? new QueuedTenantDatabaseMigrator($tenant, $this->getMigrationParameters()) - : function () use ($tenant) { - $this->artisan->call('tenants:migrate', [ - '--tenants' => [$tenant['id']], - ] + $this->getMigrationParameters()); - }; - } - - if ($this->shouldSeedAfterMigration()) { - $afterCreating[] = $this->databaseCreationQueued() - ? new QueuedTenantDatabaseSeeder($tenant) - : function () use ($tenant) { - $this->artisan->call('tenants:seed', [ - '--tenants' => [$tenant['id']], - ]); - }; - } - - if ($this->shouldCreateDatabase($tenant)) { - $this->database->createDatabase($tenant, $afterCreating); - } - - $this->event('tenant.created', $tenant); - - return $this; - } - - /** - * Delete a tenant from storage. - * - * @param Tenant $tenant - * @return self - */ - public function deleteTenant(Tenant $tenant): self - { - $this->event('tenant.deleting', $tenant); - - $this->storage->deleteTenant($tenant); - - if ($this->shouldDeleteDatabase()) { - $this->database->deleteDatabase($tenant); - } - - $this->event('tenant.deleted', $tenant); - - return $this; - } - - /** - * Alias for Stancl\Tenancy\Tenant::create. - * - * @param string|string[] $domains - * @param array $data - * @return Tenant - */ - public static function create($domains, array $data = []): Tenant - { - return Tenant::create($domains, $data); - } - - /** - * Ensure that a tenant can be created. - * - * @param Tenant $tenant - * @return void - * @throws TenantCannotBeCreatedException - */ - public function ensureTenantCanBeCreated(Tenant $tenant): void - { - if ($this->shouldCreateDatabase($tenant)) { - $this->database->ensureTenantCanBeCreated($tenant); - } - - $this->storage->ensureTenantCanBeCreated($tenant); - } - - /** - * Update an existing tenant in storage. - * - * @param Tenant $tenant - * @return self - */ - public function updateTenant(Tenant $tenant): self - { - $this->event('tenant.updating', $tenant); - - $this->storage->updateTenant($tenant); - - $this->event('tenant.updated', $tenant); - - return $this; - } - - /** - * Find tenant by domain & initialize tenancy. - * - * @param string|null $domain - * @return self - */ - public function init(string $domain = null): self - { - $domain = $domain ?? request()->getHost(); - $this->initializeTenancy($this->findByDomain($domain)); - - return $this; - } - - /** - * Find tenant by ID & initialize tenancy. - * - * @param string $id - * @return self - */ - public function initById(string $id): self - { - $this->initializeTenancy($this->find($id)); - - return $this; - } - - /** - * Find a tenant using an id. - * - * @param string $id - * @return Tenant - * @throws TenantCouldNotBeIdentifiedException - */ - public function find(string $id): Tenant - { - return $this->storage->findById($id); - } - - /** - * Find a tenant using a domain name. - * - * @param string $id - * @return Tenant - * @throws TenantCouldNotBeIdentifiedException - */ - public function findByDomain(string $domain): Tenant - { - return $this->storage->findByDomain($domain); - } - - /** - * Find a tenant using an arbitrary key. - * - * @param string $key - * @param mixed $value - * @return Tenant - * @throws TenantCouldNotBeIdentifiedException - * @throws NotImplementedException - */ - public function findBy(string $key, $value): Tenant - { - if ($key === null) { - throw new Exception('No key supplied.'); - } - - if ($value === null) { - throw new Exception('No value supplied.'); - } - - if (! $this->storage instanceof CanFindByAnyKey) { - throw new NotImplementedException(get_class($this->storage), 'findBy', - 'This method was added to the DB storage driver provided by the package in 2.2.0 and might be part of the StorageDriver contract in 3.0.0.' - ); - } - - return $this->storage->findBy($key, $value); - } - - /** - * Get all tenants. - * - * @param Tenant[]|string[] $only - * @return Collection - */ - public function all($only = []): Collection - { - $only = array_map(function ($item) { - return $item instanceof Tenant ? $item->id : $item; - }, (array) $only); - - return collect($this->storage->all($only)); - } - - /** - * Initialize tenancy. - * - * @param Tenant $tenant - * @return self - */ - public function initializeTenancy(Tenant $tenant): self - { - if ($this->initialized) { - $this->endTenancy(); - } - - $this->setTenant($tenant); - $this->bootstrapTenancy($tenant); - $this->initialized = true; - - return $this; - } - - /** @alias initializeTenancy */ - public function initialize(Tenant $tenant): self - { - return $this->initializeTenancy($tenant); - } - - /** - * Execute TenancyBootstrappers. - * - * @param Tenant $tenant - * @return self - */ - public function bootstrapTenancy(Tenant $tenant): self - { - $prevented = $this->event('bootstrapping', $tenant); - - foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) { - $this->app[$bootstrapper]->start($tenant); - } - - $this->event('bootstrapped', $tenant); - - return $this; - } - - public function endTenancy(): self - { - if (! $this->initialized) { - return $this; - } - - $prevented = $this->event('ending', $this->tenant); - - foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) { - $this->app[$bootstrapper]->end(); - } - - $this->initialized = false; - $this->tenant = null; - - $this->event('ended'); - - return $this; - } - - /** @alias endTenancy */ - public function end(): self - { - return $this->endTenancy(); - } - - /** - * Get the current tenant. - * - * @param string $key - * @return Tenant|null|mixed - */ - public function getTenant(string $key = null) - { - if (! $this->tenant) { - return; - } - - if (! is_null($key)) { - return $this->tenant[$key]; - } - - return $this->tenant; - } - - protected function setTenant(Tenant $tenant): self - { - $this->tenant = $tenant; - - return $this; - } - - protected function bootstrapFeatures(): self - { - foreach ($this->app['config']['tenancy.features'] as $feature) { - $this->app[$feature]->bootstrap($this); - } - - return $this; - } - - /** - * Return a list of TenancyBootstrappers. - * - * @param string[] $except - * @return Contracts\TenancyBootstrapper[] - */ - public function tenancyBootstrappers($except = []): array - { - return array_diff_key($this->app['config']['tenancy.bootstrappers'], array_flip($except)); - } - - public function shouldCreateDatabase(Tenant $tenant): bool - { - if (array_key_exists('_tenancy_create_database', $tenant->data)) { - return $tenant->data['_tenancy_create_database']; - } - - return $this->app['config']['tenancy.create_database'] ?? true; - } - - public function shouldMigrateAfterCreation(): bool - { - return $this->app['config']['tenancy.migrate_after_creation'] ?? false; - } - - public function shouldSeedAfterMigration(): bool - { - return $this->shouldMigrateAfterCreation() && $this->app['config']['tenancy.seed_after_migration'] ?? false; - } - - public function databaseCreationQueued(): bool - { - return $this->app['config']['tenancy.queue_database_creation'] ?? false; - } - - public function shouldDeleteDatabase(): bool - { - return $this->app['config']['tenancy.delete_database_after_tenant_deletion'] ?? false; - } - - public function getSeederParameters() - { - return $this->app['config']['tenancy.seeder_parameters'] ?? []; - } - - public function getMigrationParameters() - { - return $this->app['config']['tenancy.migration_parameters'] ?? []; - } - - /** - * Add an event listener. - * - * @param string $name - * @param callable $listener - * @return self - */ - public function eventListener(string $name, callable $listener): self - { - $this->eventListeners[$name] = $this->eventListeners[$name] ?? []; - $this->eventListeners[$name][] = $listener; - - return $this; - } - - /** - * Add an event hook. - * @alias eventListener - * - * @param string $name - * @param callable $listener - * @return self - */ - public function hook(string $name, callable $listener): self - { - return $this->eventListener($name, $listener); - } - - /** - * Trigger an event and execute its listeners. - * - * @param string $name - * @param mixed ...$args - * @return string[] - */ - public function event(string $name, ...$args): array - { - return array_reduce($this->eventListeners[$name] ?? [], function ($results, $listener) use ($args) { - return array_merge($results, $listener($this, ...$args) ?? []); - }, []); - } - - public function __call($method, $parameters) - { - if (Str::startsWith($method, 'findBy')) { - return $this->findBy(Str::snake(substr($method, 6)), $parameters[0]); - } - - static::throwBadMethodCallException($method); - } -} diff --git a/src/UniqueIDGenerators/UUIDGenerator.php b/src/UniqueIDGenerators/UUIDGenerator.php index 7a90f06c..635a7d88 100644 --- a/src/UniqueIDGenerators/UUIDGenerator.php +++ b/src/UniqueIDGenerators/UUIDGenerator.php @@ -6,10 +6,11 @@ namespace Stancl\Tenancy\UniqueIDGenerators; use Ramsey\Uuid\Uuid; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; +use Stancl\Tenancy\Database\Models\Tenant; class UUIDGenerator implements UniqueIdentifierGenerator { - public static function generate(array $domains, array $data = []): string + public static function generate(Tenant $tenant): string { return Uuid::uuid4()->toString(); } diff --git a/src/helpers.php b/src/helpers.php index 58aa8ccb..80f94c18 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,18 +2,14 @@ declare(strict_types=1); -use Stancl\Tenancy\Tenant; -use Stancl\Tenancy\TenantManager; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Tenancy; if (! function_exists('tenancy')) { - /** @return TenantManager|mixed */ - function tenancy($key = null) + /** @return Tenancy */ + function tenancy() { - if ($key) { - return app(TenantManager::class)->getTenant($key) ?? null; - } - - return app(TenantManager::class); + return app(Tenancy::class); } } diff --git a/test b/test index 0d5a2556..49535a7a 100755 --- a/test +++ b/test @@ -1,7 +1,3 @@ #!/bin/bash -set -e -printf "Variant 1 (DB)\n\n" -docker-compose exec -T test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/1.cov "$@" -printf "Variant 2 (Redis)\n\n" -docker-compose exec -T test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/2.cov "$@" +docker-compose exec -T test vendor/bin/phpunit "$@" diff --git a/tests/TestCase.php b/tests/TestCase.php index 918024e7..7594957c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -21,8 +21,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { parent::setUp(); - Redis::connection('tenancy')->flushdb(); - Redis::connection('cache')->flushdb(); + // Redis::connection('cache')->flushdb(); file_put_contents(database_path('central.sqlite'), ''); $this->artisan('migrate:fresh', [ @@ -102,7 +101,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '--realpath' => true, '--force' => true, ], - 'tenancy.storage_drivers.db.connection' => 'central', + 'tenancy.storage.connection' => 'central', 'tenancy.bootstrappers.redis' => \Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class, 'queue.connections.central' => [ 'driver' => 'sync', @@ -112,8 +111,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase ]); $app->singleton(\Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class); - - $app['config']->set(['tenancy.storage_driver' => env('TENANCY_TEST_STORAGE_DRIVER', 'redis')]); } protected function getPackageProviders($app) diff --git a/tests/v3/DatabasePreparationTest.php b/tests/v3/DatabasePreparationTest.php new file mode 100644 index 00000000..e69de29b diff --git a/tests/v3/DomainTest.php b/tests/v3/DomainTest.php new file mode 100644 index 00000000..e69de29b diff --git a/tests/v3/EventListenerTest.php b/tests/v3/EventListenerTest.php new file mode 100644 index 00000000..e69de29b diff --git a/tests/v3/HostnameIdentificationTest.php b/tests/v3/HostnameIdentificationTest.php new file mode 100644 index 00000000..e69de29b diff --git a/tests/v3/JobPipelineTest.php b/tests/v3/JobPipelineTest.php new file mode 100644 index 00000000..e69de29b diff --git a/tests/v3/TenantModelTest.php b/tests/v3/TenantModelTest.php new file mode 100644 index 00000000..97ec074f --- /dev/null +++ b/tests/v3/TenantModelTest.php @@ -0,0 +1,109 @@ +initialize($tenant); + + $this->assertSame($tenant->id, app(Tenant::class)->id); + + tenancy()->end(); + + $this->assertSame(null, app(Tenant::class)); + } + + /** @test */ + public function keys_which_dont_have_their_own_column_go_into_data_json_column() + { + $tenant = Tenant::create([ + 'foo' => 'bar', + ]); + + // Test that model works correctly + $this->assertSame('bar', $tenant->foo); + $this->assertSame(null, $tenant->data); + + // Low level test to test database structure + $this->assertSame(json_encode(['foo' => 'bar']), DB::table('tenants')->where('id', $tenant->id)->first()->data); + $this->assertSame(null, DB::table('tenants')->where('id', $tenant->id)->first()->foo ?? null); + + // Model has the correct structure when retrieved + $tenant = Tenant::first(); + $this->assertSame('bar', $tenant->foo); + $this->assertSame(null, $tenant->data); + + // Model can be updated + $tenant->update([ + 'foo' => 'baz', + 'abc' => 'xyz', + ]); + + $this->assertSame('baz', $tenant->foo); + $this->assertSame('xyz', $tenant->abc); + $this->assertSame(null, $tenant->data); + + // Model can be retrieved after update & is structure correctly + $tenant = Tenant::first(); + + $this->assertSame('baz', $tenant->foo); + $this->assertSame('xyz', $tenant->abc); + $this->assertSame(null, $tenant->data); + } + + /** @test */ + public function id_is_generated_when_no_id_is_supplied() + { + config(['tenancy.id_generator' => UUIDGenerator::class]); + + $this->mock(UUIDGenerator::class, function ($mock) { + return $mock->shouldReceive('generate')->once(); + }); + + $tenant = Tenant::create(); + + $this->assertNotNull($tenant->id); + } + + /** @test */ + public function autoincrement_ids_are_supported() + { + Schema::table('tenants', function (Blueprint $table) { + $table->bigIncrements('id')->change(); + }); + + config(['tenancy.id_generator' => null]); + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + $this->assertSame(1, $tenant1->id); + $this->assertSame(2, $tenant2->id); + } +}