diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 7dd69e0f..d3435ca2 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -17,7 +17,8 @@ class Run extends Command protected $description = 'Run a command for tenant(s)'; protected $signature = 'tenants:run {commandname : The artisan command.} - {--tenants=* : The tenant(s) to run the command for. Default: all}'; + {--tenants=* : The tenant(s) to run the command for. Default: all} + {--skip-tenants=* : The tenant(s) to skip}'; public function handle(): int { diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index c1ea221f..5b41d31a 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -10,15 +10,16 @@ use Stancl\Tenancy\Database\Concerns\PendingScope; use Symfony\Component\Console\Input\InputOption; /** - * Adds 'tenants' and 'with-pending' options. + * Adds 'tenants', 'skip-tenants', and 'with-pending' options. */ trait HasTenantOptions { protected function getOptions() { return array_merge([ - new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null), - new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs + new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null), + new InputOption('skip-tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to skip when running this command', null), + new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs ], parent::getOptions()); } @@ -42,6 +43,9 @@ trait HasTenantOptions ->when($this->option('tenants'), function ($query) { $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); }) + ->when($this->option('skip-tenants'), function ($query) { + $query->whereNotIn(tenancy()->model()->getTenantKeyName(), $this->option('skip-tenants')); + }) ->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) { $query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending')); }); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index a5b3b856..bda3eea9 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -515,3 +515,51 @@ test('migrate fresh command only deletes tenant databases if drop_tenant_databas expect($tenantHasDatabase($tenant))->toBe($shouldHaveDBAfterMigrateFresh); } })->with([true, false]); + +test('migrate commands can skip specified tenants', function (string $command) { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + pest()->artisan("{$command} --skip-tenants={$tenant1->getTenantKey()} --skip-tenants={$tenant2->getTenantKey()}"); + + tenancy()->initialize($tenant1); + + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant2); + + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant3); + + expect(Schema::hasTable('users'))->toBeTrue(); +})->with([ + 'tenants:migrate', + 'tenants:migrate-fresh', +]); + +test('run command can skip specified tenants', function () { + $tenant1 = Tenant::create()->getTenantKey(); + $tenant2 = Tenant::create()->getTenantKey(); + $tenant3 = Tenant::create()->getTenantKey(); + + pest()->artisan("tenants:run --skip-tenants=$tenant1 --skip-tenants=$tenant2 'bar foo foo@bar foobar arg --option=option'") + ->doesntExpectOutputToContain("Tenant: $tenant1") + ->doesntExpectOutputToContain("Tenant: $tenant2") + ->expectsOutputToContain("Tenant: $tenant3") + ->assertExitCode(0); +}); + +test('tenants and skip-tenants options can be used together', function () { + $tenant1 = Tenant::create()->getTenantKey(); + $tenant2 = Tenant::create()->getTenantKey(); + $tenant3 = Tenant::create()->getTenantKey(); + + // Scope to tenant1+tenant2, then skip tenant2 — only tenant1 should run + pest()->artisan("tenants:run --tenants=$tenant1 --tenants=$tenant2 --skip-tenants=$tenant2 'bar foo foo@bar foobar arg --option=option'") + ->expectsOutputToContain("Tenant: $tenant1") + ->doesntExpectOutputToContain("Tenant: $tenant2") + ->doesntExpectOutputToContain("Tenant: $tenant3") + ->assertExitCode(0); +});