Skip to content

Commit

Permalink
fix: Restrict eloquent scope and observer functionality based on mult…
Browse files Browse the repository at this point in the history
…itenanted context (#66)

* chore: Have observers exit early if outside of multitenanted context

* chore: Add support for additional extensions

* chore: Do not apply condition from scopes when outside multitenanted context

* fix: Fix tests by setting the current tenancy
  • Loading branch information
ollieread authored Nov 18, 2024
1 parent 2abfc25 commit 76e7810
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Sprout\Exceptions\TenantMismatch;
use Sprout\Exceptions\TenantMissing;
use Sprout\TenancyOptions;
use function Sprout\sprout;

/**
* Belongs to Many Tenants Observer
Expand Down Expand Up @@ -151,6 +152,10 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsToMa
*/
public function created(Model $model): void
{
if (! sprout()->withinContext()) {
return;
}

/**
* @var \Illuminate\Database\Eloquent\Relations\BelongsToMany<ChildModel, TenantModel> $relation
* @phpstan-ignore-next-line
Expand Down Expand Up @@ -199,6 +204,10 @@ public function created(Model $model): void
*/
public function retrieved(Model $model): void
{
if (! sprout()->withinContext()) {
return;
}

/**
* @var \Illuminate\Database\Eloquent\Relations\BelongsToMany<ChildModel, TenantModel> $relation
* @phpstan-ignore-next-line
Expand Down
9 changes: 9 additions & 0 deletions src/Database/Eloquent/Observers/BelongsToTenantObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Sprout\Exceptions\TenantMismatch;
use Sprout\Exceptions\TenantMissing;
use Sprout\TenancyOptions;
use function Sprout\sprout;

/**
* Belongs to Tenant Observer
Expand Down Expand Up @@ -134,6 +135,10 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsTo $
*/
public function creating(Model $model): bool
{
if (! sprout()->withinContext()) {
return true;
}

/**
* @var \Illuminate\Database\Eloquent\Relations\BelongsTo<ChildModel, TenantModel> $relation
* @phpstan-ignore-next-line
Expand Down Expand Up @@ -175,6 +180,10 @@ public function creating(Model $model): bool
*/
public function retrieved(Model $model): void
{
if (! sprout()->withinContext()) {
return;
}

/**
* @var \Illuminate\Database\Eloquent\Relations\BelongsTo<ChildModel, TenantModel> $relation
* @phpstan-ignore-next-line
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Sprout\Exceptions\TenantMissing;
use function Sprout\sprout;

/**
* Belongs to many Tenants Scope
Expand Down Expand Up @@ -38,6 +39,10 @@ final class BelongsToManyTenantsScope extends TenantChildScope
*/
public function apply(Builder $builder, Model $model): void
{
if (! sprout()->withinContext()) {
return;
}

/** @phpstan-ignore-next-line */
$tenancy = $model->getTenancy();

Expand Down
5 changes: 5 additions & 0 deletions src/Database/Eloquent/Scopes/BelongsToTenantScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Sprout\Exceptions\TenantMissing;
use function Sprout\sprout;

/**
* Belongs to Tenant Scope
Expand Down Expand Up @@ -38,6 +39,10 @@ final class BelongsToTenantScope extends TenantChildScope
*/
public function apply(Builder $builder, Model $model): void
{
if (! sprout()->withinContext()) {
return;
}

/** @phpstan-ignore-next-line */
$tenancy = $model->getTenancy();

Expand Down
9 changes: 9 additions & 0 deletions src/Database/Eloquent/Scopes/TenantChildScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
*/
abstract class TenantChildScope implements Scope
{
/**
* @var array<string, string>
*/
protected array $extensions = [];

/**
* Extend the query builder with the necessary macros
*
Expand All @@ -34,5 +39,9 @@ public function extend(Builder $builder): void
/** @phpstan-ignore-next-line */
return $builder->withoutGlobalScope($this);
});

foreach ($this->extensions as $macro => $method) {
$builder->macro($macro, $this->$method(...));
}
}
}
54 changes: 52 additions & 2 deletions tests/Database/Eloquent/BelongsToManyTenantsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Workbench\App\Models\TenantChildren;
use Workbench\App\Models\TenantChildrenOptional;
use Workbench\App\Models\TenantModel;
use function Sprout\sprout;

#[Group('database'), Group('eloquent')]
class BelongsToManyTenantsTest extends TestCase
Expand Down Expand Up @@ -73,7 +74,11 @@ public function automaticallyAssociatesWithTenantWhenCreating(): void
{
$tenant = TenantModel::factory()->create();

app(TenancyManager::class)->get()->setTenant($tenant);
$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$child = TenantChildren::factory()->create();

Expand All @@ -82,9 +87,21 @@ public function automaticallyAssociatesWithTenantWhenCreating(): void
$this->assertNotNull($child->tenants->first(fn (Model $model) => $model->is($tenant)));
}

#[Test]
public function doesNotAutomaticallyAssociateWithTenantWhenCreatingWhenOutsideMultitenantedContext(): void
{
$child = TenantChildren::factory()->create();

$this->assertTrue($child->exists);
$this->assertFalse($child->relationLoaded('tenants'));
$this->assertTrue($child->tenants->isEmpty());
}

#[Test]
public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreating(): void
{
sprout()->setCurrentTenancy(app(TenancyManager::class)->get());

$this->expectException(TenantMissing::class);
$this->expectExceptionMessage(
'There is no current tenant for tenancy [tenants]'
Expand All @@ -93,6 +110,15 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCr
TenantChildren::factory()->create();
}

#[Test]
public function doesNotThrowAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreatingWhenOutsideMultitenantedContext(): void
{
$model = TenantChildren::factory()->create();

$this->assertNotNull($model);
$this->assertTrue($model->exists);
}

#[Test]
public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithInterfaceWhenCreating(): void
{
Expand Down Expand Up @@ -142,7 +168,11 @@ public function automaticallyPopulateTheTenantRelationWhenHydrating(): void
{
$tenant = TenantModel::factory()->create();

app(TenancyManager::class)->get()->setTenant($tenant);
$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$child = TenantChildren::query()->find(TenantChildren::factory()->create()->getKey());

Expand All @@ -151,13 +181,25 @@ public function automaticallyPopulateTheTenantRelationWhenHydrating(): void
$this->assertNotNull($child->getRelation('tenants')->first(fn (Model $model) => $model->is($tenant)));
}

#[Test]
public function doesNotAutomaticallyPopulateTheTenantRelationWhenHydratingWhenOutsideMultitenantedContext(): void
{

$child = TenantChildren::query()->find(TenantChildren::factory()->create()->getKey());

$this->assertTrue($child->exists);
$this->assertFalse($child->relationLoaded('tenants'));
}

#[Test]
public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenHydrating(): void
{
$tenant = TenantModel::factory()->create();

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$child = TenantChildren::factory()->create();
Expand Down Expand Up @@ -223,7 +265,10 @@ public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDiffere

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$tenancy->addOption(TenancyOptions::throwIfNotRelated());

$child = TenantChildren::factory()->create();
Expand All @@ -248,7 +293,10 @@ public function doesNotThrowAnExceptionForTenantMismatchIfNotSetToWhenHydrating(

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$tenancy->removeOption(TenancyOptions::throwIfNotRelated());

$child = TenantChildren::factory()->create();
Expand All @@ -270,6 +318,8 @@ public function onlyReturnsModelsForTheCurrentTenant(): void

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$original = TenantChildren::factory()->create();
Expand Down
57 changes: 55 additions & 2 deletions tests/Database/Eloquent/BelongsToTenantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Workbench\App\Models\TenantChild;
use Workbench\App\Models\TenantChildOptional;
use Workbench\App\Models\TenantModel;
use function Sprout\sprout;

#[Group('database'), Group('eloquent')]
class BelongsToTenantTest extends TestCase
Expand Down Expand Up @@ -71,7 +72,11 @@ public function automaticallyAssociatesWithTenantWhenCreating(): void
{
$tenant = TenantModel::factory()->create();

app(TenancyManager::class)->get()->setTenant($tenant);
$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$child = TenantChild::factory()->create();

Expand All @@ -80,9 +85,21 @@ public function automaticallyAssociatesWithTenantWhenCreating(): void
$this->assertTrue($child->tenant->is($tenant));
}

#[Test]
public function doesNotAutomaticallyAssociateWithTenantWhenCreatingWhenOutsideMultitenantedContext(): void
{
$child = TenantChild::factory()->create();

$this->assertTrue($child->exists);
$this->assertFalse($child->relationLoaded('tenant'));
$this->assertNull($child->tenant);
}

#[Test]
public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreating(): void
{
sprout()->setCurrentTenancy(app(TenancyManager::class)->get());

$this->expectException(TenantMissing::class);
$this->expectExceptionMessage(
'There is no current tenant for tenancy [tenants]'
Expand All @@ -91,6 +108,16 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCr
TenantChild::factory()->create();
}

#[Test]
public function doesNotThrowAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreatingWhenOutsideMultitenantedContext(): void
{
$child = TenantChild::factory()->create();

$this->assertTrue($child->exists);
$this->assertFalse($child->relationLoaded('tenant'));
$this->assertNull($child->tenant);
}

#[Test]
public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithInterfaceWhenCreating(): void
{
Expand Down Expand Up @@ -138,7 +165,10 @@ public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDiffere

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$tenancy->addOption(TenancyOptions::throwIfNotRelated());

$this->expectException(TenantMismatch::class);
Expand Down Expand Up @@ -174,7 +204,11 @@ public function automaticallyPopulateTheTenantRelationWhenHydrating(): void
{
$tenant = TenantModel::factory()->create();

app(TenancyManager::class)->get()->setTenant($tenant);
$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$child = TenantChild::query()->find(TenantChild::factory()->create()->getKey());

Expand All @@ -184,6 +218,15 @@ public function automaticallyPopulateTheTenantRelationWhenHydrating(): void
$this->assertTrue($child->getRelation('tenant')->is($tenant));
}

#[Test]
public function doesNotAutomaticallyPopulateTheTenantRelationWhenHydratingWhenOutsideMultitenantedCContext(): void
{
$child = TenantChild::query()->find(TenantChild::factory()->create()->getKey());

$this->assertTrue($child->exists);
$this->assertFalse($child->relationLoaded('tenant'));
}

#[Test]
public function doNotHydrateWhenHydrateTenantRelationIsMissing(): void
{
Expand All @@ -208,6 +251,8 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenHy

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$child = TenantChild::factory()->create();
Expand Down Expand Up @@ -273,7 +318,10 @@ public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDiffere

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$tenancy->addOption(TenancyOptions::throwIfNotRelated());

$child = TenantChild::factory()->create();
Expand All @@ -298,7 +346,10 @@ public function doesNotThrowAnExceptionForTenantMismatchIfNotSetToWhenHydrating(

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$tenancy->removeOption(TenancyOptions::throwIfNotRelated());

$child = TenantChild::factory()->create();
Expand All @@ -320,6 +371,8 @@ public function onlyReturnsModelsForTheCurrentTenant(): void

$tenancy = app(TenancyManager::class)->get();

sprout()->setCurrentTenancy($tenancy);

$tenancy->setTenant($tenant);

$original = TenantChild::factory()->create();
Expand Down

0 comments on commit 76e7810

Please sign in to comment.