Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Commands/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
10 changes: 7 additions & 3 deletions src/Concerns/HasTenantOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
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());
}

Expand All @@ -42,6 +43,9 @@ protected function getTenantsQuery(?array $tenantKeys = null): Builder
->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'));
});
Expand Down
131 changes: 131 additions & 0 deletions tests/CommandsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,134 @@
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();
});
Loading