Skip to content

Commit

Permalink
Implements Context
Browse files Browse the repository at this point in the history
  • Loading branch information
masterix21 committed Jul 22, 2024
1 parent b0bc9be commit dc27786
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 66 deletions.
19 changes: 5 additions & 14 deletions config/multitenancy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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' => [

This comment has been minimized.

Copy link
@riasvdv

riasvdv Aug 19, 2024

Member

@masterix21 Why did this get removed? We're seeing issues with mailables and notifications not correctly being recognized and having the following exception:

The current tenant could not be determined in a job named `Illuminate\Queue\CallQueuedHandler@call`. No `tenantId` was set in the payload.

This comment has been minimized.

Copy link
@masterix21

masterix21 Aug 20, 2024

Author Collaborator

My mistakes, sorry: too much trust to tests. Thanks for your fix.

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.
*/
Expand Down
8 changes: 6 additions & 2 deletions src/Actions/ForgetCurrentTenantAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -10,6 +12,8 @@

class ForgetCurrentTenantAction
{
use UsesMultitenancyConfig;

public function __construct(
protected TasksCollection $tasksCollection
) {
Expand All @@ -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());
}
}
45 changes: 9 additions & 36 deletions src/Actions/MakeQueueTenantAwareAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/Concerns/BindAsCurrentTenant.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
5 changes: 5 additions & 0 deletions src/Concerns/UsesMultitenancyConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
11 changes: 10 additions & 1 deletion src/MultitenancyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
14 changes: 10 additions & 4 deletions tests/Feature/Models/TenantTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Event;
use Spatie\Multitenancy\Events\ForgettingCurrentTenantEvent;
use Spatie\Multitenancy\Events\ForgotCurrentTenantEvent;
Expand Down Expand Up @@ -34,14 +35,19 @@

it('can forget the current tenant', function () {
$containerKey = config('multitenancy.current_tenant_container_key');
$contextKey = config('multitenancy.current_tenant_context_key');

$this->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 () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -69,7 +71,7 @@

$this->artisan('queue:work --once')->assertExitCode(0);

$currentTenantIdInJob = $this->valuestore->get('tenantIdInPayload');
$currentTenantIdInJob = $this->valuestore->get('tenantId');
expect($currentTenantIdInJob)->toBeNull();
});

Expand Down Expand Up @@ -97,6 +99,6 @@

$this->artisan('queue:work --once')->assertExitCode(0);

$currentTenantIdInJob = $this->valuestore->get('tenantIdInPayload');
$currentTenantIdInJob = $this->valuestore->get('tenantId');
expect($currentTenantIdInJob)->toBeNull();
});
8 changes: 4 additions & 4 deletions tests/Feature/TenantAwareJobs/TenantAwareJobsByConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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();
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class MailableTenantAware extends Mailable implements ShouldQueue, TenantAware
{
public function build()
public function build(): Mailable
{
return $this->view('mailable');
}
Expand Down
5 changes: 4 additions & 1 deletion tests/Feature/TenantAwareJobs/TestClasses/TestJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
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;

class TestJob implements ShouldQueue
{
use InteractsWithQueue;
use Dispatchable;
use UsesMultitenancyConfig;

public Valuestore $valuestore;

Expand All @@ -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);
}
Expand Down

0 comments on commit dc27786

Please sign in to comment.