diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e316a47..dcd6b6d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -18,7 +18,7 @@ jobs: - laravel: 9.* testbench: 7.* - laravel: 8.* - testbench: ^6.23 + testbench: ^6.35 exclude: - laravel: 10.* php: 8.0 diff --git a/composer.json b/composer.json index 8b9f9f5..c5ec96e 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "require-dev": { "laravel/legacy-factories": "^1.0.4", "laravel/octane": "^1.0", + "laravel/serializable-closure": "^1.1", "mockery/mockery": "^1.4", "orchestra/testbench": "^6.23|^7.0|^8.0", "pestphp/pest": "^1.22", diff --git a/config/multitenancy.php b/config/multitenancy.php index 334c724..4fb1f10 100644 --- a/config/multitenancy.php +++ b/config/multitenancy.php @@ -4,6 +4,7 @@ use Illuminate\Events\CallQueuedListener; use Illuminate\Mail\SendQueuedMailable; use Illuminate\Notifications\SendQueuedNotifications; +use Illuminate\Queue\CallQueuedClosure; use Spatie\Multitenancy\Actions\ForgetCurrentTenantAction; use Spatie\Multitenancy\Actions\MakeQueueTenantAwareAction; use Spatie\Multitenancy\Actions\MakeTenantCurrentAction; @@ -95,6 +96,7 @@ 'queueable_to_job' => [ SendQueuedMailable::class => 'mailable', SendQueuedNotifications::class => 'notification', + CallQueuedClosure::class => 'closure', CallQueuedListener::class => 'class', BroadcastEvent::class => 'event', ], diff --git a/docs/basic-usage/making-queues-tenant-aware.md b/docs/basic-usage/making-queues-tenant-aware.md index dca271c..84a80db 100644 --- a/docs/basic-usage/making-queues-tenant-aware.md +++ b/docs/basic-usage/making-queues-tenant-aware.md @@ -55,6 +55,19 @@ or, using the config `multitenancy.php`: ], ``` +## Queueing Closures + +Dispatch a closure is slightly different from a job class because here, you can't implement `TenantAware` or `NotTenantAware` interfaces. The package can handle the queue closures by enabling the `queues_are_tenant_aware_by_default`, but if you enjoy keeping to `false` parameter, you can dispatch a tenant-aware job closure like so: + +```php +$tenant = Tenant::current(); + +dispatch(function () use ($tenant) { + $tenant->execute(function () { + // Your job + }); +}); +``` ## When the tenant cannot be retrieved diff --git a/docs/installation/base-installation.md b/docs/installation/base-installation.md index 58308d2..7547f58 100644 --- a/docs/installation/base-installation.md +++ b/docs/installation/base-installation.md @@ -26,6 +26,7 @@ use Illuminate\Broadcasting\BroadcastEvent; use Illuminate\Events\CallQueuedListener; use Illuminate\Mail\SendQueuedMailable; use Illuminate\Notifications\SendQueuedNotifications; +use Illuminate\Queue\CallQueuedClosure; use Spatie\Multitenancy\Actions\ForgetCurrentTenantAction; use Spatie\Multitenancy\Actions\MakeQueueTenantAwareAction; use Spatie\Multitenancy\Actions\MakeTenantCurrentAction; @@ -55,7 +56,9 @@ return [ * A valid task is any class that implements Spatie\Multitenancy\Tasks\SwitchTenantTask */ 'switch_tenant_tasks' => [ - // add tasks here + // \Spatie\Multitenancy\Tasks\PrefixCacheTask::class, + // \Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class, + // \Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class, ], /* @@ -89,8 +92,14 @@ return [ */ 'current_tenant_container_key' => 'currentTenant', + /** + * Set it to `true` if you like to cache the tenant(s) routes + * in a shared file using the `SwitchRouteCacheTask`. + */ + 'shared_routes_cache' => false, + /* - * You can customize some of the behavior of this package by using our own custom action. + * You can customize some of the behavior of this package by using your own custom action. * Your custom action should always extend the default one. */ 'actions' => [ @@ -109,9 +118,24 @@ return [ 'queueable_to_job' => [ SendQueuedMailable::class => 'mailable', SendQueuedNotifications::class => 'notification', + CallQueuedClosure::class => 'closure', CallQueuedListener::class => 'class', BroadcastEvent::class => 'event', ], + + /* + * Jobs tenant aware even if these don't implement the TenantAware interface. + */ + 'tenant_aware_jobs' => [ + // ... + ], + + /* + * Jobs not tenant aware even if these don't implement the NotTenantAware interface. + */ + 'not_tenant_aware_jobs' => [ + // ... + ], ]; ``` diff --git a/tests/Feature/Commands/TenantAwareCommandTest.php b/tests/Feature/Commands/TenantAwareCommandTest.php index 50e0a3b..7e09d04 100644 --- a/tests/Feature/Commands/TenantAwareCommandTest.php +++ b/tests/Feature/Commands/TenantAwareCommandTest.php @@ -4,16 +4,11 @@ use Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask; beforeEach(function () { - config(['database.default' => 'tenant']); config()->set('multitenancy.switch_tenant_tasks', [SwitchTenantDatabaseTask::class]); $this->tenant = Tenant::factory()->create(['database' => 'laravel_mt_tenant_1']); - $this->tenant->makeCurrent(); $this->anotherTenant = Tenant::factory()->create(['database' => 'laravel_mt_tenant_2']); - $this->anotherTenant->makeCurrent(); - - Tenant::forgetCurrent(); }); it('fails with a non-existing tenant') diff --git a/tests/Feature/Commands/TenantsArtisanCommandTest.php b/tests/Feature/Commands/TenantsArtisanCommandTest.php index 10b34c1..0f6fe93 100644 --- a/tests/Feature/Commands/TenantsArtisanCommandTest.php +++ b/tests/Feature/Commands/TenantsArtisanCommandTest.php @@ -5,24 +5,18 @@ use Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask; beforeEach(function () { - config(['database.default' => 'tenant']); - config()->set('multitenancy.switch_tenant_tasks', [SwitchTenantDatabaseTask::class]); $this->tenant = Tenant::factory()->create(['database' => 'laravel_mt_tenant_1']); - $this->tenant->makeCurrent(); - Schema::connection('tenant')->dropIfExists('migrations'); + $this->tenant->execute(fn () => Schema::connection('tenant')->dropIfExists('migrations')); $this->anotherTenant = Tenant::factory()->create(['database' => 'laravel_mt_tenant_2']); - $this->anotherTenant->makeCurrent(); - Schema::connection('tenant')->dropIfExists('migrations'); - - Tenant::forgetCurrent(); + $this->anotherTenant->execute(fn () => Schema::connection('tenant')->dropIfExists('migrations')); }); it('can migrate all tenant databases', function () { $this - ->artisan('tenants:artisan migrate') + ->artisan('tenants:artisan "migrate --database=tenant"') ->assertExitCode(0); assertTenantDatabaseHasTable($this->tenant, 'migrations'); @@ -30,7 +24,7 @@ }); it('can migrate a specific tenant', function () { - $this->artisan('tenants:artisan migrate --tenant="' . $this->anotherTenant->id . '"')->assertExitCode(0); + $this->artisan('tenants:artisan "migrate --database=tenant" --tenant="' . $this->anotherTenant->id . '"')->assertExitCode(0); assertTenantDatabaseDoesNotHaveTable($this->tenant, 'migrations'); assertTenantDatabaseHasTable($this->anotherTenant, 'migrations'); @@ -40,7 +34,7 @@ config(['multitenancy.tenant_artisan_search_fields' => 'domain']); $this->artisan('tenants:artisan', [ - 'artisanCommand' => 'migrate', + 'artisanCommand' => 'migrate --database=tenant', '--tenant' => $this->anotherTenant->id, ]) ->expectsOutput("No tenant(s) found.") @@ -51,7 +45,7 @@ config(['multitenancy.tenant_artisan_search_fields' => 'domain']); $this->artisan('tenants:artisan', [ - 'artisanCommand' => 'migrate', + 'artisanCommand' => 'migrate --database=tenant', '--tenant' => $this->anotherTenant->domain, ])->assertExitCode(0); diff --git a/tests/Feature/TenantAwareJobs/QueuedClosuresTest.php b/tests/Feature/TenantAwareJobs/QueuedClosuresTest.php new file mode 100644 index 0000000..c027912 --- /dev/null +++ b/tests/Feature/TenantAwareJobs/QueuedClosuresTest.php @@ -0,0 +1,70 @@ +set('multitenancy.queues_are_tenant_aware_by_default', false); + + $this->tenant = Tenant::factory()->create(); +}); + +it('succeeds with closure job when queues are tenant aware by default', function () { + $valuestore = Valuestore::make(tempFile('tenantAware.json'))->flush(); + + config()->set('multitenancy.queues_are_tenant_aware_by_default', true); + + $this->tenant->makeCurrent(); + + dispatch(function () use ($valuestore) { + $tenant = Tenant::current(); + + $valuestore->put('tenantId', $tenant?->getKey()); + $valuestore->put('tenantName', $tenant?->name); + }); + + $this->artisan('queue:work --once')->assertExitCode(0); + + expect($valuestore->get('tenantId'))->toBe($this->tenant->getKey()) + ->and($valuestore->get('tenantName'))->toBe($this->tenant->name); +}); + +it('fails with closure job when queues are not tenant aware by default', function () { + $valuestore = Valuestore::make(tempFile('tenantAware.json'))->flush(); + + $this->tenant->makeCurrent(); + + dispatch(function () use ($valuestore) { + $tenant = Tenant::current(); + + $valuestore->put('tenantId', $tenant?->getKey()); + $valuestore->put('tenantName', $tenant?->name); + }); + + $this->artisan('queue:work --once')->assertExitCode(0); + + expect($valuestore->get('tenantId'))->toBeNull() + ->and($valuestore->get('tenantName'))->toBeNull(); +}); + +it('succeeds with closure job when a tenant is specified', function () { + $valuestore = Valuestore::make(tempFile('tenantAware.json'))->flush(); + + $currentTenant = $this->tenant; + + dispatch(function () use ($valuestore, $currentTenant) { + $currentTenant->makeCurrent(); + + $tenant = Tenant::current(); + + $valuestore->put('tenantId', $tenant?->getKey()); + $valuestore->put('tenantName', $tenant?->name); + + $currentTenant->forget(); + }); + + $this->artisan('queue:work --once')->assertExitCode(0); + + expect($valuestore->get('tenantId'))->toBe($this->tenant->getKey()) + ->and($valuestore->get('tenantName'))->toBe($this->tenant->name); +});