diff --git a/config/multitenancy.php b/config/multitenancy.php index 94e06ed2..016fe263 100644 --- a/config/multitenancy.php +++ b/config/multitenancy.php @@ -66,6 +66,11 @@ */ 'landlord_database_connection_name' => null, + /* + * This key will be used to associate the current tenant in the context + */ + 'current_tenant_context_key' => 'tenantId', + /* * This key will be used to bind the current tenant in the container. */ @@ -88,20 +93,6 @@ 'migrate_tenant' => MigrateTenantAction::class, ], - /* - * You can customize the way in which the package resolves the queueable to a job. - * - * For example, using the package laravel-actions (by Loris Leiva), you can - * resolve JobDecorator to getAction() like so: JobDecorator::class => 'getAction' - */ - '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. */ diff --git a/src/Actions/ForgetCurrentTenantAction.php b/src/Actions/ForgetCurrentTenantAction.php index a3ff21c9..b1cb12f2 100644 --- a/src/Actions/ForgetCurrentTenantAction.php +++ b/src/Actions/ForgetCurrentTenantAction.php @@ -2,6 +2,8 @@ namespace Spatie\Multitenancy\Actions; +use Illuminate\Support\Facades\Context; +use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig; use Spatie\Multitenancy\Contracts\IsTenant; use Spatie\Multitenancy\Events\ForgettingCurrentTenantEvent; use Spatie\Multitenancy\Events\ForgotCurrentTenantEvent; @@ -10,6 +12,8 @@ class ForgetCurrentTenantAction { + use UsesMultitenancyConfig; + public function __construct( protected TasksCollection $tasksCollection ) { @@ -35,8 +39,8 @@ protected function performTaskToForgetCurrentTenant(): static protected function clearBoundCurrentTenant(): void { - $containerKey = config('multitenancy.current_tenant_container_key'); + app()->forgetInstance($this->currentTenantContainerKey()); - app()->forgetInstance($containerKey); + Context::forget($this->currentTenantContextKey()); } } diff --git a/src/Actions/MakeQueueTenantAwareAction.php b/src/Actions/MakeQueueTenantAwareAction.php index bc4ebce3..1d222296 100644 --- a/src/Actions/MakeQueueTenantAwareAction.php +++ b/src/Actions/MakeQueueTenantAwareAction.php @@ -4,8 +4,9 @@ use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\Events\JobRetryRequested; -use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Context; use Spatie\Multitenancy\Concerns\BindAsCurrentTenant; +use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig; use Spatie\Multitenancy\Contracts\IsTenant; use Spatie\Multitenancy\Exceptions\CurrentTenantCouldNotBeDeterminedInTenantAwareJob; use Spatie\Multitenancy\Jobs\NotTenantAware; @@ -14,30 +15,15 @@ class MakeQueueTenantAwareAction { use BindAsCurrentTenant; + use UsesMultitenancyConfig; public function execute(): void { $this - ->listenForJobsBeingQueued() ->listenForJobsBeingProcessed() ->listenForJobsRetryRequested(); } - protected function listenForJobsBeingQueued(): static - { - app('queue')->createPayloadUsing(function ($connectionName, $queue, $payload) { - $queueable = $payload['data']['command']; - - if (! $this->isTenantAware($queueable)) { - return []; - } - - return ['tenantId' => app(IsTenant::class)::current()?->getKey()]; - }); - - return $this; - } - protected function listenForJobsBeingProcessed(): static { app('events')->listen(JobProcessing::class, function (JobProcessing $event) { @@ -56,9 +42,11 @@ protected function listenForJobsRetryRequested(): static return $this; } - protected function isTenantAware(object $queueable): bool + protected function isTenantAware(JobProcessing|JobRetryRequested $event): bool { - $reflection = new \ReflectionClass($this->getJobFromQueueable($queueable)); + $jobName = $this->getEventPayload($event)['data']['commandName']; + + $reflection = new \ReflectionClass($jobName); if ($reflection->implementsInterface(TenantAware::class)) { return true; @@ -90,7 +78,7 @@ protected function getEventPayload($event): ?array protected function findTenant(JobProcessing|JobRetryRequested $event): IsTenant { - $tenantId = $this->getEventPayload($event)['tenantId'] ?? null; + $tenantId = Context::get($this->currentTenantContextKey()); if (! $tenantId) { $event->job->delete(); @@ -107,24 +95,9 @@ protected function findTenant(JobProcessing|JobRetryRequested $event): IsTenant return $tenant; } - protected function getJobFromQueueable(object $queueable) - { - $job = Arr::get(config('multitenancy.queueable_to_job'), $queueable::class); - - if (! $job) { - return $queueable; - } - - if (method_exists($queueable, $job)) { - return $queueable->{$job}(); - } - - return $queueable->$job; - } - protected function bindOrForgetCurrentTenant(JobProcessing|JobRetryRequested $event): void { - if (array_key_exists('tenantId', $this->getEventPayload($event))) { + if ($this->isTenantAware($event)) { $this->bindAsCurrentTenant($this->findTenant($event)->makeCurrent()); return; diff --git a/src/Concerns/BindAsCurrentTenant.php b/src/Concerns/BindAsCurrentTenant.php index 9ef18d4a..4d919d9c 100644 --- a/src/Concerns/BindAsCurrentTenant.php +++ b/src/Concerns/BindAsCurrentTenant.php @@ -2,18 +2,24 @@ namespace Spatie\Multitenancy\Concerns; +use Illuminate\Support\Facades\Context; use Spatie\Multitenancy\Contracts\IsTenant; trait BindAsCurrentTenant { protected function bindAsCurrentTenant(IsTenant $tenant): static { + $contextKey = config('multitenancy.current_tenant_context_key'); $containerKey = config('multitenancy.current_tenant_container_key'); + Context::forget($contextKey); + app()->forgetInstance($containerKey); app()->instance($containerKey, $tenant); + Context::add($contextKey, $tenant->getKey()); + return $this; } } diff --git a/src/Concerns/UsesMultitenancyConfig.php b/src/Concerns/UsesMultitenancyConfig.php index 1d3c4498..e197121e 100644 --- a/src/Concerns/UsesMultitenancyConfig.php +++ b/src/Concerns/UsesMultitenancyConfig.php @@ -17,6 +17,11 @@ public function landlordDatabaseConnectionName(): ?string return config('multitenancy.landlord_database_connection_name') ?? config('database.default'); } + public function currentTenantContextKey(): string + { + return config('multitenancy.current_tenant_context_key'); + } + public function currentTenantContainerKey(): string { return config('multitenancy.current_tenant_container_key'); diff --git a/src/MultitenancyServiceProvider.php b/src/MultitenancyServiceProvider.php index 0606d043..5f94e80c 100644 --- a/src/MultitenancyServiceProvider.php +++ b/src/MultitenancyServiceProvider.php @@ -2,6 +2,8 @@ namespace Spatie\Multitenancy; +use Illuminate\Log\Context\Repository; +use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Event; use Laravel\Octane\Events\RequestReceived as OctaneRequestReceived; use Laravel\Octane\Events\RequestTerminated as OctaneRequestTerminated; @@ -30,13 +32,20 @@ public function packageBooted(): void $this->app->bind(Multitenancy::class, fn ($app) => new Multitenancy($app)); + $this->detectsLaravelOctane(); + } + + protected function detectsLaravelOctane(): static + { if (! isset($_SERVER['LARAVEL_OCTANE'])) { app(Multitenancy::class)->start(); - return; + return $this; } Event::listen(fn (OctaneRequestReceived $requestReceived) => app(Multitenancy::class)->start()); Event::listen(fn (OctaneRequestTerminated $requestTerminated) => app(Multitenancy::class)->end()); + + return $this; } } diff --git a/tests/Feature/Models/TenantTest.php b/tests/Feature/Models/TenantTest.php index 97a9a2ab..6d550a82 100644 --- a/tests/Feature/Models/TenantTest.php +++ b/tests/Feature/Models/TenantTest.php @@ -1,5 +1,6 @@ tenant->makeCurrent(); - expect(Tenant::current()->id)->toEqual($this->tenant->id); - expect(app()->has($containerKey))->toBeTrue(); + + expect(Tenant::current()->id)->toEqual($this->tenant->id) + ->and(app()->has($containerKey))->toBeTrue() + ->and(Context::get($contextKey))->toEqual($this->tenant->id); Tenant::forgetCurrent(); - expect(Tenant::current())->toBeNull(); - expect(app())->has($containerKey)->toBeFalse(); + + expect(Tenant::current())->toBeNull() + ->and(app())->has($containerKey)->toBeFalse() + ->and(Context::get($contextKey))->toBeNull(); }); it('can check if a current tenant has been set', function () { diff --git a/tests/Feature/TenantAwareJobs/QueueIsTenantAwareByDefaultTest.php b/tests/Feature/TenantAwareJobs/QueueIsTenantAwareByDefaultTest.php index 309dbe5d..7b92ee06 100644 --- a/tests/Feature/TenantAwareJobs/QueueIsTenantAwareByDefaultTest.php +++ b/tests/Feature/TenantAwareJobs/QueueIsTenantAwareByDefaultTest.php @@ -32,7 +32,9 @@ $this->artisan('queue:work --once')->assertExitCode(0); $currentTenantIdInJob = $this->valuestore->get('tenantId'); - expect($this->tenant->id)->toEqual($currentTenantIdInJob); + + expect($this->valuestore->get('tenantIdInContext'))->toBe($this->tenant->getKey()) + ->and($this->tenant->id)->toEqual($currentTenantIdInJob); }); it('will inject the right tenant even when the current tenant switches', function () { @@ -69,7 +71,7 @@ $this->artisan('queue:work --once')->assertExitCode(0); - $currentTenantIdInJob = $this->valuestore->get('tenantIdInPayload'); + $currentTenantIdInJob = $this->valuestore->get('tenantId'); expect($currentTenantIdInJob)->toBeNull(); }); @@ -97,6 +99,6 @@ $this->artisan('queue:work --once')->assertExitCode(0); - $currentTenantIdInJob = $this->valuestore->get('tenantIdInPayload'); + $currentTenantIdInJob = $this->valuestore->get('tenantId'); expect($currentTenantIdInJob)->toBeNull(); }); diff --git a/tests/Feature/TenantAwareJobs/TenantAwareJobsByConfigTest.php b/tests/Feature/TenantAwareJobs/TenantAwareJobsByConfigTest.php index 50c81ce3..2d5b110b 100644 --- a/tests/Feature/TenantAwareJobs/TenantAwareJobsByConfigTest.php +++ b/tests/Feature/TenantAwareJobs/TenantAwareJobsByConfigTest.php @@ -21,8 +21,8 @@ app(Dispatcher::class)->dispatch(new TestJob($this->valuestore)); - expect($this->valuestore->has('tenantIdInPayload'))->toBeTrue() - ->and($this->valuestore->get('tenantIdInPayload'))->not->toBeNull(); + expect($this->valuestore->has('tenantIdInContext'))->toBeTrue() + ->and($this->valuestore->get('tenantIdInContext'))->not->toBeNull(); }); it('fails with jobs in not tenant aware jobs list', function () { @@ -33,6 +33,6 @@ app(Dispatcher::class)->dispatch(new TestJob($this->valuestore)); expect($this->valuestore->get('tenantId'))->toBeNull() - ->and($this->valuestore->has('tenantIdInPayload'))->toBeTrue() - ->and($this->valuestore->get('tenantIdInPayload'))->toBeNull(); + ->and($this->valuestore->get('tenantName'))->toBeNull() + ->and($this->valuestore->has('tenantIdInContext'))->toBeTrue(); }); diff --git a/tests/Feature/TenantAwareJobs/TestClasses/MailableTenantAware.php b/tests/Feature/TenantAwareJobs/TestClasses/MailableTenantAware.php index e82a4383..d009d7b9 100644 --- a/tests/Feature/TenantAwareJobs/TestClasses/MailableTenantAware.php +++ b/tests/Feature/TenantAwareJobs/TestClasses/MailableTenantAware.php @@ -8,7 +8,7 @@ class MailableTenantAware extends Mailable implements ShouldQueue, TenantAware { - public function build() + public function build(): Mailable { return $this->view('mailable'); } diff --git a/tests/Feature/TenantAwareJobs/TestClasses/TestJob.php b/tests/Feature/TenantAwareJobs/TestClasses/TestJob.php index 9630bc50..8fb9431d 100644 --- a/tests/Feature/TenantAwareJobs/TestClasses/TestJob.php +++ b/tests/Feature/TenantAwareJobs/TestClasses/TestJob.php @@ -5,6 +5,8 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Support\Facades\Context; +use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig; use Spatie\Multitenancy\Models\Tenant; use Spatie\Valuestore\Valuestore; @@ -12,6 +14,7 @@ class TestJob implements ShouldQueue { use InteractsWithQueue; use Dispatchable; + use UsesMultitenancyConfig; public Valuestore $valuestore; @@ -22,7 +25,7 @@ public function __construct(Valuestore $valuestore) public function handle() { - $this->valuestore->put('tenantIdInPayload', $this->job?->payload()['tenantId'] ?? null); + $this->valuestore->put('tenantIdInContext', Context::get($this->currentTenantContextKey())); $this->valuestore->put('tenantId', Tenant::current()?->id); $this->valuestore->put('tenantName', Tenant::current()?->name); }