From f1e03fb9212c02c4cd4dc17160ba501e97962251 Mon Sep 17 00:00:00 2001 From: Jimish Gamit Date: Mon, 2 Mar 2026 12:26:03 +0530 Subject: [PATCH 1/5] feat: option to skip tenant --- src/Commands/Run.php | 3 ++- src/Concerns/HasTenantOptions.php | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 5beb3268..42cebf79 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -10,7 +10,7 @@ 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 { @@ -18,6 +18,7 @@ trait HasTenantOptions { return array_merge([ ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null], + ['skip-tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to skip when running this command', null], ['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')); }); From d0407fc58d3a659219103700b79a0f60d72e407b Mon Sep 17 00:00:00 2001 From: Jimish Gamit Date: Mon, 2 Mar 2026 13:18:19 +0530 Subject: [PATCH 2/5] fix: phpstan getOptions issue --- src/Concerns/HasTenantOptions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index 42cebf79..5b41d31a 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -17,9 +17,9 @@ trait HasTenantOptions protected function getOptions() { return array_merge([ - ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null], - ['skip-tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to skip when running this command', null], - ['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()); } From d51a27d802de24d66c539d9db8618529039b764a Mon Sep 17 00:00:00 2001 From: Jimish Gamit Date: Mon, 2 Mar 2026 13:21:30 +0530 Subject: [PATCH 3/5] fix: TenantDump command getOptions phpstan fix --- src/Commands/TenantDump.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 32677efc..97f9d539 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -63,7 +63,7 @@ class TenantDump extends DumpCommand protected function getOptions(): array { return array_merge([ - ['tenant', null, InputOption::VALUE_OPTIONAL, '', null], + new InputOption('tenant', null, InputOption::VALUE_OPTIONAL, '', null), ], parent::getOptions()); } } From e68aedb5eb92ad3558a9314ab3f73e3e6cee9a0d Mon Sep 17 00:00:00 2001 From: Jimish Gamit Date: Mon, 2 Mar 2026 13:31:22 +0530 Subject: [PATCH 4/5] add: test cases implementation --- tests/CommandsTest.php | 131 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index a5b3b856..1cfa3c3b 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -515,3 +515,134 @@ test('migrate fresh command only deletes tenant databases if drop_tenant_databas expect($tenantHasDatabase($tenant))->toBe($shouldHaveDBAfterMigrateFresh); } })->with([true, false]); + +test('migrate command skips specified tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate', [ + '--skip-tenants' => [$tenant2->getTenantKey()], + ]); + + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->end(); + + tenancy()->initialize($tenant3); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); +}); + +test('migrate command skips multiple tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate', [ + '--skip-tenants' => [$tenant1->getTenantKey(), $tenant2->getTenantKey()], + ]); + + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->end(); + + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->end(); + + tenancy()->initialize($tenant3); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); +}); + +test('run command skips specified tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate-fresh'); + + $id1 = $tenant1->getTenantKey(); + $id2 = $tenant2->getTenantKey(); + $id3 = $tenant3->getTenantKey(); + + pest()->artisan("tenants:run --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") + ->expectsOutputToContain("Tenant: $id1") + ->doesntExpectOutputToContain("Tenant: $id2") + ->expectsOutputToContain("Tenant: $id3") + ->assertExitCode(0); +}); + +test('run command skips multiple tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate-fresh'); + + $id1 = $tenant1->getTenantKey(); + $id2 = $tenant2->getTenantKey(); + $id3 = $tenant3->getTenantKey(); + + pest()->artisan("tenants:run --skip-tenants=$id1 --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") + ->doesntExpectOutputToContain("Tenant: $id1") + ->doesntExpectOutputToContain("Tenant: $id2") + ->expectsOutputToContain("Tenant: $id3") + ->assertExitCode(0); +}); + +test('tenants and skip-tenants options can be used together', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + $tenant3 = Tenant::create(); + + Artisan::call('tenants:migrate-fresh'); + + $id1 = $tenant1->getTenantKey(); + $id2 = $tenant2->getTenantKey(); + $id3 = $tenant3->getTenantKey(); + + // Scope to tenant1+tenant2, then skip tenant2 — only tenant1 should run + pest()->artisan("tenants:run --tenants=$id1 --tenants=$id2 --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") + ->expectsOutputToContain("Tenant: $id1") + ->doesntExpectOutputToContain("Tenant: $id2") + ->doesntExpectOutputToContain("Tenant: $id3") + ->assertExitCode(0); +}); + +test('migrate-fresh command skips specified tenants', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + // Migrate all tenants first so both have the users table + Artisan::call('tenants:migrate'); + + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + // migrate-fresh on tenant1 only (skip tenant2) + pest()->artisan('tenants:migrate-fresh', [ + '--skip-tenants' => [$tenant2->getTenantKey()], + '--force' => true, + ])->assertExitCode(0); + + // tenant1 should still have the table (re-created by migrate-fresh) + tenancy()->initialize($tenant1); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); + + // tenant2 was skipped, so its DB is untouched — table still exists + tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeTrue(); + tenancy()->end(); +}); From 00f4e9fcc0a1684c0579b8e25fb566f97ac05c2c Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 5 May 2026 11:59:48 +0200 Subject: [PATCH 5/5] Simplify tests --- tests/CommandsTest.php | 133 ++++++++--------------------------------- 1 file changed, 25 insertions(+), 108 deletions(-) diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 1cfa3c3b..bda3eea9 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -516,133 +516,50 @@ test('migrate fresh command only deletes tenant databases if drop_tenant_databas } })->with([true, false]); -test('migrate command skips specified tenants', function () { +test('migrate commands can skip specified tenants', function (string $command) { $tenant1 = Tenant::create(); $tenant2 = Tenant::create(); $tenant3 = Tenant::create(); - Artisan::call('tenants:migrate', [ - '--skip-tenants' => [$tenant2->getTenantKey()], - ]); + pest()->artisan("{$command} --skip-tenants={$tenant1->getTenantKey()} --skip-tenants={$tenant2->getTenantKey()}"); tenancy()->initialize($tenant1); - expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); + + expect(Schema::hasTable('users'))->toBeFalse(); tenancy()->initialize($tenant2); + expect(Schema::hasTable('users'))->toBeFalse(); - tenancy()->end(); tenancy()->initialize($tenant3); + expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); -}); +})->with([ + 'tenants:migrate', + 'tenants:migrate-fresh', +]); -test('migrate command skips multiple tenants', function () { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - $tenant3 = Tenant::create(); +test('run command can skip specified tenants', function () { + $tenant1 = Tenant::create()->getTenantKey(); + $tenant2 = Tenant::create()->getTenantKey(); + $tenant3 = Tenant::create()->getTenantKey(); - Artisan::call('tenants:migrate', [ - '--skip-tenants' => [$tenant1->getTenantKey(), $tenant2->getTenantKey()], - ]); - - tenancy()->initialize($tenant1); - expect(Schema::hasTable('users'))->toBeFalse(); - tenancy()->end(); - - tenancy()->initialize($tenant2); - expect(Schema::hasTable('users'))->toBeFalse(); - tenancy()->end(); - - tenancy()->initialize($tenant3); - expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); -}); - -test('run command skips specified tenants', function () { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - $tenant3 = Tenant::create(); - - Artisan::call('tenants:migrate-fresh'); - - $id1 = $tenant1->getTenantKey(); - $id2 = $tenant2->getTenantKey(); - $id3 = $tenant3->getTenantKey(); - - pest()->artisan("tenants:run --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") - ->expectsOutputToContain("Tenant: $id1") - ->doesntExpectOutputToContain("Tenant: $id2") - ->expectsOutputToContain("Tenant: $id3") - ->assertExitCode(0); -}); - -test('run command skips multiple tenants', function () { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - $tenant3 = Tenant::create(); - - Artisan::call('tenants:migrate-fresh'); - - $id1 = $tenant1->getTenantKey(); - $id2 = $tenant2->getTenantKey(); - $id3 = $tenant3->getTenantKey(); - - pest()->artisan("tenants:run --skip-tenants=$id1 --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") - ->doesntExpectOutputToContain("Tenant: $id1") - ->doesntExpectOutputToContain("Tenant: $id2") - ->expectsOutputToContain("Tenant: $id3") + 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(); - $tenant2 = Tenant::create(); - $tenant3 = Tenant::create(); - - Artisan::call('tenants:migrate-fresh'); - - $id1 = $tenant1->getTenantKey(); - $id2 = $tenant2->getTenantKey(); - $id3 = $tenant3->getTenantKey(); + $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=$id1 --tenants=$id2 --skip-tenants=$id2 'foo foo --b=bar --c=xyz'") - ->expectsOutputToContain("Tenant: $id1") - ->doesntExpectOutputToContain("Tenant: $id2") - ->doesntExpectOutputToContain("Tenant: $id3") + 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); }); - -test('migrate-fresh command skips specified tenants', function () { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - - // Migrate all tenants first so both have the users table - Artisan::call('tenants:migrate'); - - tenancy()->initialize($tenant1); - expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); - - tenancy()->initialize($tenant2); - expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); - - // migrate-fresh on tenant1 only (skip tenant2) - pest()->artisan('tenants:migrate-fresh', [ - '--skip-tenants' => [$tenant2->getTenantKey()], - '--force' => true, - ])->assertExitCode(0); - - // tenant1 should still have the table (re-created by migrate-fresh) - tenancy()->initialize($tenant1); - expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); - - // tenant2 was skipped, so its DB is untouched — table still exists - tenancy()->initialize($tenant2); - expect(Schema::hasTable('users'))->toBeTrue(); - tenancy()->end(); -});