From 42dab2985ae81662b06a2211caafd8a7613eda2c Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 11 Oct 2022 10:33:32 +0200 Subject: [PATCH 1/8] Add `current()` and `currentOrFail()` tenant methods (#970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add and test `Tenant::current()` * Add and test `Tenant::currentOrFail()` * Fix code style (php-cs-fixer) * Update currentOrFail declaration Co-authored-by: Samuel Štancl * Change self return type to static Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- src/Database/Models/Tenant.php | 12 ++++++++++++ tests/TenantModelTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 88c34146..83e75332 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts; use Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Database\TenantCollection; use Stancl\Tenancy\Events; +use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; /** * @property string|int $id @@ -45,6 +46,17 @@ class Tenant extends Model implements Contracts\Tenant return $this->getAttribute($this->getTenantKeyName()); } + public static function current(): static|null + { + return tenant(); + } + + /** @throws TenancyNotInitializedException */ + public static function currentOrFail(): static + { + return static::current() ?? throw new TenancyNotInitializedException; + } + public function newCollection(array $models = []): TenantCollection { return new TenantCollection($models); diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index b4fd38f6..fb62260c 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -18,6 +18,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\UUIDGenerator; +use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; test('created event is dispatched', function () { Event::fake([TenantCreated::class]); @@ -141,6 +142,31 @@ test('a command can be run on a collection of tenants', function () { expect(Tenant::find('t2')->foo)->toBe('xyz'); }); +test('the current method returns the currently initialized tenant', function() { + tenancy()->initialize($tenant = Tenant::create()); + + expect(Tenant::current())->toBe($tenant); +}); + +test('the current method returns null if there is no currently initialized tenant', function() { + tenancy()->end(); + + expect(Tenant::current())->toBeNull(); +}); + +test('currentOrFail method returns the currently initialized tenant', function() { + tenancy()->initialize($tenant = Tenant::create()); + + expect(Tenant::currentOrFail())->toBe($tenant); +}); + +test('currentOrFail method throws an exception if there is no currently initialized tenant', function() { + tenancy()->end(); + + expect(fn() => Tenant::currentOrFail())->toThrow(TenancyNotInitializedException::class); +}); + + class MyTenant extends Tenant { protected $table = 'tenants'; From 080b271bb372d9ea3ef30e0ef31988db92b954f0 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 17 Oct 2022 15:19:30 +0200 Subject: [PATCH 2/8] [4.x] Drop tenant databases on `migrate:fresh` (#971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test that `migrate:fresh` deletes tenant databases * Delete tenants on `migrate:fresh` * Fix code style (php-cs-fixer) * Add config key for dropping tenant databases on `migrate:fresh` * Add MigrateFreshOverride * Try to override `migrate:fresh` in TenancyServiceProvider * Update `migrate:fresh` test * Fix code style (php-cs-fixer) * Drop tenant databases by default * Change `migrate:fresh` test to test if the tenant DBs are dropped by default * Override `migrate:fresh` by extending `FreshCommand` in TenancyServiceProvider * Update MigrateFreshOverride * Fix code style (php-cs-fixer) * Fix commands test * Simplify handle method * Fix code style (php-cs-fixer) * Don't drop tenant DBs on migrate:fresh by default * Change command overriding * Update migrate:fresh test * always register MigrateFreshOverride * misc Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- assets/config.php | 19 ++++---- composer.json | 3 +- phpstan.neon | 1 + src/Commands/MigrateFreshOverride.php | 19 ++++++++ src/Events/Contracts/TenantEvent.php | 2 +- src/TenancyServiceProvider.php | 5 ++ tests/CommandsTest.php | 67 ++++++++++++++++++++++----- 7 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 src/Commands/MigrateFreshOverride.php diff --git a/assets/config.php b/assets/config.php index 6130bade..cd0a6c42 100644 --- a/assets/config.php +++ b/assets/config.php @@ -116,18 +116,21 @@ return [ 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, 'sqlsrv' => Stancl\Tenancy\Database\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class, - /** - * Use this database manager for MySQL to have a DB user created for each tenant database. - * You can customize the grants given to these users by changing the $grants property. - */ + /** + * Use this database manager for MySQL to have a DB user created for each tenant database. + * You can customize the grants given to these users by changing the $grants property. + */ // 'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class, - /** - * Disable the pgsql manager above, and enable the one below if you - * want to separate tenant DBs by schemas rather than databases. - */ + /** + * Disable the pgsql manager above, and enable the one below if you + * want to separate tenant DBs by schemas rather than databases. + */ // 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database ], + + // todo docblock + 'drop_tenant_databases_on_migrate_fresh' => false, ], /** diff --git a/composer.json b/composer.json index 0dc9df09..b30ea94c 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5", "pestphp/pest": "^1.21", - "nunomaduro/larastan": "^1.0" + "nunomaduro/larastan": "^1.0", + "spatie/invade": "^1.1" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon index 3e9ba51d..0567d5ff 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ includes: - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/spatie/invade/phpstan-extension.neon parameters: paths: diff --git a/src/Commands/MigrateFreshOverride.php b/src/Commands/MigrateFreshOverride.php new file mode 100644 index 00000000..88e9e21e --- /dev/null +++ b/src/Commands/MigrateFreshOverride.php @@ -0,0 +1,19 @@ +model()::cursor()->each->delete(); + } + + return parent::handle(); + } +} diff --git a/src/Events/Contracts/TenantEvent.php b/src/Events/Contracts/TenantEvent.php index 951fabfc..e07708b7 100644 --- a/src/Events/Contracts/TenantEvent.php +++ b/src/Events/Contracts/TenantEvent.php @@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Events\Contracts; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\Tenant; -abstract class TenantEvent +abstract class TenantEvent // todo we could add a feature to JobPipeline that automatically gets data for the send() from here { use SerializesModels; diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 7e12a857..63a22a11 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy; use Illuminate\Cache\CacheManager; +use Illuminate\Database\Console\Migrations\FreshCommand; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; @@ -90,6 +91,10 @@ class TenancyServiceProvider extends ServiceProvider Commands\Up::class, ]); + $this->app->extend(FreshCommand::class, function () { + return new Commands\MigrateFreshOverride; + }); + $this->publishes([ __DIR__ . '/../assets/config.php' => config_path('tenancy.php'), ], 'config'); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 219d87b4..bac5005d 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -2,24 +2,28 @@ declare(strict_types=1); -use Illuminate\Database\DatabaseManager; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Tests\Etc\User; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Jobs\DeleteDomains; +use Illuminate\Support\Facades\Artisan; use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Jobs\DeleteDatabase; +use Illuminate\Database\DatabaseManager; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\TenantDeleted; +use Stancl\Tenancy\Tests\Etc\TestSeeder; +use Stancl\Tenancy\Events\DeletingTenant; +use Stancl\Tenancy\Tests\Etc\ExampleSeeder; +use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\ExampleSeeder; -use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Tests\Etc\TestSeeder; -use Stancl\Tenancy\Tests\Etc\User; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; + beforeEach(function () { Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { @@ -267,6 +271,47 @@ test('run command works when sub command asks questions and accepts arguments', expect($user->email)->toBe('email@localhost'); }); +test('migrate fresh command only deletes tenant databases if drop_tenant_databases_on_migrate_fresh is true', function (bool $dropTenantDBsOnMigrateFresh) { + Event::listen(DeletingTenant::class, + JobPipeline::make([DeleteDomains::class])->send(function (DeletingTenant $event) { + return $event->tenant; + })->shouldBeQueued(false)->toListener() + ); + + Event::listen( + TenantDeleted::class, + JobPipeline::make([DeleteDatabase::class])->send(function (TenantDeleted $event) { + return $event->tenant; + })->shouldBeQueued(false)->toListener() + ); + + config(['tenancy.database.drop_tenant_databases_on_migrate_fresh' => $dropTenantDBsOnMigrateFresh]); + $shouldHaveDBAfterMigrateFresh = ! $dropTenantDBsOnMigrateFresh; + + /** @var Tenant[] $tenants */ + $tenants = [ + Tenant::create(), + Tenant::create(), + Tenant::create(), + ]; + + $tenantHasDatabase = fn (Tenant $tenant) => $tenant->database()->manager()->databaseExists($tenant->database()->getName()); + + foreach ($tenants as $tenant) { + expect($tenantHasDatabase($tenant))->toBeTrue(); + } + + pest()->artisan('migrate:fresh', [ + '--force' => true, + '--path' => __DIR__ . '/../assets/migrations', + '--realpath' => true, + ]); + + foreach ($tenants as $tenant) { + expect($tenantHasDatabase($tenant))->toBe($shouldHaveDBAfterMigrateFresh); + } +})->with([true, false]); + // todo@tests function runCommandWorks(): void { From 3f60c4a652b20ca8c988c2821374af1d38df6508 Mon Sep 17 00:00:00 2001 From: Jori Stein <44996807+stein-j@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:48:24 -0400 Subject: [PATCH 3/8] Add maintenance mode events (#979) --- assets/TenancyServiceProvider.stub.php | 2 ++ src/Database/Concerns/MaintenanceMode.php | 6 ++++++ src/Events/TenantMaintenanceModeDisabled.php | 10 ++++++++++ src/Events/TenantMaintenanceModeEnabled.php | 10 ++++++++++ tests/MaintenanceModeTest.php | 15 +++++++++++++++ 5 files changed, 43 insertions(+) create mode 100644 src/Events/TenantMaintenanceModeDisabled.php create mode 100644 src/Events/TenantMaintenanceModeEnabled.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 75784361..7c52e295 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -58,6 +58,8 @@ class TenancyServiceProvider extends ServiceProvider return $event->tenant; })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. ], + Events\TenantMaintenanceModeEnabled::class => [], + Events\TenantMaintenanceModeDisabled::class => [], // Domain events Events\CreatingDomain::class => [], diff --git a/src/Database/Concerns/MaintenanceMode.php b/src/Database/Concerns/MaintenanceMode.php index cc4490f6..c147f748 100644 --- a/src/Database/Concerns/MaintenanceMode.php +++ b/src/Database/Concerns/MaintenanceMode.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; +use Stancl\Tenancy\Events\TenantMaintenanceModeDisabled; +use Stancl\Tenancy\Events\TenantMaintenanceModeEnabled; /** * @mixin \Illuminate\Database\Eloquent\Model @@ -21,10 +23,14 @@ trait MaintenanceMode 'status' => $data['status'] ?? 503, ], ]); + + event(new TenantMaintenanceModeEnabled($this)); } public function bringUpFromMaintenance(): void { $this->update(['maintenance_mode' => null]); + + event(new TenantMaintenanceModeDisabled($this)); } } diff --git a/src/Events/TenantMaintenanceModeDisabled.php b/src/Events/TenantMaintenanceModeDisabled.php new file mode 100644 index 00000000..5b42cff6 --- /dev/null +++ b/src/Events/TenantMaintenanceModeDisabled.php @@ -0,0 +1,10 @@ +get('http://acme.localhost/foo')->assertStatus(200); }); +test('maintenance mode events are fired', function () { + $tenant = MaintenanceTenant::create(); + + Event::fake(); + + $tenant->putDownForMaintenance(); + + Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeEnabled::class); + + $tenant->bringUpFromMaintenance(); + + Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeDisabled::class); +}); + test('tenants can be put into maintenance mode using artisan commands', function() { Route::get('/foo', function () { return 'bar'; From 693e00b9be9daf7fb3ada777d5a510f7ec424950 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Mon, 17 Oct 2022 17:48:49 +0000 Subject: [PATCH 4/8] Fix code style (php-cs-fixer) --- src/Database/Concerns/MaintenanceMode.php | 1 + src/Events/TenantMaintenanceModeDisabled.php | 2 +- src/Events/TenantMaintenanceModeEnabled.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Database/Concerns/MaintenanceMode.php b/src/Database/Concerns/MaintenanceMode.php index c147f748..1ad173cf 100644 --- a/src/Database/Concerns/MaintenanceMode.php +++ b/src/Database/Concerns/MaintenanceMode.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; + use Stancl\Tenancy\Events\TenantMaintenanceModeDisabled; use Stancl\Tenancy\Events\TenantMaintenanceModeEnabled; diff --git a/src/Events/TenantMaintenanceModeDisabled.php b/src/Events/TenantMaintenanceModeDisabled.php index 5b42cff6..5b2d9778 100644 --- a/src/Events/TenantMaintenanceModeDisabled.php +++ b/src/Events/TenantMaintenanceModeDisabled.php @@ -7,4 +7,4 @@ namespace Stancl\Tenancy\Events; class TenantMaintenanceModeDisabled extends Contracts\TenantEvent { // -} \ No newline at end of file +} diff --git a/src/Events/TenantMaintenanceModeEnabled.php b/src/Events/TenantMaintenanceModeEnabled.php index 39d01032..752c83a5 100644 --- a/src/Events/TenantMaintenanceModeEnabled.php +++ b/src/Events/TenantMaintenanceModeEnabled.php @@ -7,4 +7,4 @@ namespace Stancl\Tenancy\Events; class TenantMaintenanceModeEnabled extends Contracts\TenantEvent { // -} \ No newline at end of file +} From 05f1b2d6f5fbda5762fd0a21aaa668ac6d14003a Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Tue, 18 Oct 2022 16:52:16 +0500 Subject: [PATCH 5/8] Add cookie option on Initialize Tenancy by Request identification (#980) * Add cookie option on Initialize Tenancy by Request identification * add cookie property --- .../InitializeTenancyByRequestData.php | 16 +++++++++++----- tests/RequestDataIdentificationTest.php | 19 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php index ba587d9a..ca29f3d7 100644 --- a/src/Middleware/InitializeTenancyByRequestData.php +++ b/src/Middleware/InitializeTenancyByRequestData.php @@ -12,6 +12,7 @@ use Stancl\Tenancy\Tenancy; class InitializeTenancyByRequestData extends IdentificationMiddleware { public static string $header = 'X-Tenant'; + public static string $cookie = 'X-Tenant'; public static string $queryParameter = 'tenant'; public static ?Closure $onFail = null; @@ -33,13 +34,18 @@ class InitializeTenancyByRequestData extends IdentificationMiddleware protected function getPayload(Request $request): ?string { - $tenant = null; if (static::$header && $request->hasHeader(static::$header)) { - $tenant = $request->header(static::$header); - } elseif (static::$queryParameter && $request->has(static::$queryParameter)) { - $tenant = $request->get(static::$queryParameter); + return $request->header(static::$header); } - return $tenant; + if (static::$queryParameter && $request->has(static::$queryParameter)) { + return $request->get(static::$queryParameter); + } + + if (static::$cookie && $request->hasCookie(static::$cookie)) { + return $request->cookie(static::$cookie); + } + + return null; } } diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index e5a05f65..e10c00e1 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -20,19 +20,18 @@ beforeEach(function () { afterEach(function () { InitializeTenancyByRequestData::$header = 'X-Tenant'; + InitializeTenancyByRequestData::$cookie = 'X-Tenant'; InitializeTenancyByRequestData::$queryParameter = 'tenant'; }); test('header identification works', function () { InitializeTenancyByRequestData::$header = 'X-Tenant'; $tenant = Tenant::create(); - $tenant2 = Tenant::create(); $this ->withoutExceptionHandling() - ->get('test', [ - 'X-Tenant' => $tenant->id, - ]) + ->withHeader('X-Tenant', $tenant->id) + ->get('test') ->assertSee($tenant->id); }); @@ -40,10 +39,20 @@ test('query parameter identification works', function () { InitializeTenancyByRequestData::$queryParameter = 'tenant'; $tenant = Tenant::create(); - $tenant2 = Tenant::create(); $this ->withoutExceptionHandling() ->get('test?tenant=' . $tenant->id) ->assertSee($tenant->id); }); + +test('cookie identification works', function () { + InitializeTenancyByRequestData::$cookie = 'X-Tenant'; + $tenant = Tenant::create(); + + $this + ->withoutExceptionHandling() + ->withUnencryptedCookie('X-Tenant', $tenant->id) + ->get('test',) + ->assertSee($tenant->id); +}); From e4f5b9248540fe03b2c03989e38b9e5aa67b5a6e Mon Sep 17 00:00:00 2001 From: Jori Stein <44996807+stein-j@users.noreply.github.com> Date: Tue, 18 Oct 2022 13:11:57 -0400 Subject: [PATCH 6/8] [4.x] Update commands CLI outputs (#968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Using laravel components * Ensure commands returns success * update tests * clean * bump EndBug version * Update ci.yml * Update ci.yml * Update ci.yml * revert CI changes * Update ci.yml * Update ci.yml * Update ci.yml * revert CI changes to it's original state * fix typos, improve code * improve Install & TenantList commands * php-cs-fixer * type GitHub properly Co-authored-by: Abrar Ahmad Co-authored-by: Samuel Štancl Co-authored-by: Samuel Štancl --- .github/workflows/ci.yml | 1 + assets/config.php | 8 +- src/Commands/Down.php | 15 ++-- src/Commands/Install.php | 146 +++++++++++++++++++++++++++------- src/Commands/Link.php | 10 ++- src/Commands/Migrate.php | 2 +- src/Commands/MigrateFresh.php | 30 ++++--- src/Commands/Rollback.php | 2 +- src/Commands/Run.php | 16 ++-- src/Commands/Seed.php | 2 +- src/Commands/TenantDump.php | 18 ++--- src/Commands/TenantList.php | 33 ++++++-- src/Commands/Up.php | 8 +- tests/CommandsTest.php | 14 ++-- tests/MaintenanceModeTest.php | 8 +- 15 files changed, 217 insertions(+), 96 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db2b0ffc..26de6a18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,3 +102,4 @@ jobs: author_name: "PHP CS Fixer" author_email: "phpcsfixer@example.com" message: Fix code style (php-cs-fixer) + diff --git a/assets/config.php b/assets/config.php index cd0a6c42..eb68d9b0 100644 --- a/assets/config.php +++ b/assets/config.php @@ -2,16 +2,14 @@ declare(strict_types=1); -use Stancl\Tenancy\Database\Models\Domain; -use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Middleware; use Stancl\Tenancy\Resolvers; return [ - 'tenant_model' => Tenant::class, - 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + 'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class, + 'domain_model' => Stancl\Tenancy\Database\Models\Domain::class, - 'domain_model' => Domain::class, + 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, /** * The list of domains hosting your central app. diff --git a/src/Commands/Down.php b/src/Commands/Down.php index 6b390957..e7341d7f 100644 --- a/src/Commands/Down.php +++ b/src/Commands/Down.php @@ -22,24 +22,23 @@ class Down extends DownCommand public function handle(): int { - // The base down command is heavily used. Instead of saving the data inside a file, - // the data is stored the tenant database, which means some Laravel features - // are not available with tenants. - $payload = $this->getDownDatabasePayload(); - // This runs for all tenants if no --tenants are specified tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) { - $this->line("Tenant: {$tenant['id']}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $tenant->putDownForMaintenance($payload); }); - $this->comment('Tenants are now in maintenance mode.'); + $this->components->info('Tenants are now in maintenance mode.'); return 0; } - /** Get the payload to be placed in the "down" file. */ + /** + * Get the payload to be placed in the "down" file. This + * payload is the same as the original function + * but without the 'template' option. + */ protected function getDownDatabasePayload(): array { return [ diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 12a2c2c9..424aa261 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -4,50 +4,136 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; +use Closure; use Illuminate\Console\Command; class Install extends Command { protected $signature = 'tenancy:install'; - protected $description = 'Install stancl/tenancy.'; + protected $description = 'Install Tenancy for Laravel.'; - public function handle(): void + public function handle(): int { - $this->comment('Installing stancl/tenancy...'); - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'config', - ]); - $this->info('✔️ Created config/tenancy.php'); + $this->step( + name: 'Publishing config file', + tag: 'config', + file: 'config/tenancy.php', + newLineBefore: true, + ); - if (! file_exists(base_path('routes/tenant.php'))) { - $this->callSilent('vendor:publish', [ + $this->step( + name: 'Publishing routes [routes/tenant.php]', + tag: 'routes', + file: 'routes/tenant.php', + ); + + $this->step( + name: 'Publishing service provider', + tag: 'providers', + file: 'app/Providers/TenancyServiceProvider.php', + ); + + $this->step( + name: 'Publishing migrations', + tag: 'migrations', + files: [ + 'database/migrations/2019_09_15_000010_create_tenants_table.php', + 'database/migrations/2019_09_15_000020_create_domains_table.php', + ], + warning: 'Migrations already exist', + ); + + $this->step( + name: 'Creating [database/migrations/tenant] folder', + task: fn () => mkdir(database_path('migrations/tenant')), + unless: is_dir(database_path('migrations/tenant')), + warning: 'Folder [database/migrations/tenant] already exists.', + newLineAfter: true, + ); + + $this->components->info('✨️ Tenancy for Laravel successfully installed.'); + + $this->askForSupport(); + + return 0; + } + + /** + * Run a step of the installation process. + * + * @param string $name The name of the step. + * @param Closure|null $task The task code. + * @param bool $unless Condition specifying when the task should NOT run. + * @param string|null $warning Warning shown when the $unless condition is true. + * @param string|null $file Name of the file being added. + * @param string|null $tag The tag being published. + * @param array|null $files Names of files being added. + * @param bool $newLineBefore Should a new line be printed after the step. + * @param bool $newLineAfter Should a new line be printed after the step. + */ + protected function step( + string $name, + Closure $task = null, + bool $unless = false, + string $warning = null, + string $file = null, + string $tag = null, + array $files = null, + bool $newLineBefore = false, + bool $newLineAfter = false, + ): void { + if ($file) { + $name .= " [$file]"; // Append clickable path to the task name + $unless = file_exists(base_path($file)); // Make the condition a check for the file's existence + $warning = "File [$file] already exists."; // Make the warning a message about the file already existing + } + + if ($tag) { + $task = fn () => $this->callSilent('vendor:publish', [ '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'routes', + '--tag' => $tag, ]); - $this->info('✔️ Created routes/tenant.php'); + } + + if ($files) { + // Show a warning if any of the files already exist + $unless = count(array_filter($files, fn ($file) => file_exists(base_path($file)))) !== 0; + } + + if (! $unless) { + if ($newLineBefore) { + $this->newLine(); + } + + $this->components->task($name, $task ?? fn () => null); + + if ($files) { + // Print out a clickable list of the added files + $this->components->bulletList(array_map(fn (string $file) => "[$file]", $files)); + } + + if ($newLineAfter) { + $this->newLine(); + } } else { - $this->info('Found routes/tenant.php.'); + $this->components->warn($warning); } + } - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'providers', - ]); - $this->info('✔️ Created TenancyServiceProvider.php'); - - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'migrations', - ]); - $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!'); - - if (! is_dir(database_path('migrations/tenant'))) { - mkdir(database_path('migrations/tenant')); - $this->info('✔️ Created database/migrations/tenant folder.'); + /** If the user accepts, opens the GitHub project in the browser. */ + public function askForSupport(): void + { + if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) { + if (PHP_OS_FAMILY === 'Darwin') { + exec('open https://github.com/archtechx/tenancy'); + } + if (PHP_OS_FAMILY === 'Windows') { + exec('start https://github.com/archtechx/tenancy'); + } + if (PHP_OS_FAMILY === 'Linux') { + exec('xdg-open https://github.com/archtechx/tenancy'); + } } - - $this->comment('✨️ stancl/tenancy installed successfully.'); } } diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 53f3cf6f..0a587122 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -23,7 +23,7 @@ class Link extends Command protected $description = 'Create or remove tenant symbolic links.'; - public function handle(): void + public function handle(): int { $tenants = $this->getTenants(); @@ -35,14 +35,18 @@ class Link extends Command } } catch (Exception $exception) { $this->error($exception->getMessage()); + + return 1; } + + return 0; } protected function removeLinks(LazyCollection $tenants): void { RemoveStorageSymlinksAction::handle($tenants); - $this->info('The links have been removed.'); + $this->components->info('The links have been removed.'); } protected function createLinks(LazyCollection $tenants): void @@ -53,6 +57,6 @@ class Link extends Command (bool) ($this->option('force') ?? false), ); - $this->info('The links have been created.'); + $this->components->info('The links have been created.'); } } diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 739b56de..82395fcc 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -43,7 +43,7 @@ class Migrate extends MigrateCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new MigratingDatabase($tenant)); diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 32dc6ee5..657c4990 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -23,23 +23,27 @@ class MigrateFresh extends Command $this->setName('tenants:migrate-fresh'); } - public function handle(): void + public function handle(): int { tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->info('Dropping tables.'); - $this->call('db:wipe', array_filter([ - '--database' => 'tenant', - '--drop-views' => $this->option('drop-views'), - '--force' => true, - ])); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); - $this->info('Migrating.'); - $this->callSilent('tenants:migrate', [ - '--tenants' => [$tenant->getTenantKey()], - '--force' => true, - ]); + $this->components->task('Dropping tables', function () { + $this->callSilently('db:wipe', array_filter([ + '--database' => 'tenant', + '--drop-views' => $this->option('drop-views'), + '--force' => true, + ])); + }); + + $this->components->task('Migrating', function () use ($tenant) { + $this->callSilent('tenants:migrate', [ + '--tenants' => [$tenant->getTenantKey()], + '--force' => true, + ]); + }); }); - $this->info('Done.'); + return 0; } } diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index d3989cc0..1e84ab12 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -37,7 +37,7 @@ class Rollback extends RollbackCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new RollingBackDatabase($tenant)); diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 9bb04716..5ecc7c77 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -19,30 +19,32 @@ class Run extends Command protected $signature = 'tenants:run {commandname : The artisan command.} {--tenants=* : The tenant(s) to run the command for. Default: all}'; - public function handle(): void + public function handle(): int { $argvInput = $this->argvInput(); tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $this->getLaravel() ->make(Kernel::class) ->handle($argvInput, new ConsoleOutput); }); + + return 0; } protected function argvInput(): ArgvInput { - /** @var string $commandname */ - $commandname = $this->argument('commandname'); + /** @var string $commandName */ + $commandName = $this->argument('commandname'); // Convert string command to array - $subcommand = explode(' ', $commandname); + $subCommand = explode(' ', $commandName); // Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it - array_unshift($subcommand, 'artisan'); + array_unshift($subCommand, 'artisan'); - return new ArgvInput($subcommand); + return new ArgvInput($subCommand); } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 496c04e6..8ed0b6d9 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -36,7 +36,7 @@ class Seed extends SeedCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new SeedingDatabase($tenant)); diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 9c8698c6..6edae6b0 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; -use Illuminate\Console\Command; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\DumpCommand; @@ -22,13 +21,6 @@ class TenantDump extends DumpCommand } public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int - { - $this->tenant()->run(fn () => parent::handle($connections, $dispatcher)); - - return Command::SUCCESS; - } - - public function tenant(): Tenant { $tenant = $this->option('tenant') ?? tenant() @@ -39,9 +31,15 @@ class TenantDump extends DumpCommand $tenant = tenancy()->find($tenant); } - throw_if(! $tenant, 'Could not identify the tenant to use for dumping the schema.'); + if ($tenant === null) { + $this->components->error('Could not find tenant to use for dumping the schema.'); - return $tenant; + return 1; + } + + parent::handle($connections, $dispatcher); + + return 0; } protected function getOptions(): array diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index 9fd3f8bd..c008ba59 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Tenant; @@ -14,19 +15,35 @@ class TenantList extends Command protected $description = 'List tenants.'; - public function handle(): void + public function handle(): int { - $this->info('Listing all tenants.'); - $tenants = tenancy()->query()->cursor(); + $this->components->info("Listing {$tenants->count()} tenants."); + foreach ($tenants as $tenant) { /** @var Model&Tenant $tenant */ - if ($tenant->domains) { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); - } else { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); - } + $this->components->twoColumnDetail($this->tenantCLI($tenant), $this->domainsCLI($tenant->domains)); } + + $this->newLine(); + + return 0; + } + + /** Generate the visual CLI output for the tenant name. */ + protected function tenantCLI(Model&Tenant $tenant): string + { + return "{$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"; + } + + /** Generate the visual CLI output for the domain names. */ + protected function domainsCLI(?Collection $domains): ?string + { + if (! $domains) { + return null; + } + + return "{$domains->pluck('domain')->implode(' / ')}"; } } diff --git a/src/Commands/Up.php b/src/Commands/Up.php index a3f690c2..08c935c3 100644 --- a/src/Commands/Up.php +++ b/src/Commands/Up.php @@ -15,13 +15,15 @@ class Up extends Command protected $description = 'Put tenants out of maintenance mode.'; - public function handle(): void + public function handle(): int { tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $tenant->bringUpFromMaintenance(); }); - $this->comment('Tenants are now out of maintenance mode.'); + $this->components->info('Tenants are now out of maintenance mode.'); + + return 0; } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index bac5005d..793dca30 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -171,7 +171,9 @@ test('install command works', function () { mkdir($dir, 0777, true); } - pest()->artisan('tenancy:install'); + pest()->artisan('tenancy:install') + ->expectsConfirmation('Would you like to show your support by starring the project on GitHub?', 'no') + ->assertExitCode(0); expect(base_path('routes/tenant.php'))->toBeFile(); expect(base_path('config/tenancy.php'))->toBeFile(); expect(app_path('Providers/TenancyServiceProvider.php'))->toBeFile(); @@ -214,7 +216,8 @@ test('run command with array of tenants works', function () { test('link command works', function() { $tenantId1 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey(); - pest()->artisan('tenants:link'); + pest()->artisan('tenants:link') + ->assertExitCode(0); $this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public")); $this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1"))); @@ -224,7 +227,7 @@ test('link command works', function() { pest()->artisan('tenants:link', [ '--remove' => true, - ]); + ])->assertExitCode(0); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId1")); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId2")); @@ -256,8 +259,9 @@ test('run command works when sub command asks questions and accepts arguments', pest()->artisan("tenants:run --tenants=$id 'user:addwithname Abrar' ") ->expectsQuestion('What is your email?', 'email@localhost') - ->expectsOutput("Tenant: $id") - ->expectsOutput("User created: Abrar(email@localhost)"); + ->expectsOutputToContain("Tenant: $id.") + ->expectsOutput("User created: Abrar(email@localhost)") + ->assertExitCode(0); // Assert we are in central context expect(tenancy()->initialized)->toBeFalse(); diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index e31fe026..86366e2f 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -59,12 +59,18 @@ test('tenants can be put into maintenance mode using artisan commands', function pest()->get('http://acme.localhost/foo')->assertStatus(200); + pest()->artisan('tenants:down') + ->expectsOutputToContain('Tenants are now in maintenance mode.') + ->assertExitCode(0); + Artisan::call('tenants:down'); tenancy()->end(); // End tenancy before making a request pest()->get('http://acme.localhost/foo')->assertStatus(503); - Artisan::call('tenants:up'); + pest()->artisan('tenants:up') + ->expectsOutputToContain('Tenants are now out of maintenance mode.') + ->assertExitCode(0); tenancy()->end(); // End tenancy before making a request pest()->get('http://acme.localhost/foo')->assertStatus(200); From 5d688e6e5df7db2dc39ae62c2ccafa3204a41554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 18 Oct 2022 21:52:02 +0200 Subject: [PATCH 7/8] remove duplicate 'routes/tenant.php' from message --- src/Commands/Install.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 424aa261..77c96588 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -23,7 +23,7 @@ class Install extends Command ); $this->step( - name: 'Publishing routes [routes/tenant.php]', + name: 'Publishing routes', tag: 'routes', file: 'routes/tenant.php', ); From fe0a322b87969f42031c411a48479046bd54fb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 25 Oct 2022 12:53:31 +0200 Subject: [PATCH 8/8] add public connection() method to the Tenant DB manager interface --- .../StatefulTenantDatabaseManager.php | 22 +++++++++++++++++++ .../Contracts/TenantDatabaseManager.php | 9 -------- src/Database/DatabaseConfig.php | 4 +++- .../TenantDatabaseManager.php | 6 ++--- tests/DatabasePreparationTest.php | 4 +--- tests/TenantDatabaseManagerTest.php | 6 ++++- 6 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 src/Database/Contracts/StatefulTenantDatabaseManager.php diff --git a/src/Database/Contracts/StatefulTenantDatabaseManager.php b/src/Database/Contracts/StatefulTenantDatabaseManager.php new file mode 100644 index 00000000..36a08db2 --- /dev/null +++ b/src/Database/Contracts/StatefulTenantDatabaseManager.php @@ -0,0 +1,22 @@ +setConnection($this->getTemplateConnectionName()); + if ($databaseManager instanceof Contracts\StatefulTenantDatabaseManager) { + $databaseManager->setConnection($this->getTemplateConnectionName()); + } return $databaseManager; } diff --git a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php index b7dd15fa..87916088 100644 --- a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php @@ -6,15 +6,15 @@ namespace Stancl\Tenancy\Database\TenantDatabaseManagers; use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager as Contract; +use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager; use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; -abstract class TenantDatabaseManager implements Contract // todo better naming? +abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager { /** The database connection to the server. */ protected string $connection; - protected function database(): Connection + public function database(): Connection { if (! isset($this->connection)) { throw new NoConnectionSetException(static::class); diff --git a/tests/DatabasePreparationTest.php b/tests/DatabasePreparationTest.php index e31fac9b..d5641af4 100644 --- a/tests/DatabasePreparationTest.php +++ b/tests/DatabasePreparationTest.php @@ -22,9 +22,7 @@ test('database can be created after tenant creation', function () { })->toListener()); $tenant = Tenant::create(); - - $manager = app(MySQLDatabaseManager::class); - $manager->setConnection('mysql'); + $manager = $tenant->database()->manager(); expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); }); diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index b16c06b6..33a3158f 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; @@ -36,7 +37,10 @@ test('databases can be created and deleted', function ($driver, $databaseManager $name = 'db' . pest()->randomString(); $manager = app($databaseManager); - $manager->setConnection($driver); + + if ($manager instanceof StatefulTenantDatabaseManager) { + $manager->setConnection($driver); + } expect($manager->databaseExists($name))->toBeFalse();