-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(database.eloquent): Add tenant child model functionality (#32)
* chore: Add basic hydration options to default tenancy * chore: Add attribute for marking method as a tenant relation * refactor: Rename IsTenantModel to IsTenant * chore: Add contract/interface for tenant relation handlers * chore: Add a base abstract tenant relation handler for common functionality * fix: Set the tenancy current resolver to null as default * feat(database.eloquent): Add belongs to tenant relation handler * feat(database.eloquent): Add belongs to many tenants relation handler * refactor: Abstract out tenancy options for hydration to flag class * chore: Current state of tenant child trait and has one/main handlers * test(database.eloquent): Testing for initial working tenant relation handlers * refactor: Removed Eloquent relation handler contract, implementations and tests * refactor: Tidied up tenancy options and updated typings for them * refactor: Create simple observer and base scope for belongsTo and belongsToMany relations * test: Add some tests for the initial trait functionality * refactor: Remove unused abstract tenant relation handler * feat: Complete single-database support * test: Fix observer tests to not throw exception when models not related to current tenant * refactor: Add a manual override for tenant restrictions to child models * refactor: Remove untestable and unnecessary code from belongs to many tenants observer * feat: Add method for temporarily disabling tenant restrictions Completes: #29 #30 #31
- Loading branch information
Showing
35 changed files
with
2,130 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sprout\Attributes; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_METHOD)] | ||
final class TenantRelation | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sprout\Database\Eloquent\Concerns; | ||
|
||
use Sprout\Database\Eloquent\Observers\BelongsToManyTenantsObserver; | ||
use Sprout\Database\Eloquent\Scopes\BelongsToManyTenantsScope; | ||
|
||
trait BelongsToManyTenants | ||
{ | ||
use IsTenantChild; | ||
|
||
public static function bootBelongsToManyTenants(): void | ||
{ | ||
// Automatically scope queries | ||
static::addGlobalScope(new BelongsToManyTenantsScope()); | ||
|
||
// Add the observer | ||
static::observe(new BelongsToManyTenantsObserver()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sprout\Database\Eloquent\Concerns; | ||
|
||
use Sprout\Database\Eloquent\Observers\BelongsToTenantObserver; | ||
use Sprout\Database\Eloquent\Scopes\BelongsToTenantScope; | ||
|
||
trait BelongsToTenant | ||
{ | ||
use IsTenantChild; | ||
|
||
public static function bootBelongsToTenant(): void | ||
{ | ||
// Automatically scope queries | ||
static::addGlobalScope(new BelongsToTenantScope()); | ||
|
||
// Add the observer | ||
static::observe(new BelongsToTenantObserver()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Sprout\Database\Eloquent\Concerns; | ||
|
||
use Illuminate\Database\Eloquent\Relations\Relation; | ||
use ReflectionClass; | ||
use ReflectionException; | ||
use ReflectionMethod; | ||
use RuntimeException; | ||
use Sprout\Attributes\TenantRelation; | ||
use Sprout\Contracts\Tenancy; | ||
use Sprout\Database\Eloquent\Contracts\OptionalTenant; | ||
use Sprout\Managers\TenancyManager; | ||
|
||
/** | ||
* @phpstan-require-extends \Illuminate\Database\Eloquent\Model | ||
* | ||
* @mixin \Illuminate\Database\Eloquent\Model | ||
*/ | ||
trait IsTenantChild | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
protected static string $tenantRelationName; | ||
|
||
protected static bool $ignoreTenantRestrictions = false; | ||
|
||
public static function shouldIgnoreTenantRestrictions(): bool | ||
{ | ||
return self::$ignoreTenantRestrictions; | ||
} | ||
|
||
public static function ignoreTenantRestrictions(): void | ||
{ | ||
self::$ignoreTenantRestrictions = true; | ||
} | ||
|
||
public static function resetTenantRestrictions(): void | ||
{ | ||
self::$ignoreTenantRestrictions = false; | ||
} | ||
|
||
/** | ||
* @template RetType of mixed | ||
* | ||
* @param callable(): RetType $callback | ||
* | ||
* @return mixed | ||
* | ||
* @phpstan-return RetType | ||
*/ | ||
public static function withoutTenantRestrictions(callable $callback): mixed | ||
{ | ||
self::ignoreTenantRestrictions(); | ||
|
||
$return = $callback(); | ||
|
||
self::resetTenantRestrictions(); | ||
|
||
return $return; | ||
} | ||
|
||
public static function isTenantOptional(): bool | ||
{ | ||
return is_subclass_of(static::class, OptionalTenant::class) | ||
|| ( | ||
method_exists(static::class, 'shouldIgnoreTenantRestrictions') | ||
&& static::shouldIgnoreTenantRestrictions() // @phpstan-ignore-line | ||
); | ||
} | ||
|
||
private function findTenantRelationName(): string | ||
{ | ||
try { | ||
$methods = collect((new ReflectionClass(static::class))->getMethods(ReflectionMethod::IS_PUBLIC)) | ||
->filter(function (ReflectionMethod $method) { | ||
return ! $method->isStatic() && $method->getAttributes(TenantRelation::class); | ||
}) | ||
->map(fn (ReflectionMethod $method) => $method->getName()); | ||
|
||
if ($methods->isEmpty()) { | ||
throw new RuntimeException('No tenant relation found in model [' . static::class . ']'); | ||
} | ||
|
||
if ($methods->count() > 1) { | ||
throw new RuntimeException( | ||
'Models can only have one tenant relation, [' . static::class . '] has ' . $methods->count() | ||
); | ||
} | ||
|
||
return $methods->first(); | ||
} catch (ReflectionException $exception) { | ||
throw new RuntimeException('Unable to find tenant relation for model [' . static::class . ']', previous: $exception); // @codeCoverageIgnore | ||
} | ||
} | ||
|
||
public function getTenantRelationName(): ?string | ||
{ | ||
if (! isset($this->tenantRelationNames[static::class])) { | ||
self::$tenantRelationName = $this->findTenantRelationName(); | ||
} | ||
|
||
return self::$tenantRelationName ?? null; | ||
} | ||
|
||
public function getTenancyName(): ?string | ||
{ | ||
return null; | ||
} | ||
|
||
public function getTenancy(): Tenancy | ||
{ | ||
return app(TenancyManager::class)->get($this->getTenancyName()); | ||
} | ||
|
||
public function getTenantRelation(): Relation | ||
{ | ||
return $this->{$this->getTenantRelationName()}(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
namespace Sprout\Database\Eloquent\Contracts; | ||
|
||
/** | ||
* Optional Tenant Contract | ||
* | ||
* This contract marks an Eloquent model that relates to a tenant as having | ||
* that tenant be optional, so it can be interacted with if there's no | ||
* tenant. | ||
*/ | ||
interface OptionalTenant | ||
{ | ||
|
||
} |
Oops, something went wrong.