diff --git a/resources/config/multitenancy.php b/resources/config/multitenancy.php index cf1825e..64487e9 100644 --- a/resources/config/multitenancy.php +++ b/resources/config/multitenancy.php @@ -19,7 +19,6 @@ 'options' => [ TenancyOptions::hydrateTenantRelation(), TenancyOptions::throwIfNotRelated(), - TenancyOptions::makeJobsTenantAware(), ], ], diff --git a/resources/config/sprout.php b/resources/config/sprout.php index 732ccc3..85da994 100644 --- a/resources/config/sprout.php +++ b/resources/config/sprout.php @@ -29,14 +29,14 @@ */ 'bootstrappers' => [ + // Set the current tenant within the Laravel context + \Sprout\Listeners\SetCurrentTenantContext::class, // Calls the setup method on the current identity resolver \Sprout\Listeners\PerformIdentityResolverSetup::class, // Performs any clean-up from the previous tenancy \Sprout\Listeners\CleanupServiceOverrides::class, // Sets up service overrides for the current tenancy \Sprout\Listeners\SetupServiceOverrides::class, - // Set the current tenant within the Laravel context - \Sprout\Listeners\SetCurrentTenantContext::class, ], /* @@ -53,9 +53,15 @@ // This will override the storage by introducing a 'sprout' driver // that wraps any other storage drive in a tenant resource subdirectory. \Sprout\Overrides\StorageOverride::class, + // This will hydrate tenants when running jobs, based on the current + // context. + \Sprout\Overrides\JobOverride::class, // This will override the cache by introducing a 'sprout' driver // that adds a prefix to cache stores for the current tenant. \Sprout\Overrides\CacheOverride::class, + // This is a simple override that removes all currently resolved + // guards to prevent user auth leaking. + \Sprout\Overrides\AuthOverride::class, // This will override the cookie settings so that all created cookies // are specific to the tenant. \Sprout\Overrides\CookieOverride::class, diff --git a/src/Attributes/CurrentTenant.php b/src/Attributes/CurrentTenant.php index b82d9d6..acac82f 100644 --- a/src/Attributes/CurrentTenant.php +++ b/src/Attributes/CurrentTenant.php @@ -9,16 +9,45 @@ use Sprout\Contracts\Tenant; use Sprout\Managers\TenancyManager; +/** + * Current Tenant Attribute + * + * This is a contextual attribute that allows for the auto-injection of the + * current tenant for the default, or a given tenancy. + * + * @see https://laravel.com/docs/11.x/container#contextual-attributes + * + * @package Core + */ #[Attribute(Attribute::TARGET_PARAMETER)] final readonly class CurrentTenant implements ContextualAttribute { + /** + * The tenancy to use + * + * @var string|null + */ public ?string $tenancy; + /** + * Create a new instance + * + * @param string|null $tenancy + */ public function __construct(?string $tenancy = null) { $this->tenancy = $tenancy; } + /** + * Resolve the tenant using this attribute + * + * @param \Sprout\Attributes\CurrentTenant $tenant + * @param \Illuminate\Container\Container $container + * + * @return \Sprout\Contracts\Tenant|null + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function resolve(CurrentTenant $tenant, Container $container): ?Tenant { /** diff --git a/src/Attributes/TenantRelation.php b/src/Attributes/TenantRelation.php index e0a2796..12d8c60 100644 --- a/src/Attributes/TenantRelation.php +++ b/src/Attributes/TenantRelation.php @@ -5,6 +5,17 @@ use Attribute; +/** + * Tenant Relation Attribute + * + * This attribute marks a relation method within an Eloquent model as + * relating to the tenant. + * + * This is primarily used in the tenant child/descendant functionality that + * comes with Sprout. + * + * @package Database\Eloquent + */ #[Attribute(Attribute::TARGET_METHOD)] final class TenantRelation { diff --git a/src/Concerns/FindsIdentityInRouteParameter.php b/src/Concerns/FindsIdentityInRouteParameter.php index e445c6c..b7ffaee 100644 --- a/src/Concerns/FindsIdentityInRouteParameter.php +++ b/src/Concerns/FindsIdentityInRouteParameter.php @@ -11,17 +11,43 @@ use Sprout\Contracts\Tenant; /** + * Find Identity in Route Parameter + * + * This trait provides both helper methods and default implementations for + * methods required by the {@see \Sprout\Contracts\IdentityResolverUsesParameters} + * interface. + * * @package Resolvers * * @phpstan-require-implements \Sprout\Contracts\IdentityResolverUsesParameters */ trait FindsIdentityInRouteParameter { - + /** + * The route parameter pattern + * + * @var string|null + */ private ?string $pattern = null; + /** + * The route parameter name + * + * @var string + */ private string $parameter = '{tenancy}_{resolver}'; + /** + * Initialise the pattern and parameter name values + * + * This method sets the value for {@see self::$pattern}, and optionally + * {@see self::$parameter} if the value isn't null. + * + * @param string|null $pattern + * @param string|null $parameter + * + * @return void + */ protected function initialiseRouteParameter(?string $pattern = null, ?string $parameter = null): void { $this->setPattern($pattern); @@ -31,11 +57,25 @@ protected function initialiseRouteParameter(?string $pattern = null, ?string $pa } } + /** + * Set the route parameter pattern + * + * @param string|null $pattern + * + * @return void + */ public function setPattern(?string $pattern): void { $this->pattern = $pattern; } + /** + * Set the route parameter name + * + * @param string $parameter + * + * @return void + */ public function setParameter(string $parameter): void { $this->parameter = $parameter; @@ -44,6 +84,10 @@ public function setParameter(string $parameter): void /** * Get the name of the route parameter * + * This method uses the route parameter name stored in {@see self::$parameter}, + * replacing occurrences of {tenancy} with the name of the + * tenancy, and {resolver} with the name of the resolver. + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Contracts\Tenancy $tenancy @@ -60,7 +104,11 @@ public function getRouteParameterName(Tenancy $tenancy): string } /** - * Get the route parameter with braces + * Get the route parameter placeholder + * + * This method returns the route parameter provided by + * {@see self::getRouteParameterName()}, but wrapped with curly braces for + * use in route definitions. * * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy * @@ -71,12 +119,19 @@ public function getRouteParameter(Tenancy $tenancy): string return '{' . $this->getRouteParameterName($tenancy) . '}'; } + /** + * Get the route parameter pattern + * + * @return string|null + */ public function getPattern(): ?string { return $this->pattern; } /** + * Check if there is a route parameter pattern set + * * @return bool * * @phpstan-assert-if-true string $this->getPattern() @@ -87,19 +142,29 @@ public function hasPattern(): bool return $this->pattern !== null; } + /** + * Get the route parameter pattern + * + * @return string + */ public function getParameter(): string { return $this->parameter; } /** + * Get the route parameter pattern mapping + * + * This method returns an array mappings the route parameter name to its + * pattern, for use in route definitions. + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Contracts\Tenancy $tenancy * * @return array */ - protected function getParameterPattern(Tenancy $tenancy): array + protected function getParameterPatternMapping(Tenancy $tenancy): array { if (! $this->hasPattern()) { return []; @@ -111,6 +176,12 @@ protected function getParameterPattern(Tenancy $tenancy): array } /** + * Apply the route parameter pattern mapping to a route + * + * This method applies the route parameter pattern mapping provided by + * {@see self::getParameterPatternMapping()} to a supplied route registrar, + * for a supplied tenancy. + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Illuminate\Routing\RouteRegistrar $registrar @@ -118,13 +189,13 @@ protected function getParameterPattern(Tenancy $tenancy): array * * @return \Illuminate\Routing\RouteRegistrar */ - protected function applyParameterPattern(RouteRegistrar $registrar, Tenancy $tenancy): RouteRegistrar + protected function applyParameterPatternMapping(RouteRegistrar $registrar, Tenancy $tenancy): RouteRegistrar { if ($this->hasPattern()) { return $registrar; } - return $registrar->where($this->getParameterPattern($tenancy)); + return $registrar->where($this->getParameterPatternMapping($tenancy)); } /** diff --git a/src/Concerns/HasCustomCreators.php b/src/Concerns/HasCustomCreators.php deleted file mode 100644 index 6a8723c..0000000 --- a/src/Concerns/HasCustomCreators.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * @phpstan-var array, string): FactoryClass> - */ - protected static array $customCreators = []; - - /** - * @param string $name - * @param \Closure $callback - * - * @phpstan-param \Closure(Application, array, string): FactoryClass $callback - * - * @return void - */ - public static function register(string $name, \Closure $callback): void - { - static::$customCreators[$name] = $callback; - } -} diff --git a/src/Concerns/OverridesCookieSettings.php b/src/Concerns/OverridesCookieSettings.php new file mode 100644 index 0000000..b150cf4 --- /dev/null +++ b/src/Concerns/OverridesCookieSettings.php @@ -0,0 +1,86 @@ + $tenancy - * @param \Illuminate\Http\Response $response - * - * @return void - */ - public function terminate(Tenancy $tenancy, Response $response): void; -} diff --git a/src/Contracts/IdentityResolverUsesParameters.php b/src/Contracts/IdentityResolverUsesParameters.php index 8c45df0..f81be5e 100644 --- a/src/Contracts/IdentityResolverUsesParameters.php +++ b/src/Contracts/IdentityResolverUsesParameters.php @@ -6,9 +6,14 @@ use Illuminate\Routing\Route; /** + * Identity Resolver uses Parameters Contract + * + * This contract marks an identity resolver as being capable of using route + * parameters to resolve the identifier for a tenant. + * * @package Resolvers */ -interface IdentityResolverUsesParameters +interface IdentityResolverUsesParameters extends IdentityResolver { /** * Get the name of the route parameter diff --git a/src/Contracts/ServiceOverride.php b/src/Contracts/ServiceOverride.php index fa0589c..86c0758 100644 --- a/src/Contracts/ServiceOverride.php +++ b/src/Contracts/ServiceOverride.php @@ -7,6 +7,8 @@ * * This contract marks a class as being responsible for handling the overriding * of a core Laravel service, such as cookies, sessions, or the database. + * + * @package Overrides */ interface ServiceOverride { diff --git a/src/Database/Eloquent/Concerns/BelongsToManyTenants.php b/src/Database/Eloquent/Concerns/BelongsToManyTenants.php index ee9776a..2954e9b 100644 --- a/src/Database/Eloquent/Concerns/BelongsToManyTenants.php +++ b/src/Database/Eloquent/Concerns/BelongsToManyTenants.php @@ -6,10 +6,24 @@ use Sprout\Database\Eloquent\Observers\BelongsToManyTenantsObserver; use Sprout\Database\Eloquent\Scopes\BelongsToManyTenantsScope; +/** + * Belongs to many Tenants + * + * This trait provides the basic supporting functionality required to automate + * the relation between an Eloquent model and multiple {@see \Sprout\Contracts\Tenant}, + * using a belongs to many relationship. + * + * @package Database\Eloquent + */ trait BelongsToManyTenants { use IsTenantChild; + /** + * Boot the trait + * + * @return void + */ public static function bootBelongsToManyTenants(): void { // Automatically scope queries diff --git a/src/Database/Eloquent/Concerns/BelongsToTenant.php b/src/Database/Eloquent/Concerns/BelongsToTenant.php index 2c0b7d1..6413d62 100644 --- a/src/Database/Eloquent/Concerns/BelongsToTenant.php +++ b/src/Database/Eloquent/Concerns/BelongsToTenant.php @@ -6,10 +6,24 @@ use Sprout\Database\Eloquent\Observers\BelongsToTenantObserver; use Sprout\Database\Eloquent\Scopes\BelongsToTenantScope; +/** + * Belongs to many Tenants + * + * This trait provides the basic supporting functionality required to automate + * the relation between an Eloquent model and a {@see \Sprout\Contracts\Tenant}, + * using a belongs to relationship. + * + * @package Database\Eloquent + */ trait BelongsToTenant { use IsTenantChild; + /** + * Boot the trait + * + * @return void + */ public static function bootBelongsToTenant(): void { // Automatically scope queries diff --git a/src/Database/Eloquent/Concerns/HasTenantResources.php b/src/Database/Eloquent/Concerns/HasTenantResources.php index 8552c0d..9376dcb 100644 --- a/src/Database/Eloquent/Concerns/HasTenantResources.php +++ b/src/Database/Eloquent/Concerns/HasTenantResources.php @@ -8,12 +8,25 @@ use Sprout\Contracts\TenantHasResources; /** + * Has Tenant Resources + * + * This trait provides helper methods alongside default implementations and + * functionality to support a {@see \Sprout\Contracts\Tenant} model that also + * implements the {@see \Sprout\Contracts\TenantHasResources} interface. + * * @phpstan-require-implements \Sprout\Contracts\Tenant * @phpstan-require-implements \Sprout\Contracts\TenantHasResources * @phpstan-require-extends \Illuminate\Database\Eloquent\Model + * + * @package Database\Eloquent */ trait HasTenantResources { + /** + * Boot the trait + * + * @return void + */ public static function bootHasTenantResources(): void { static::creating(static function (Model&TenantHasResources $model) { diff --git a/src/Database/Eloquent/Concerns/IsTenant.php b/src/Database/Eloquent/Concerns/IsTenant.php index f32e34a..cb01603 100644 --- a/src/Database/Eloquent/Concerns/IsTenant.php +++ b/src/Database/Eloquent/Concerns/IsTenant.php @@ -4,8 +4,15 @@ namespace Sprout\Database\Eloquent\Concerns; /** + * Is Tenant + * + * This trait provides a default implementation of the {@see \Sprout\Contracts\Tenant} + * interface to simplify the creation of tenant models. + * * @phpstan-require-implements \Sprout\Contracts\Tenant * @phpstan-require-extends \Illuminate\Database\Eloquent\Model + * + * @pacakge Database\Eloquent */ trait IsTenant { diff --git a/src/Database/Eloquent/Concerns/IsTenantChild.php b/src/Database/Eloquent/Concerns/IsTenantChild.php index a59d901..41bb0e8 100644 --- a/src/Database/Eloquent/Concerns/IsTenantChild.php +++ b/src/Database/Eloquent/Concerns/IsTenantChild.php @@ -7,42 +7,74 @@ use ReflectionClass; use ReflectionException; use ReflectionMethod; -use RuntimeException; use Sprout\Attributes\TenantRelation; use Sprout\Contracts\Tenancy; use Sprout\Database\Eloquent\Contracts\OptionalTenant; +use Sprout\Exceptions\TenantRelationException; use Sprout\Managers\TenancyManager; /** + * Is Tenant Child + * + * This trait provides helper methods and functionality that supports the + * automatic handling of Eloquent models that are direct descendants of + * {@see \Sprout\Contracts\Tenant} models. + * * @phpstan-require-extends \Illuminate\Database\Eloquent\Model * * @mixin \Illuminate\Database\Eloquent\Model + * + * @package Database\Eloquent */ trait IsTenantChild { /** + * The name of the tenant relation + * * @var string */ protected static string $tenantRelationName; + /** + * Whether to ignore tenant ownership restrictions + * + * @var bool + */ protected static bool $ignoreTenantRestrictions = false; + /** + * Check if tenant ownership restrictions should be ignored + * + * @return bool + */ public static function shouldIgnoreTenantRestrictions(): bool { return self::$ignoreTenantRestrictions; } + /** + * Enable the ignoring of tenant ownership restrictions + * + * @return void + */ public static function ignoreTenantRestrictions(): void { self::$ignoreTenantRestrictions = true; } + /** + * Disable the ignoring of tenant ownership restrictions + * + * @return void + */ public static function resetTenantRestrictions(): void { self::$ignoreTenantRestrictions = false; } /** + * Temporarily disable tenant ownership restrictions and run the provided callback + * * @template RetType of mixed * * @param callable(): RetType $callback @@ -62,6 +94,13 @@ public static function withoutTenantRestrictions(callable $callback): mixed return $return; } + /** + * Check if the model can function without a tenant + * + * @return bool + * + * @see \Sprout\Database\Eloquent\Contracts\OptionalTenant + */ public static function isTenantOptional(): bool { return is_subclass_of(static::class, OptionalTenant::class) @@ -71,6 +110,15 @@ public static function isTenantOptional(): bool ); } + /** + * Attempt to find the name of the tenant relation + * + * @return string + * + * @throws \Sprout\Exceptions\TenantRelationException + * + * @see \Sprout\Attributes\TenantRelation + */ private function findTenantRelationName(): string { try { @@ -81,21 +129,26 @@ private function findTenantRelationName(): string ->map(fn (ReflectionMethod $method) => $method->getName()); if ($methods->isEmpty()) { - throw new RuntimeException('No tenant relation found in model [' . static::class . ']'); + throw TenantRelationException::missing(static::class); } if ($methods->count() > 1) { - throw new RuntimeException( - 'Models can only have one tenant relation, [' . static::class . '] has ' . $methods->count() - ); + throw TenantRelationException::tooMany(static::class, $methods->count()); } return $methods->first(); } catch (ReflectionException $exception) { - throw new RuntimeException('Unable to find tenant relation for model [' . static::class . ']', previous: $exception); // @codeCoverageIgnore + throw TenantRelationException::missing(static::class, previous: $exception); // @codeCoverageIgnore } } + /** + * Get the name of the tenant relation + * + * @return string|null + * + * @throws \Sprout\Exceptions\TenantRelationException + */ public function getTenantRelationName(): ?string { if (! isset($this->tenantRelationNames[static::class])) { @@ -105,16 +158,34 @@ public function getTenantRelationName(): ?string return self::$tenantRelationName ?? null; } + /** + * Get the name of the tenancy this model relates to a tenant of + * + * @return string|null + */ public function getTenancyName(): ?string { return null; } + /** + * Get the tenancy this model relates to a tenant of + * + * @return \Sprout\Contracts\Tenancy + */ public function getTenancy(): Tenancy { - return app(TenancyManager::class)->get($this->getTenancyName()); + /** @var \Sprout\Managers\TenancyManager $tenancyManager */ + $tenancyManager = app(TenancyManager::class); + + return $tenancyManager->get($this->getTenancyName()); } + /** + * Get the tenant relation + * + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ public function getTenantRelation(): Relation { return $this->{$this->getTenantRelationName()}(); diff --git a/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php b/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php index c3f07a7..6436ff0 100644 --- a/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php +++ b/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php @@ -12,8 +12,18 @@ use Sprout\TenancyOptions; /** + * Belongs to Many Tenants Observer + * + * This is an observer automatically attached to Eloquent models that relate to + * tenants using a "belongs to many" relationship, to automate association + * and hydration of the tenant relation. + * * @template ChildModel of \Illuminate\Database\Eloquent\Model * @template TenantModel of \Illuminate\Database\Eloquent\Model&\Sprout\Contracts\Tenant + * + * @see \Sprout\Database\Eloquent\Concerns\BelongsToManyTenants + * + * @package Database\Eloquent */ class BelongsToManyTenantsObserver { @@ -126,6 +136,10 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsToMa } /** + * Handle the created event on the model + * + * The created event is fired after a model is persisted to the database. + * * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToManyTenants $model * * @return void @@ -169,6 +183,11 @@ public function created(Model $model): void } /** + * Handle the retrieved event on the model + * + * The retrieved event is fired after a model is retrieved from + * persistent storage and hydrated. + * * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToManyTenants $model * * @return void @@ -194,7 +213,11 @@ public function retrieved(Model $model): void // If the initial checks do not pass if (! $this->passesInitialChecks($model, $tenancy, $relation, true)) { - // Just exit, an exception will have be thrown + // Just exit, an exception will have been thrown + return; + } + + if (! TenancyOptions::shouldHydrateTenantRelation($tenancy)) { return; } @@ -209,6 +232,8 @@ public function retrieved(Model $model): void } /** + * Set the hydrate value of a relation + * * @param \Illuminate\Database\Eloquent\Model $model * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation * @param \Sprout\Contracts\Tenant $tenant diff --git a/src/Database/Eloquent/Observers/BelongsToTenantObserver.php b/src/Database/Eloquent/Observers/BelongsToTenantObserver.php index f3749e2..47a1fd3 100644 --- a/src/Database/Eloquent/Observers/BelongsToTenantObserver.php +++ b/src/Database/Eloquent/Observers/BelongsToTenantObserver.php @@ -12,8 +12,18 @@ use Sprout\TenancyOptions; /** + * Belongs to Tenant Observer + * + * This is an observer automatically attached to Eloquent models that relate to + * a single tenant using a "belongs to" relationship, to automate association + * and hydration of the tenant relation. + * * @template ChildModel of \Illuminate\Database\Eloquent\Model * @template TenantModel of \Illuminate\Database\Eloquent\Model&\Sprout\Contracts\Tenant + * + * @see \Sprout\Database\Eloquent\Concerns\BelongsToTenant + * + * @package Database\Eloquent */ class BelongsToTenantObserver { @@ -108,6 +118,11 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsTo $ } /** + * Handle the creating event on the model + * + * The creating event is fired right before a model is persisted to the + * database. + * * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model * * @return bool @@ -144,6 +159,11 @@ public function creating(Model $model): bool } /** + * Handle the retrieved event on the model + * + * The retrieved event is fired after a model is retrieved from + * persistent storage and hydrated. + * * @param \Illuminate\Database\Eloquent\Model&\Sprout\Database\Eloquent\Concerns\BelongsToTenant $model * * @return void @@ -173,6 +193,10 @@ public function retrieved(Model $model): void return; } + if (! TenancyOptions::shouldHydrateTenantRelation($tenancy)) { + return; + } + // Populate the relation with the tenant $model->setRelation($relation->getRelationName(), $tenancy->tenant()); } diff --git a/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php b/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php index c5f09a6..3d23f82 100644 --- a/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php +++ b/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php @@ -5,10 +5,21 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Sprout\Database\Eloquent\Contracts\OptionalTenant; -use Sprout\Database\Eloquent\TenantChildScope; use Sprout\Exceptions\TenantMissing; +/** + * Belongs to many Tenants Scope + * + * This is a scope that is automatically attached to Eloquent models that relate + * to tenants using a "belongs to many" relationship. + * + * It automatically adds the necessary clauses to queries to help avoid data + * leaking between tenants in a "Shared Database, Shared Schema" setup. + * + * @see \Sprout\Database\Eloquent\Concerns\BelongsToManyTenants + * + * @package Database\Eloquent + */ final class BelongsToManyTenantsScope extends TenantChildScope { /** @@ -44,7 +55,7 @@ public function apply(Builder $builder, Model $model): void // Finally, add the clause so that all queries are scoped to the // current tenant $builder->whereHas( - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore-next-line */ $model->getTenantRelationName(), function (Builder $builder) use ($tenancy) { $builder->whereKey($tenancy->key()); diff --git a/src/Database/Eloquent/Scopes/BelongsToTenantScope.php b/src/Database/Eloquent/Scopes/BelongsToTenantScope.php index dbf3bf3..c166ba1 100644 --- a/src/Database/Eloquent/Scopes/BelongsToTenantScope.php +++ b/src/Database/Eloquent/Scopes/BelongsToTenantScope.php @@ -5,10 +5,21 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Sprout\Database\Eloquent\Contracts\OptionalTenant; -use Sprout\Database\Eloquent\TenantChildScope; use Sprout\Exceptions\TenantMissing; +/** + * Belongs to Tenant Scope + * + * This is a scope automatically attached to Eloquent models that relate + * to a single tenant using a "belongs to" relationship. + * + * It automatically adds the necessary clauses to queries to help avoid data + * leaking between tenants in a "Shared Database, Shared Schema" setup. + * + * @see \Sprout\Database\Eloquent\Concerns\BelongsToManyTenants + * + * @package Database\Eloquent + */ final class BelongsToTenantScope extends TenantChildScope { /** diff --git a/src/Database/Eloquent/TenantChildScope.php b/src/Database/Eloquent/Scopes/TenantChildScope.php similarity index 66% rename from src/Database/Eloquent/TenantChildScope.php rename to src/Database/Eloquent/Scopes/TenantChildScope.php index 877f30b..b183770 100644 --- a/src/Database/Eloquent/TenantChildScope.php +++ b/src/Database/Eloquent/Scopes/TenantChildScope.php @@ -1,12 +1,21 @@ null. + * + * Sprout makes heavy use of this event internally to bootstrap tenants tenancies. + * * @template TenantClass of Tenant * * @method static void dispatch(Tenancy $tenancy, Tenant|null $previous, Tenant|null $current) * @method static void dispatchIf(bool $condition, Tenancy $tenancy, Tenant|null $previous, Tenant|null $current) * @method static void dispatchUnless(bool $condition, Tenancy $tenancy, Tenant|null $previous, Tenant|null $current) + * + * @package Core */ final readonly class CurrentTenantChanged { use Dispatchable; /** + * The tenancy whose current tenant changed + * * @var \Sprout\Contracts\Tenancy */ public Tenancy $tenancy; /** + * The current tenant + * * @var \Sprout\Contracts\Tenant|null * * @phpstan-var TenantClass|null @@ -31,6 +47,8 @@ public ?Tenant $current; /** + * The previous tenant + * * @var \Sprout\Contracts\Tenant|null * * @phpstan-var TenantClass|null @@ -38,6 +56,8 @@ public ?Tenant $previous; /** + * Create a new instance + * * @param \Sprout\Contracts\Tenancy $tenancy * @param \Sprout\Contracts\Tenant|null $previous * @param \Sprout\Contracts\Tenant|null $current diff --git a/src/Events/TenantFound.php b/src/Events/TenantFound.php index c862b75..364eefd 100644 --- a/src/Events/TenantFound.php +++ b/src/Events/TenantFound.php @@ -8,6 +8,16 @@ use Sprout\Contracts\Tenant; /** + * Tenant Found Event + * + * This is an event used to notify the application and provide reactivity when + * a tenant is found. + * It is an abstract base class that exists so that developers can listen to + * all events where tenants are found, regardless of the method used. + * + * @see \Sprout\Events\TenantIdentified + * @see \Sprout\Events\TenantLoaded + * * @template TenantClass of Tenant * * @method static void dispatch(Tenant $tenant, Tenancy $tenancy) @@ -19,13 +29,8 @@ use Dispatchable; /** - * @var \Sprout\Contracts\Tenant + * The tenancy whose tenant was found * - * @phpstan-var TenantClass - */ - public Tenant $tenant; - - /** * @var \Sprout\Contracts\Tenancy * * @phpstan-var \Sprout\Contracts\Tenancy @@ -33,6 +38,17 @@ public Tenancy $tenancy; /** + * The tenant that was found + * + * @var \Sprout\Contracts\Tenant + * + * @phpstan-var TenantClass + */ + public Tenant $tenant; + + /** + * Create a new instance + * * @param \Sprout\Contracts\Tenant $tenant * @param \Sprout\Contracts\Tenancy $tenancy * diff --git a/src/Events/TenantIdentified.php b/src/Events/TenantIdentified.php index 73ee9b0..ec91e27 100644 --- a/src/Events/TenantIdentified.php +++ b/src/Events/TenantIdentified.php @@ -4,9 +4,17 @@ namespace Sprout\Events; /** + * Tenant Identified Event + * + * This is a child of {@see \Sprout\Events\TenantFound} that is used to notify + * the application and provide reactivity when a tenant is found using its + * identifier, referred to as being "identified". + * * @template TenantClass of \Sprout\Contracts\Tenant * * @extends \Sprout\Events\TenantFound + * + * @package Core */ final readonly class TenantIdentified extends TenantFound { diff --git a/src/Events/TenantLoaded.php b/src/Events/TenantLoaded.php index c983491..c4af520 100644 --- a/src/Events/TenantLoaded.php +++ b/src/Events/TenantLoaded.php @@ -4,9 +4,17 @@ namespace Sprout\Events; /** + * Tenant Identified Event + * + * This is a child of {@see \Sprout\Events\TenantFound} that is used to notify + * the application and provide reactivity when a tenant is found using its + * key, referred to as being "loaded". + * * @template TenantClass of \Sprout\Contracts\Tenant * * @extends \Sprout\Events\TenantFound + * + * @package Core */ final readonly class TenantLoaded extends TenantFound { diff --git a/src/Exceptions/CompatibilityException.php b/src/Exceptions/CompatibilityException.php new file mode 100644 index 0000000..0db131e --- /dev/null +++ b/src/Exceptions/CompatibilityException.php @@ -0,0 +1,30 @@ +sprout = $sprout; + } + + /** + * Handle the request + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string ...$options + * + * @return \Illuminate\Http\Response + * + * @throws \Sprout\Exceptions\NoTenantFound + */ + public function handle(Request $request, Closure $next, string ...$options): Response + { + [$resolverName, $tenancyName] = ResolutionHelper::parseOptions($options); + + /** @var \Illuminate\Http\Response $response */ + $response = $next($request); + + /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ + $tenancy = $this->sprout->tenancies()->get($tenancyName); + + if (! $tenancy->check()) { + return $response; + } + + $resolver = $tenancy->resolver(); + + if (! ($resolver instanceof HeaderIdentityResolver) || $resolver->getName() !== $resolverName) { + return $response; + } + + return $response->withHeaders([ + $resolver->getRequestHeaderName($tenancy) => $tenancy->identifier() + ]); + } +} diff --git a/src/Http/Middleware/TenantRoutes.php b/src/Http/Middleware/TenantRoutes.php index b538947..a03dc87 100644 --- a/src/Http/Middleware/TenantRoutes.php +++ b/src/Http/Middleware/TenantRoutes.php @@ -6,34 +6,28 @@ use Closure; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Sprout\Contracts\IdentityResolverTerminates; -use Sprout\Exceptions\NoTenantFound; -use Sprout\Managers\IdentityResolverManager; -use Sprout\Managers\TenancyManager; -use Sprout\Sprout; use Sprout\Support\ResolutionHelper; use Sprout\Support\ResolutionHook; /** * Tenant Routes Middleware * - * Marks routes are being tenanted. + * This piece of middleware has a dual function. + * It marks routes as being multitenanted if resolving during routing, and it + * will resolve tenants if resolving during middleware. + * + * @package Core */ final class TenantRoutes { - public const ALIAS = 'sprout.tenanted'; - /** - * @var \Sprout\Sprout + * The alias for this middleware */ - private Sprout $sprout; - - public function __construct(Sprout $sprout) - { - $this->sprout = $sprout; - } + public const ALIAS = 'sprout.tenanted'; /** + * Handle the request + * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string ...$options @@ -44,45 +38,15 @@ public function __construct(Sprout $sprout) */ public function handle(Request $request, Closure $next, string ...$options): Response { - if (count($options) === 2) { - [$resolverName, $tenancyName] = $options; - } else if (count($options) === 1) { - [$resolverName] = $options; - $tenancyName = null; - } else { - $resolverName = $tenancyName = null; - } + [$resolverName, $tenancyName] = ResolutionHelper::parseOptions($options); ResolutionHelper::handleResolution( $request, ResolutionHook::Middleware, $resolverName, - $tenancyName + $tenancyName, ); - // TODO: Decide whether to do anything with the following conditions - //if (! $tenancy->wasResolved()) { - //} - // - //if ($tenancy->resolver() !== $resolver) { - //} - return $next($request); } - - public function terminate(Request $request, Response $response): void - { - if ($this->sprout->hasCurrentTenancy()) { - /** @var \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy */ - $tenancy = $this->sprout->getCurrentTenancy(); - - if ($tenancy->wasResolved()) { - $resolver = $tenancy->resolver(); - - if ($resolver instanceof IdentityResolverTerminates) { - $resolver->terminate($tenancy, $response); - } - } - } - } } diff --git a/src/Http/Resolvers/CookieIdentityResolver.php b/src/Http/Resolvers/CookieIdentityResolver.php index 0857554..4e9831d 100644 --- a/src/Http/Resolvers/CookieIdentityResolver.php +++ b/src/Http/Resolvers/CookieIdentityResolver.php @@ -4,26 +4,43 @@ namespace Sprout\Http\Resolvers; use Closure; +use Illuminate\Cookie\CookieJar; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Illuminate\Routing\Router; use Illuminate\Routing\RouteRegistrar; use Illuminate\Support\Facades\Cookie; -use Sprout\Contracts\IdentityResolverTerminates; use Sprout\Contracts\Tenancy; +use Sprout\Contracts\Tenant; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Support\BaseIdentityResolver; -final class CookieIdentityResolver extends BaseIdentityResolver implements IdentityResolverTerminates +/** + * Cookie Identity Resolver + * + * This class is responsible for resolving tenant identities from the current + * request using cookies. + * + * @package Http\Resolvers + */ +final class CookieIdentityResolver extends BaseIdentityResolver { + /** + * The cookie name + * + * @var string + */ private string $cookie; /** + * Additional options for the cookie + * * @var array */ private array $options; /** + * Create a new instance + * * @param string $name * @param string|null $cookie * @param array $options @@ -37,13 +54,28 @@ public function __construct(string $name, ?string $cookie = null, array $options $this->options = $options; } - public function getCookie(): string + /** + * Get the cookie name + * + * @return string + */ + public function getCookieName(): string { return $this->cookie; } /** - * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy + * Get the cookie name with replacements + * + * This method returns the name of the cookie returned by + * {@see self::getCookieName()}, except it replaces {tenancy} + * and {resolver} with the name of the tenancy, and resolver, + * respectively. + * + * You can use an uppercase character for the first character, {Tenancy} + * and {Resolver}, and it'll be run through {@see \ucfirst()}. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy * * @return string */ @@ -52,7 +84,7 @@ public function getRequestCookieName(Tenancy $tenancy): string return str_replace( ['{tenancy}', '{resolver}', '{Tenancy}', '{Resolver}'], [$tenancy->getName(), $this->getName(), ucfirst($tenancy->getName()), ucfirst($this->getName())], - $this->getCookie() + $this->getCookieName() ); } @@ -101,14 +133,25 @@ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): } /** - * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy - * @param \Illuminate\Http\Response $response + * Perform setup actions for the tenant + * + * When a tenant is marked as the current tenant within a tenancy, this + * method will be called to perform any necessary setup actions. + * This method is also called if there is no current tenant, as there may + * be actions needed. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant|null $tenant + * + * @phpstan-param Tenant|null $tenant * * @return void */ - public function terminate(Tenancy $tenancy, Response $response): void + public function setup(Tenancy $tenancy, ?Tenant $tenant): void { - if ($tenancy->check()) { + if ($tenant !== null && $tenancy->check()) { /** * @var array{name:string, value:string} $details */ @@ -119,11 +162,13 @@ public function terminate(Tenancy $tenancy, Response $response): void ] ); - $response->withCookie(Cookie::make(...$details)); + app(CookieJar::class)->queue(Cookie::make(...$details)); } } /** + * Get the details for cookie creation, with defaults + * * @param array $details * * @return array diff --git a/src/Http/Resolvers/HeaderIdentityResolver.php b/src/Http/Resolvers/HeaderIdentityResolver.php index 64ade02..9b602bf 100644 --- a/src/Http/Resolvers/HeaderIdentityResolver.php +++ b/src/Http/Resolvers/HeaderIdentityResolver.php @@ -8,13 +8,34 @@ use Illuminate\Routing\Router; use Illuminate\Routing\RouteRegistrar; use Sprout\Contracts\Tenancy; +use Sprout\Http\Middleware\AddTenantHeaderToResponse; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Support\BaseIdentityResolver; +/** + * Header Identity Resolver + * + * This class is responsible for resolving tenant identities from the current + * request using headers. + * + * @package Http\Resolvers + */ final class HeaderIdentityResolver extends BaseIdentityResolver { + /** + * The header name + * + * @var string + */ private string $header; + /** + * Create a new instance + * + * @param string $name + * @param string|null $header + * @param array<\Sprout\Support\ResolutionHook> $hooks + */ public function __construct(string $name, ?string $header = null, array $hooks = []) { parent::__construct($name, $hooks); @@ -23,15 +44,27 @@ public function __construct(string $name, ?string $header = null, array $hooks = } /** + * Get the name of the header + * * @return string */ - public function getHeader(): string + public function getHeaderName(): string { return $this->header; } /** - * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy + * Get the header name with replacements + * + * This method returns the name of the header returned by + * {@see self::getHeaderName()}, except it replaces {tenancy} + * and {resolver} with the name of the tenancy, and resolver, + * respectively. + * + * You can use an uppercase character for the first character, {Tenancy} + * and {Resolver}, and it'll be run through {@see \ucfirst()}. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy * * @return string */ @@ -40,7 +73,7 @@ public function getRequestHeaderName(Tenancy $tenancy): string return str_replace( ['{tenancy}', '{resolver}', '{Tenancy}', '{Resolver}'], [$tenancy->getName(), $this->getName(), ucfirst($tenancy->getName()), ucfirst($this->getName())], - $this->getHeader() + $this->getHeaderName() ); } @@ -77,7 +110,9 @@ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string */ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): RouteRegistrar { - return $router->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]) - ->group($groupRoutes); + return $router->middleware([ + TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName(), + AddTenantHeaderToResponse::class . ':' . $this->getName() . ',' . $tenancy->getName(), + ])->group($groupRoutes); } } diff --git a/src/Http/Resolvers/PathIdentityResolver.php b/src/Http/Resolvers/PathIdentityResolver.php index c6c8a1b..efc4670 100644 --- a/src/Http/Resolvers/PathIdentityResolver.php +++ b/src/Http/Resolvers/PathIdentityResolver.php @@ -17,14 +17,36 @@ use Sprout\Overrides\SessionOverride; use Sprout\Support\BaseIdentityResolver; +/** + * Path Identity Resolver + * + * This class is responsible for resolving tenant identities from the current + * request using the path. + * + * @package Http\Resolvers + */ final class PathIdentityResolver extends BaseIdentityResolver implements IdentityResolverUsesParameters { use FindsIdentityInRouteParameter { setup as parameterSetup; } + /** + * The path segment containing the identifier + * + * @var int + */ private int $segment = 1; + /** + * Create a new instance + * + * @param string $name + * @param int|null $segment + * @param string|null $pattern + * @param string|null $parameter + * @param array<\Sprout\Support\ResolutionHook> $hooks + */ public function __construct(string $name, ?int $segment = null, ?string $pattern = null, ?string $parameter = null, array $hooks = []) { parent::__construct($name, $hooks); @@ -79,7 +101,7 @@ public function getSegment(): int */ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): RouteRegistrar { - return $this->applyParameterPattern( + return $this->applyParameterPatternMapping( $router->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]) ->prefix($this->getRoutePrefix($tenancy)) ->group($groupRoutes), diff --git a/src/Http/Resolvers/SessionIdentityResolver.php b/src/Http/Resolvers/SessionIdentityResolver.php index fdb5175..4ec51e3 100644 --- a/src/Http/Resolvers/SessionIdentityResolver.php +++ b/src/Http/Resolvers/SessionIdentityResolver.php @@ -7,19 +7,34 @@ use Illuminate\Http\Request; use Illuminate\Routing\Router; use Illuminate\Routing\RouteRegistrar; -use RuntimeException; use Sprout\Contracts\Tenancy; +use Sprout\Exceptions\CompatibilityException; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Overrides\SessionOverride; use Sprout\Support\BaseIdentityResolver; use Sprout\Support\ResolutionHook; use function Sprout\sprout; +/** + * Session Identity Resolver + * + * This class is responsible for resolving tenant identities from the current + * request using the session. + * + * @package Http\Resolvers + */ final class SessionIdentityResolver extends BaseIdentityResolver { + /** + * The name of the session + * + * @var string + */ private string $session; /** + * Create a new instance + * * @param string $name * @param string|null $session */ @@ -30,13 +45,28 @@ public function __construct(string $name, ?string $session = null) $this->session = $session ?? 'multitenancy.{tenancy}'; } - public function getSession(): string + /** + * Get the name of the session + * + * @return string + */ + public function getSessionName(): string { return $this->session; } /** - * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy + * Get the session name with replacements + * + * This method returns the name of the header returned by + * {@see self::getSessionName()}, except it replaces {tenancy} + * and {resolver} with the name of the tenancy, and resolver, + * respectively. + * + * You can use an uppercase character for the first character, {Tenancy} + * and {Resolver}, and it'll be run through {@see \ucfirst()}. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy * * @return string */ @@ -45,7 +75,7 @@ public function getRequestSessionName(Tenancy $tenancy): string return str_replace( ['{tenancy}', '{resolver}', '{Tenancy}', '{Resolver}'], [$tenancy->getName(), $this->getName(), ucfirst($tenancy->getName()), ucfirst($this->getName())], - $this->getSession() + $this->getSessionName() ); } @@ -60,11 +90,13 @@ public function getRequestSessionName(Tenancy $tenancy): string * @param \Sprout\Contracts\Tenancy $tenancy * * @return string|null + * + * @throws \Sprout\Exceptions\CompatibilityException */ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string { if (sprout()->hasOverride(SessionOverride::class)) { - throw new RuntimeException('Cannot use the session resolver for tenancy [' . $tenancy->getName() . '] and the session override'); + throw CompatibilityException::make('resolver', $this->getName(), 'override', SessionOverride::class); } /** diff --git a/src/Http/Resolvers/SubdomainIdentityResolver.php b/src/Http/Resolvers/SubdomainIdentityResolver.php index f42da2f..07b46ca 100644 --- a/src/Http/Resolvers/SubdomainIdentityResolver.php +++ b/src/Http/Resolvers/SubdomainIdentityResolver.php @@ -17,14 +17,36 @@ use Sprout\Overrides\SessionOverride; use Sprout\Support\BaseIdentityResolver; +/** + * The Subdomain Identity Resolver + * + * This class is responsible for resolving tenant identities from the current + * request using a subdomain. + * + * @package Http\Resolvers + */ final class SubdomainIdentityResolver extends BaseIdentityResolver implements IdentityResolverUsesParameters { use FindsIdentityInRouteParameter { setup as parameterSetup; } + /** + * The parent domain + * + * @var string + */ private string $domain; + /** + * Create a new instance + * + * @param string $name + * @param string $domain + * @param string|null $pattern + * @param string|null $parameter + * @param array<\Sprout\Support\ResolutionHook> $hooks + */ public function __construct(string $name, string $domain, ?string $pattern = null, ?string $parameter = null, array $hooks = []) { parent::__construct($name, $hooks); @@ -58,6 +80,8 @@ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string } /** + * Get the domain name with parameter for the route definition + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Contracts\Tenancy $tenancy @@ -85,7 +109,7 @@ protected function getRouteDomain(Tenancy $tenancy): string */ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): RouteRegistrar { - return $this->applyParameterPattern( + return $this->applyParameterPatternMapping( $router->domain($this->getRouteDomain($tenancy)) ->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]) ->group($groupRoutes), diff --git a/src/Http/RouterMethods.php b/src/Http/RouterMethods.php index 97e3752..165e574 100644 --- a/src/Http/RouterMethods.php +++ b/src/Http/RouterMethods.php @@ -8,9 +8,19 @@ use Sprout\Managers\IdentityResolverManager; use Sprout\Managers\TenancyManager; +/** + * Route Methods Mixin + * + * This class is used as a mixin to add Sprout specific methods to + * {@see \Illuminate\Routing\Router}. + * + * @package Core + */ class RouterMethods { /** + * Create tenanted routes + * * @param Closure $routes * @param string|null $resolver * @param string|null $tenancy diff --git a/src/Listeners/CleanupServiceOverrides.php b/src/Listeners/CleanupServiceOverrides.php index a43ac84..dd6c2ef 100644 --- a/src/Listeners/CleanupServiceOverrides.php +++ b/src/Listeners/CleanupServiceOverrides.php @@ -6,6 +6,14 @@ use Sprout\Events\CurrentTenantChanged; use Sprout\Sprout; +/** + * Clean-up Service Overrides + * + * This class is an event listener for {@see \Sprout\Events\CurrentTenantChanged} + * that cleans up any existing service overrides when the tenancy changes. + * + * @package Overrides + */ final class CleanupServiceOverrides { /** @@ -13,12 +21,19 @@ final class CleanupServiceOverrides */ private Sprout $sprout; + /** + * Create a new instance + * + * @param \Sprout\Sprout $sprout + */ public function __construct(Sprout $sprout) { $this->sprout = $sprout; } /** + * Handle event + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Events\CurrentTenantChanged $event diff --git a/src/Listeners/IdentifyTenantOnRouting.php b/src/Listeners/IdentifyTenantOnRouting.php index f683999..f0b17a8 100644 --- a/src/Listeners/IdentifyTenantOnRouting.php +++ b/src/Listeners/IdentifyTenantOnRouting.php @@ -11,9 +11,19 @@ use Sprout\Support\ResolutionHelper; use Sprout\Support\ResolutionHook; +/** + * Identify Tenant on Routing + * + * This class is an event listener for {@see \Illuminate\Routing\Events\RouteMatched} + * that handles tenant identification if it's enabled. + * + * @package Core + */ final class IdentifyTenantOnRouting { /** + * Handle the event + * * @param \Illuminate\Routing\Events\RouteMatched $event * * @return void @@ -40,6 +50,8 @@ public function handle(RouteMatched $event): void } /** + * Parse the route middleware stack to find the marker middleware + * * @param \Illuminate\Routing\Route $route * * @return array|null diff --git a/src/Listeners/PerformIdentityResolverSetup.php b/src/Listeners/PerformIdentityResolverSetup.php index c04b87b..c9f6caa 100644 --- a/src/Listeners/PerformIdentityResolverSetup.php +++ b/src/Listeners/PerformIdentityResolverSetup.php @@ -9,9 +9,19 @@ use Sprout\Events\CurrentTenantChanged; use Sprout\Sprout; +/** + * Perform Identity Resolver Setup + * + * This class is an event listener for {@see \Sprout\Events\CurrentTenantChanged} + * that handles the setup action hook for the current resolver. + * + * @package Core + */ final class PerformIdentityResolverSetup { /** + * Handle the event + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Events\CurrentTenantChanged $event diff --git a/src/Listeners/SetCurrentTenantContext.php b/src/Listeners/SetCurrentTenantContext.php index 90cb42c..7ac412c 100644 --- a/src/Listeners/SetCurrentTenantContext.php +++ b/src/Listeners/SetCurrentTenantContext.php @@ -7,6 +7,15 @@ use Sprout\Events\CurrentTenantChanged; use Sprout\Sprout; +/** + * Set Current Tenant Context + * + * This class is an event listener for {@see \Sprout\Events\CurrentTenantChanged} + * that handles the setting of the current tenants key, within Laravels + * context service. + * + * @package Core + */ final class SetCurrentTenantContext { /** diff --git a/src/Listeners/SetCurrentTenantForJob.php b/src/Listeners/SetCurrentTenantForJob.php index 3deaec3..3151a96 100644 --- a/src/Listeners/SetCurrentTenantForJob.php +++ b/src/Listeners/SetCurrentTenantForJob.php @@ -8,6 +8,15 @@ use Sprout\Managers\TenancyManager; use Sprout\TenancyOptions; +/** + * Set Current Tenant For Job + * + * This class is an event listener for {@see \Illuminate\Queue\Events\JobProcessing} + * that ensures there are current tenants when processing jobs, utilising + * Laravels context service. + * + * @package Overrides + */ final class SetCurrentTenantForJob { /** @@ -33,12 +42,8 @@ public function handle(JobProcessing $event): void /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ $tenancy = $this->tenancies->get($tenancyName); - // We don't want to set a tenant if there's already one, and we don't - // want to set a tenant on tenancies that don't have tenant-aware jobs - if (! $tenancy->check() && TenancyOptions::shouldJobsBeTenantAware($tenancy)) { - // It's always the key, so we load instead of identifying - $tenancy->load($key); - } + // It's always the key, so we load instead of identifying + $tenancy->load($key); } } } diff --git a/src/Listeners/SetupServiceOverrides.php b/src/Listeners/SetupServiceOverrides.php index 50ca93d..f3395af 100644 --- a/src/Listeners/SetupServiceOverrides.php +++ b/src/Listeners/SetupServiceOverrides.php @@ -6,6 +6,14 @@ use Sprout\Events\CurrentTenantChanged; use Sprout\Sprout; +/** + * Setup Service Overrides + * + * This class is an event listener for {@see \Sprout\Events\CurrentTenantChanged} + * that sets up any service overrides using their setup action hook. + * + * @package Override + */ final class SetupServiceOverrides { /** @@ -13,12 +21,19 @@ final class SetupServiceOverrides */ private Sprout $sprout; + /** + * Create a new instance + * + * @param \Sprout\Sprout $sprout + */ public function __construct(Sprout $sprout) { $this->sprout = $sprout; } /** + * Handle the event + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Events\CurrentTenantChanged $event diff --git a/src/Managers/IdentityResolverManager.php b/src/Managers/IdentityResolverManager.php index 4bed29b..5035374 100644 --- a/src/Managers/IdentityResolverManager.php +++ b/src/Managers/IdentityResolverManager.php @@ -3,7 +3,7 @@ namespace Sprout\Managers; -use InvalidArgumentException; +use Sprout\Exceptions\MisconfigurationException; use Sprout\Http\Resolvers\CookieIdentityResolver; use Sprout\Http\Resolvers\HeaderIdentityResolver; use Sprout\Http\Resolvers\PathIdentityResolver; @@ -12,7 +12,14 @@ use Sprout\Support\BaseFactory; /** + * Identity Resolver Manager + * + * This is a manager and factory, responsible for creating and storing + * implementations of {@see \Sprout\Contracts\IdentityResolver}. + * * @extends \Sprout\Support\BaseFactory<\Sprout\Contracts\IdentityResolver> + * + * @package Core */ final class IdentityResolverManager extends BaseFactory { @@ -47,13 +54,13 @@ protected function getConfigKey(string $name): string * @phpstan-param array{domain?: string, pattern?: string|null, parameter?: string|null, hooks?: array<\Sprout\Support\ResolutionHook>} $config * * @return \Sprout\Http\Resolvers\SubdomainIdentityResolver + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function createSubdomainResolver(array $config, string $name): SubdomainIdentityResolver { if (! isset($config['domain'])) { - throw new InvalidArgumentException( - 'No domain provided for resolver [' . $name . ']' - ); + throw MisconfigurationException::missingConfig('domain', 'resolver', $name); } return new SubdomainIdentityResolver( @@ -74,15 +81,15 @@ protected function createSubdomainResolver(array $config, string $name): Subdoma * @phpstan-param array{segment?: int|null, pattern?: string|null, parameter?: string|null, hooks?: array<\Sprout\Support\ResolutionHook>} $config * * @return \Sprout\Http\Resolvers\PathIdentityResolver + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function createPathResolver(array $config, string $name): PathIdentityResolver { $segment = $config['segment'] ?? 1; if ($segment < 1) { - throw new InvalidArgumentException( - 'Invalid path segment [' . $segment . '], path segments should be 1 indexed' - ); + throw MisconfigurationException::invalidConfig('segment', 'resolver', $name); } return new PathIdentityResolver( diff --git a/src/Managers/ProviderManager.php b/src/Managers/ProviderManager.php index 603fe1d..9bc152d 100644 --- a/src/Managers/ProviderManager.php +++ b/src/Managers/ProviderManager.php @@ -4,17 +4,22 @@ namespace Sprout\Managers; use Illuminate\Database\Eloquent\Model; -use InvalidArgumentException; use Sprout\Contracts\Tenant; +use Sprout\Exceptions\MisconfigurationException; use Sprout\Providers\DatabaseTenantProvider; use Sprout\Providers\EloquentTenantProvider; use Sprout\Support\BaseFactory; use Sprout\Support\GenericTenant; /** + * Tenant Provider Manager + * + * This is a manager and factory, responsible for creating and storing + * implementations of {@see \Sprout\Contracts\TenantProvider}. + * * @extends \Sprout\Support\BaseFactory<\Sprout\Contracts\TenantProvider> * - * @package Providers + * @package Core */ final class ProviderManager extends BaseFactory { @@ -53,13 +58,13 @@ protected function getConfigKey(string $name): string * @phpstan-param array{model?: class-string} $config * * @phpstan-return \Sprout\Providers\EloquentTenantProvider + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function createEloquentProvider(array $config, string $name): EloquentTenantProvider { if (! isset($config['model'])) { - throw new InvalidArgumentException( - 'No model provided for provider [' . $name . ']' - ); + throw MisconfigurationException::missingConfig('model', 'provider', $name); } if ( @@ -67,12 +72,10 @@ protected function createEloquentProvider(array $config, string $name): Eloquent || ! is_subclass_of($config['model'], Model::class) || ! is_subclass_of($config['model'], Tenant::class) ) { - throw new InvalidArgumentException( - 'Invalid model provided for provider [' . $name . ']' - ); + throw MisconfigurationException::invalidConfig('model', 'provider', $name); } - return new EloquentTenantProvider($name, $config['model']); + return new EloquentTenantProvider($name, $config['model']); } /** @@ -88,6 +91,8 @@ protected function createEloquentProvider(array $config, string $name): Eloquent * @phpstan-param array{entity?: class-string, table?: string|class-string<\Illuminate\Database\Eloquent\Model>, connection?: string} $config * * @phpstan-return \Sprout\Providers\DatabaseTenantProvider + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function createDatabaseProvider(array $config, string $name): DatabaseTenantProvider { @@ -98,15 +103,11 @@ protected function createDatabaseProvider(array $config, string $name): Database || ! is_subclass_of($config['entity'], Tenant::class) ) ) { - throw new InvalidArgumentException( - 'Invalid entity provided for provider [' . $name . ']' - ); + throw MisconfigurationException::invalidConfig('entity', 'provider', $name); } if (! isset($config['table'])) { - throw new InvalidArgumentException( - 'No table provided for provider [' . $name . ']' - ); + throw MisconfigurationException::missingConfig('table', 'provider', $name); } // This allows users to provide a model name for retrieval of table and @@ -115,9 +116,7 @@ protected function createDatabaseProvider(array $config, string $name): Database // It's worth checking that the provided value is in fact a model, // otherwise things are going to get awkward if (! is_subclass_of($config['table'], Model::class)) { - throw new InvalidArgumentException( - 'Invalid table provided for provider [' . $name . ']' - ); + throw MisconfigurationException::invalidConfig('table', 'provider', $name); } $model = new $config['table'](); diff --git a/src/Managers/TenancyManager.php b/src/Managers/TenancyManager.php index 2cd90ff..46d2e23 100644 --- a/src/Managers/TenancyManager.php +++ b/src/Managers/TenancyManager.php @@ -8,6 +8,11 @@ use Sprout\Support\DefaultTenancy; /** + * Tenancy Manager + * + * This is a manager and factory, responsible for creating and storing + * implementations of {@see \Sprout\Contracts\Tenancy}. + * * @extends \Sprout\Support\BaseFactory<\Sprout\Contracts\Tenancy> */ final class TenancyManager extends BaseFactory @@ -17,6 +22,12 @@ final class TenancyManager extends BaseFactory */ private ProviderManager $providerManager; + /** + * Create a new instance + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Sprout\Managers\ProviderManager $providerManager + */ public function __construct(Application $app, ProviderManager $providerManager) { parent::__construct($app); @@ -47,6 +58,8 @@ protected function getConfigKey(string $name): string } /** + * Create the default implementation + * * @param array $config * @param string $name * diff --git a/src/Overrides/AuthOverride.php b/src/Overrides/AuthOverride.php new file mode 100644 index 0000000..ab2cb6a --- /dev/null +++ b/src/Overrides/AuthOverride.php @@ -0,0 +1,85 @@ +authManager = $authManager; + } + + /** + * Set up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when a new tenant is marked as the current tenant. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @return void + */ + public function setup(Tenancy $tenancy, Tenant $tenant): void + { + $this->forgetGuards(); + } + + /** + * Clean up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when the current tenant is unset, either to be replaced + * by another tenant, or none. + * + * It will be called before {@see self::setup()}, but only if the previous + * tenant was not null. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @return void + */ + public function cleanup(Tenancy $tenancy, Tenant $tenant): void + { + $this->forgetGuards(); + } + + /** + * Forget all resolved guards + * + * @return void + */ + private function forgetGuards(): void + { + if ($this->authManager->hasResolvedGuards()) { + $this->authManager->forgetGuards(); + } + } +} diff --git a/src/Overrides/CacheOverride.php b/src/Overrides/CacheOverride.php index 3c80eea..d4629dd 100644 --- a/src/Overrides/CacheOverride.php +++ b/src/Overrides/CacheOverride.php @@ -17,13 +17,25 @@ use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +use Sprout\Exceptions\MisconfigurationException; use Sprout\Exceptions\TenantMissing; use Sprout\Sprout; -/** @codeCoverageIgnore */ +/** + * Cache Override + * + * This class provides the override/multitenancy extension/features for Laravels + * cache service. + * + * @package Overrides + * + * @codeCoverageIgnore + */ final class CacheOverride implements BootableServiceOverride { /** + * Cache stores that can be purged + * * @var list */ private static array $purgableStores = []; @@ -62,7 +74,7 @@ function (Application $app, array $config) use ($sprout, $cacheManager) { $tenant = $tenancy->tenant(); if (! isset($config['override'])) { - throw new RuntimeException('No cache store provided to override'); + throw MisconfigurationException::missingConfig('override', self::class, 'override'); } /** @var array $storeConfig */ @@ -94,7 +106,7 @@ function (Application $app, array $config) use ($sprout, $cacheManager) { 'memcached' => $this->createTenantedMemcachedStore($prefix, $storeConfig), 'redis' => $this->createTenantedRedisStore($prefix, $storeConfig), 'database' => $this->createTenantedDatabaseStore($prefix, $storeConfig), - default => throw new RuntimeException('Unsupported cache driver'), + default => throw MisconfigurationException::invalidConfig('driver', 'override', CacheOverride::class) }, array_merge($config, $storeConfig)); } @@ -102,6 +114,8 @@ function (Application $app, array $config) use ($sprout, $cacheManager) { } /** + * Create a memcache cache store that's tenanted + * * @param string $prefix * @param array $config * @@ -122,6 +136,8 @@ private function createTenantedMemcachedStore(string $prefix, array $config): Me } /** + * Create a Redis cache store that's tenanted + * * @param string $prefix * @param array $config * @@ -138,6 +154,8 @@ private function createTenantedRedisStore(string $prefix, array $config): RedisS } /** + * Create a database cache store that's tenanted + * * @param string $prefix * @param array $config * diff --git a/src/Overrides/CookieOverride.php b/src/Overrides/CookieOverride.php index 4971ac4..5949c8a 100644 --- a/src/Overrides/CookieOverride.php +++ b/src/Overrides/CookieOverride.php @@ -4,41 +4,22 @@ namespace Sprout\Overrides; use Illuminate\Cookie\CookieJar; +use Sprout\Concerns\OverridesCookieSettings; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +/** + * Cookie Override + * + * This class provides the override/multitenancy extension/features for Laravels + * cookie service. + * + * @package Overrides + */ final class CookieOverride implements ServiceOverride { - private static ?string $path = null; - - private static ?string $domain = null; - - private static ?bool $secure = null; - - private static ?string $sameSite = null; - - public static function setDomain(?string $domain): void - { - self::$domain = $domain; - } - - public static function setPath(?string $path): void - { - self::$path = $path; - } - - // @codeCoverageIgnoreStart - public static function setSameSite(?string $sameSite): void - { - self::$sameSite = $sameSite; - } - - public static function setSecure(?bool $secure): void - { - self::$secure = $secure; - } - // @codeCoverageIgnoreEnd + use OverridesCookieSettings; /** * Set up the service override diff --git a/src/Overrides/JobOverride.php b/src/Overrides/JobOverride.php new file mode 100644 index 0000000..be05142 --- /dev/null +++ b/src/Overrides/JobOverride.php @@ -0,0 +1,81 @@ +listen(JobProcessing::class, SetCurrentTenantForJob::class); + } + + /** + * Set up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when a new tenant is marked as the current tenant. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @return void + */ + public function setup(Tenancy $tenancy, Tenant $tenant): void + { + // I am intentionally empty + } + + /** + * Clean up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when the current tenant is unset, either to be replaced + * by another tenant, or none. + * + * It will be called before {@see self::setup()}, but only if the previous + * tenant was not null. + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @return void + */ + public function cleanup(Tenancy $tenancy, Tenant $tenant): void + { + // I am intentionally empty + } +} diff --git a/src/Overrides/Session/DatabaseSessionHandler.php b/src/Overrides/Session/DatabaseSessionHandler.php index 9da1c90..d8e9f42 100644 --- a/src/Overrides/Session/DatabaseSessionHandler.php +++ b/src/Overrides/Session/DatabaseSessionHandler.php @@ -6,17 +6,35 @@ use Illuminate\Database\Query\Builder; use Illuminate\Session\DatabaseSessionHandler as OriginalDatabaseSessionHandler; use RuntimeException; +use Sprout\Exceptions\TenancyMissing; use Sprout\Exceptions\TenantMissing; use function Sprout\sprout; +/** + * Database Session Handler + * + * This is a database session driver that wraps the default + * {@see \Illuminate\Session\DatabaseSessionHandler} and adds a where clause + * to the query to ensure sessions are tenanted. + * + * @package Overrides + */ class DatabaseSessionHandler extends OriginalDatabaseSessionHandler { + /** + * Get a fresh query builder instance for the table. + * + * @return \Illuminate\Database\Query\Builder + * + * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissing + */ protected function getQuery(): Builder { $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw new RuntimeException('No current tenancy'); + throw TenancyMissing::make(); } if ($tenancy->check() === false) { diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index 5024e57..e2620e1 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -8,50 +8,42 @@ use Illuminate\Session\FileSessionHandler; use Illuminate\Session\SessionManager; use RuntimeException; +use Sprout\Concerns\OverridesCookieSettings; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantHasResources; +use Sprout\Exceptions\MisconfigurationException; +use Sprout\Exceptions\TenancyMissing; use Sprout\Exceptions\TenantMissing; use Sprout\Overrides\Session\DatabaseSessionHandler; use Sprout\Sprout; use function Sprout\sprout; -/** @codeCoverageIgnore */ +/** + * Session Override + * + * This class provides the override/multitenancy extension/features for Laravels + * session service. + * + * @package Overrides + * + * @codeCoverageIgnore + */ final class SessionOverride implements BootableServiceOverride { - private static ?string $path = null; - - private static ?string $domain = null; - - private static ?bool $secure = null; - - private static ?string $sameSite = null; + use OverridesCookieSettings; + /** + * @var bool + */ private static bool $overrideDatabase = true; - public static function setDomain(?string $domain): void - { - self::$domain = $domain; - } - - public static function setPath(?string $path): void - { - self::$path = $path; - } - - // @codeCoverageIgnoreStart - public static function setSameSite(?string $sameSite): void - { - self::$sameSite = $sameSite; - } - - public static function setSecure(?bool $secure): void - { - self::$secure = $secure; - } - // @codeCoverageIgnoreEnd - + /** + * Prevent this override from overriding the database driver + * + * @return void + */ public static function doNotOverrideDatabase(): void { self::$overrideDatabase = false; @@ -174,7 +166,7 @@ private static function createFilesDriver(): Closure $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw new RuntimeException('No current tenancy'); + throw TenancyMissing::make(); } // If there's no tenant, error out @@ -186,8 +178,7 @@ private static function createFilesDriver(): Closure // If the tenant isn't configured for resources, also error out if (! ($tenant instanceof TenantHasResources)) { - // TODO: Better exception - throw new RuntimeException('Current tenant isn\t configured for resources'); + throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); } $path .= $tenant->getTenantResourceKey(); diff --git a/src/Overrides/StorageOverride.php b/src/Overrides/StorageOverride.php index 725a52b..66f077b 100644 --- a/src/Overrides/StorageOverride.php +++ b/src/Overrides/StorageOverride.php @@ -12,9 +12,18 @@ use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantHasResources; +use Sprout\Exceptions\MisconfigurationException; use Sprout\Exceptions\TenantMissing; use Sprout\Sprout; +/** + * Storage Override + * + * This class provides the override/multitenancy extension/features for Laravels + * storage service. + * + * @package Overrides + */ final class StorageOverride implements BootableServiceOverride { /** @@ -86,6 +95,14 @@ public function cleanup(Tenancy $tenancy, Tenant $tenant): void } } + /** + * Create a driver creator + * + * @param \Sprout\Sprout $sprout + * @param \Illuminate\Filesystem\FilesystemManager $manager + * + * @return \Closure + */ private static function creator(Sprout $sprout, FilesystemManager $manager): Closure { return static function (Application $app, array $config) use ($sprout, $manager): Filesystem { @@ -100,8 +117,7 @@ private static function creator(Sprout $sprout, FilesystemManager $manager): Clo // If the tenant isn't configured for resources, also error out if (! ($tenant instanceof TenantHasResources)) { - // TODO: Better exception - throw new RuntimeException('Current tenant isn\t configured for resources'); + throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); } $tenantConfig = self::getTenantStorageConfig($manager, $tenant, $config); @@ -112,6 +128,8 @@ private static function creator(Sprout $sprout, FilesystemManager $manager): Clo } /** + * Tenantise the storage config + * * @param \Illuminate\Filesystem\FilesystemManager $manager * @param \Sprout\Contracts\TenantHasResources $tenant * @param array $config @@ -136,12 +154,22 @@ private static function getTenantStorageConfig(FilesystemManager $manager, Tenan return $tenantConfig; } + /** + * Create a storage prefix using the current tenant + * + * @param \Sprout\Contracts\TenantHasResources $tenant + * @param string $pathPrefix + * + * @return string + */ private static function createTenantedPrefix(TenantHasResources $tenant, string $pathPrefix): string { return str_replace('{tenant}', $tenant->getTenantResourceKey(), $pathPrefix); } /** + * Get the config of the disk being tenantised + * * @param array $config * * @return array diff --git a/src/Providers/DatabaseTenantProvider.php b/src/Providers/DatabaseTenantProvider.php index 5bc8024..5e9f46a 100644 --- a/src/Providers/DatabaseTenantProvider.php +++ b/src/Providers/DatabaseTenantProvider.php @@ -10,6 +10,13 @@ use Sprout\Support\GenericTenant; /** + * Database Tenant Provider + * + * This is an implementation of {@see \Sprout\Contracts\TenantProvider} that + * uses Laravels base query builder. + * + * @package Core + * * @template EntityClass of \Sprout\Contracts\Tenant * * @extends \Sprout\Support\BaseTenantProvider diff --git a/src/Sprout.php b/src/Sprout.php index 71ef740..41f6a3b 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -10,6 +10,13 @@ use Sprout\Managers\ProviderManager; use Sprout\Managers\TenancyManager; +/** + * Sprout + * + * This is the core Sprout class. + * + * @package Core + */ final class Sprout { /** @@ -27,17 +34,34 @@ final class Sprout */ private array $overrides = []; + /** + * Create a new instance + * + * @param \Illuminate\Contracts\Foundation\Application $app + */ public function __construct(Application $app) { $this->app = $app; } + /** + * Get a config item from the sprout config + * + * @param string $key + * @param mixed|null $default + * + * @return mixed + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function config(string $key, mixed $default = null): mixed { return $this->app->make('config')->get('sprout.' . $key, $default); } /** + * Set the current tenancy + * * @template TenantClass of \Sprout\Contracts\Tenant * * @param \Sprout\Contracts\Tenancy $tenancy @@ -51,12 +75,19 @@ public function setCurrentTenancy(Tenancy $tenancy): void } } + /** + * Check if there is a current tenancy + * + * @return bool + */ public function hasCurrentTenancy(): bool { return count($this->tenancies) > 0; } /** + * Get the current tenancy + * * @return \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant>|null */ public function getCurrentTenancy(): ?Tenancy @@ -69,6 +100,8 @@ public function getCurrentTenancy(): ?Tenancy } /** + * Get all the current tenancies + * * @return \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant>[] */ public function getAllCurrentTenancies(): array @@ -76,31 +109,73 @@ public function getAllCurrentTenancies(): array return $this->tenancies; } + /** + * Should Sprout listen for the routing event + * + * @return bool + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function shouldListenForRouting(): bool { return (bool)$this->config('listen_for_routing', true); } + /** + * Get the identity resolver manager + * + * @return \Sprout\Managers\IdentityResolverManager + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function resolvers(): IdentityResolverManager { return $this->app->make(IdentityResolverManager::class); } + /** + * Get the tenant providers manager + * + * @return \Sprout\Managers\ProviderManager + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function providers(): ProviderManager { return $this->app->make(ProviderManager::class); } + /** + * Get the tenancy manager + * + * @return \Sprout\Managers\TenancyManager + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function tenancies(): TenancyManager { return $this->app->make(TenancyManager::class); } + /** + * Is an override enabled + * + * @param string $class + * + * @return bool + */ public function hasOverride(string $class): bool { return isset($this->overrides[$class]); } + /** + * Add an override + * + * @param \Sprout\Contracts\ServiceOverride $override + * + * @return $this + */ public function addOverride(ServiceOverride $override): self { $this->overrides[$override::class] = $override; @@ -109,6 +184,8 @@ public function addOverride(ServiceOverride $override): self } /** + * Get all overrides + * * @return array, \Sprout\Contracts\ServiceOverride> */ public function getOverrides(): array diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index be7c73a..ef00f74 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -4,7 +4,6 @@ namespace Sprout; use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Queue\Events\JobProcessing; use Illuminate\Routing\Events\RouteMatched; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; @@ -15,29 +14,27 @@ use Sprout\Http\Middleware\TenantRoutes; use Sprout\Http\RouterMethods; use Sprout\Listeners\IdentifyTenantOnRouting; -use Sprout\Listeners\SetCurrentTenantForJob; use Sprout\Managers\IdentityResolverManager; use Sprout\Managers\ProviderManager; use Sprout\Managers\TenancyManager; +/** + * Sprout Service Provider + * + * @package Core + */ class SproutServiceProvider extends ServiceProvider { private Sprout $sprout; public function register(): void { - $this->handleCoreConfig(); $this->registerSprout(); $this->registerManagers(); $this->registerMiddleware(); $this->registerRouteMixin(); } - private function handleCoreConfig(): void - { - $this->mergeConfigFrom(__DIR__ . '/../resources/config/sprout.php', 'sprout'); - } - private function registerSprout(): void { $this->sprout = new Sprout($this->app); @@ -94,7 +91,10 @@ public function boot(): void private function publishConfig(): void { - $this->publishes([__DIR__ . '/../resources/config/multitenancy.php' => config_path('multitenancy.php')], ['config', 'sprout-config']); + $this->publishes([ + __DIR__ . '/../resources/config/sprout.php' => config_path('sprout.php'), + __DIR__ . '/../resources/config/multitenancy.php' => config_path('multitenancy.php'), + ], ['config', 'sprout-config']); } private function registerServiceOverrides(): void @@ -123,8 +123,6 @@ private function registerEventListeners(): void if ($this->sprout->shouldListenForRouting()) { $events->listen(RouteMatched::class, IdentifyTenantOnRouting::class); } - - $events->listen(JobProcessing::class, SetCurrentTenantForJob::class); } private function registerTenancyBootstrappers(): void diff --git a/src/Support/BaseFactory.php b/src/Support/BaseFactory.php index 5a28849..2d4035a 100644 --- a/src/Support/BaseFactory.php +++ b/src/Support/BaseFactory.php @@ -4,24 +4,44 @@ namespace Sprout\Support; use Illuminate\Contracts\Foundation\Application; -use InvalidArgumentException; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; -use RuntimeException; -use Sprout\Concerns\HasCustomCreators; +use Sprout\Exceptions\MisconfigurationException; /** + * Base Factory + * + * This is an abstract base factory used by Sprout internals. * * @template FactoryClass of object * * @package Core + * + * @internal */ abstract class BaseFactory { /** - * @use \Sprout\Concerns\HasCustomCreators + * Custom creators + * + * @var array + * + * @phpstan-var array, string): FactoryClass> */ - use HasCustomCreators; + protected static array $customCreators = []; + + /** + * Register a custom creator + * + * @param string $name + * @param \Closure $callback + * + * @phpstan-param \Closure(Application, array, string): FactoryClass $callback + * + * @return void + */ + public static function register(string $name, \Closure $callback): void + { + static::$customCreators[$name] = $callback; + } /** * The Laravel application @@ -69,11 +89,22 @@ abstract protected function getConfigKey(string $name): string; * Get the default name * * @return string + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function getDefaultName(): string { - /** @phpstan-ignore-next-line */ - return $this->app['config']->get('multitenancy.defaults.' . $this->getFactoryName()); + /** @var \Illuminate\Config\Repository $config */ + $config = app('config'); + + /** @var string|null $name */ + $name = $config->get('multitenancy.defaults.' . $this->getFactoryName()); + + if ($name === null) { + throw MisconfigurationException::noDefault($this->getFactoryName()); + } + + return $name; } /** @@ -82,14 +113,16 @@ protected function getDefaultName(): string * @param string $name * * @return array|null - * - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \Psr\Container\NotFoundExceptionInterface */ protected function getConfig(string $name): ?array { - /** @phpstan-ignore-next-line */ - return $this->app['config']->get($this->getConfigKey($name)); + /** @var \Illuminate\Config\Repository $repo */ + $repo = app('config'); + + /** @var array|null $config */ + $config = $repo->get($this->getConfigKey($name)); + + return $config; } /** @@ -101,12 +134,15 @@ protected function getConfig(string $name): ?array * @return object * * @phpstan-return FactoryClass + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function callCustomCreator(string $name, array $config): object { if (! isset(static::$customCreators[$name])) { - throw new InvalidArgumentException( - 'Custom creator [' . $name . '] does not exist' + throw MisconfigurationException::notFound( + 'custom creator', + $this->getFactoryName() . '::' . $name ); } @@ -123,24 +159,17 @@ protected function callCustomCreator(string $name, array $config): object * @return object * * @phpstan-return FactoryClass + * + * @throws \Sprout\Exceptions\MisconfigurationException */ protected function resolve(string $name): object { // We need config, even if it's empty - try { - $config = $this->getConfig($name); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - throw new RuntimeException( - 'Unable to load config for [' . $this->getFactoryName() . '::' . $name . ']', - previous: $e - ); - } + $config = $this->getConfig($name); // If there's no config, complain if ($config === null) { - throw new InvalidArgumentException( - 'Invalid [' . $this->getFactoryName() . '], no config found' - ); + throw MisconfigurationException::notFound('config', $this->getFactoryName() . '::' . $name); } // Ooo custom creation logic, let's use that @@ -165,9 +194,7 @@ protected function resolve(string $name): object } // There's no valid creator, so we'll complain - throw new InvalidArgumentException( - 'Unable to create [' . $this->getFactoryName() . '::' . $name . '], no valid creator found' - ); + throw MisconfigurationException::notFound('creator', $this->getFactoryName() . '::' . $name); } /** @@ -178,6 +205,8 @@ protected function resolve(string $name): object * @return object * * @phpstan-return FactoryClass + * + * @throws \Sprout\Exceptions\MisconfigurationException */ public function get(?string $name = null): object { diff --git a/src/Support/BaseIdentityResolver.php b/src/Support/BaseIdentityResolver.php index fb9be2b..a1e957d 100644 --- a/src/Support/BaseIdentityResolver.php +++ b/src/Support/BaseIdentityResolver.php @@ -8,6 +8,14 @@ use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +/** + * Base Identity Resolver + * + * This is an abstract {@see \Sprout\Contracts\IdentityResolver} to provide + * a shared implementation of common functionality. + * + * @package Core + */ abstract class BaseIdentityResolver implements IdentityResolver { /** @@ -21,6 +29,8 @@ abstract class BaseIdentityResolver implements IdentityResolver private array $hooks; /** + * Create a new instance + * * @param string $name * @param array<\Sprout\Support\ResolutionHook> $hooks */ diff --git a/src/Support/BaseTenantProvider.php b/src/Support/BaseTenantProvider.php index 8a128d7..4fcfaba 100644 --- a/src/Support/BaseTenantProvider.php +++ b/src/Support/BaseTenantProvider.php @@ -6,9 +6,16 @@ use Sprout\Contracts\TenantProvider; /** + * Base Tenant Provider + * + * This is an abstract {@see \Sprout\Contracts\TenantProvider} to provide + * a shared implementation of common functionality. + * * @template EntityClass of \Sprout\Contracts\Tenant * * @implements \Sprout\Contracts\TenantProvider + * + * @package Core */ abstract class BaseTenantProvider implements TenantProvider { diff --git a/src/Support/DefaultTenancy.php b/src/Support/DefaultTenancy.php index 92ad42b..f88b381 100644 --- a/src/Support/DefaultTenancy.php +++ b/src/Support/DefaultTenancy.php @@ -13,6 +13,13 @@ use Sprout\Events\TenantLoaded; /** + * Default Tenancy + * + * This is a default implementation of the {@see \Sprout\Contracts\Tenancy} + * interface. + * + * @package Core + * * @template TenantClass of \Sprout\Contracts\Tenant * * @implements \Sprout\Contracts\Tenancy @@ -49,6 +56,8 @@ final class DefaultTenancy implements Tenancy private ?ResolutionHook $hook = null; /** + * Create a new instance + * * @param string $name * @param \Sprout\Contracts\TenantProvider $provider * @param list $options diff --git a/src/Support/GenericTenant.php b/src/Support/GenericTenant.php index cab11f4..305f510 100644 --- a/src/Support/GenericTenant.php +++ b/src/Support/GenericTenant.php @@ -5,6 +5,15 @@ use Sprout\Contracts\Tenant; +/** + * Generic Tenant + * + * This is a default implementation of the {@see \Sprout\Contracts\Tenant} + * interface for the use with {@see \Sprout\Providers\DatabaseTenantProvider} + * as the tenant entity. + * + * @pacakge Core + */ class GenericTenant implements Tenant { /** diff --git a/src/Support/ResolutionHelper.php b/src/Support/ResolutionHelper.php index fc942d2..4953ff7 100644 --- a/src/Support/ResolutionHelper.php +++ b/src/Support/ResolutionHelper.php @@ -10,20 +10,40 @@ class ResolutionHelper { + /** + * @param array $options + * + * @return array + */ + public static function parseOptions(array $options): array + { + if (count($options) === 2) { + [$resolverName, $tenancyName] = $options; + } else if (count($options) === 1) { + [$resolverName] = $options; + $tenancyName = null; + } else { + $resolverName = $tenancyName = null; + } + + return [$resolverName, $tenancyName]; + } + /** * @param \Illuminate\Http\Request $request * @param \Sprout\Support\ResolutionHook $hook * @param string|null $resolverName * @param string|null $tenancyName + * @param bool $throw * * @return bool * - * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Sprout\Exceptions\NoTenantFound */ public static function handleResolution(Request $request, ResolutionHook $hook, ?string $resolverName = null, ?string $tenancyName = null, bool $throw = true): bool { - $sprout = app()->make(Sprout::class); + /** @var \Sprout\Sprout $sprout */ + $sprout = app(Sprout::class); $resolver = $sprout->resolvers()->get($resolverName); $tenancy = $sprout->tenancies()->get($tenancyName); diff --git a/src/Support/ResolutionHook.php b/src/Support/ResolutionHook.php index 9166bba..a5c0035 100644 --- a/src/Support/ResolutionHook.php +++ b/src/Support/ResolutionHook.php @@ -3,13 +3,33 @@ namespace Sprout\Support; +/** + * Resolution Hook + * + * This enum is used as a way of identifying the various points within the + * Laravel request lifecycle where tenants can be resolved. + * + * @package Core + */ enum ResolutionHook { + /** + * During the bootstrapping fo Laravel + */ case Bootstrapping; + /** + * During the booting of service providers + */ case Booting; + /** + * During the route resolution + */ case Routing; + /** + * During the middleware stack + */ case Middleware; } diff --git a/src/TenancyOptions.php b/src/TenancyOptions.php index 3c114a2..63103da 100644 --- a/src/TenancyOptions.php +++ b/src/TenancyOptions.php @@ -5,6 +5,13 @@ use Sprout\Contracts\Tenancy; +/** + * Tenancy Options + * + * This is a helper class for providing and check for tenancy options. + * + * @package Core + */ class TenancyOptions { /** @@ -27,16 +34,6 @@ public static function throwIfNotRelated(): string return 'tenant-relation.strict'; } - /** - * Make sure that queued jobs are aware of the current tenant - * - * @return string - */ - public static function makeJobsTenantAware(): string - { - return 'tenant-aware.jobs'; - } - /** * @param \Sprout\Contracts\Tenancy<*> $tenancy * @@ -56,14 +53,4 @@ public static function shouldThrowIfNotRelated(Tenancy $tenancy): bool { return $tenancy->hasOption(static::throwIfNotRelated()); } - - /** - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * - * @return bool - */ - public static function shouldJobsBeTenantAware(Tenancy $tenancy): bool - { - return $tenancy->hasOption(static::makeJobsTenantAware()); - } } diff --git a/src/helpers.php b/src/helpers.php index a382ad2..1091615 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -9,5 +9,5 @@ */ function sprout(): Sprout { - return app()->make(Sprout::class); + return app(Sprout::class); } diff --git a/tests/Database/Eloquent/TenantChildTest.php b/tests/Database/Eloquent/TenantChildTest.php index 9f1e7a3..f6d52f0 100644 --- a/tests/Database/Eloquent/TenantChildTest.php +++ b/tests/Database/Eloquent/TenantChildTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Attributes\Test; use RuntimeException; use Sprout\Database\Eloquent\Concerns\IsTenantChild; +use Sprout\Exceptions\TenantRelationException; use Workbench\App\Models\NoTenantRelationModel; use Workbench\App\Models\TenantChild; use Workbench\App\Models\TenantChildren; @@ -52,8 +53,8 @@ public function throwsAnExceptionIfItCantFindTheTenantRelation(): void { $model = new NoTenantRelationModel(); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('No tenant relation found in model [' . NoTenantRelationModel::class . ']'); + $this->expectException(TenantRelationException::class); + $this->expectExceptionMessage('Cannot find tenant relation for model [' . NoTenantRelationModel::class . ']'); $model->getTenantRelationName(); } @@ -63,8 +64,8 @@ public function throwsAnExceptionIfThereAreMultipleTenantRelations(): void { $model = new TooManyTenantRelationModel(); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Models can only have one tenant relation, [' . TooManyTenantRelationModel::class . '] has 2'); + $this->expectException(TenantRelationException::class); + $this->expectExceptionMessage('Expected one tenant relation, found 2 in model [' . TooManyTenantRelationModel::class . ']'); $model->getTenantRelationName(); } diff --git a/tests/Listeners/SetCurrentTenantForJobTest.php b/tests/Listeners/SetCurrentTenantForJobTest.php index 04543e0..0d33a77 100644 --- a/tests/Listeners/SetCurrentTenantForJobTest.php +++ b/tests/Listeners/SetCurrentTenantForJobTest.php @@ -5,13 +5,19 @@ use Illuminate\Config\Repository; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Queue\QueueManager; use Illuminate\Support\Facades\Context; +use Orchestra\Testbench\Attributes\DefineEnvironment; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Sprout\Managers\TenancyManager; +use Sprout\Overrides\AuthOverride; +use Sprout\Overrides\CacheOverride; +use Sprout\Overrides\CookieOverride; +use Sprout\Overrides\JobOverride; +use Sprout\Overrides\SessionOverride; +use Sprout\Overrides\StorageOverride; use Sprout\TenancyOptions; use Workbench\App\Jobs\TestTenantJob; use Workbench\App\Models\TenantModel; @@ -30,12 +36,24 @@ protected function defineEnvironment($app): void }); } - #[Test] + protected function noJobOverride($app): void + { + tap($app['config'], static function (Repository $config) { + $config->set('sprout.services', [ + StorageOverride::class, + CacheOverride::class, + AuthOverride::class, + CookieOverride::class, + SessionOverride::class, + ]); + }); + } + + #[Test, DefineEnvironment('noJobOverride')] public function doesNotSetCurrentTenantForJobWithoutOption(): void { /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ $tenancy = app(TenancyManager::class)->get(); - $tenancy->removeOption(TenancyOptions::makeJobsTenantAware()); $this->assertFalse($tenancy->check()); @@ -56,7 +74,6 @@ public function setsCurrentTenantForJobWithOption(): void { /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ $tenancy = app(TenancyManager::class)->get(); - $tenancy->addOption(TenancyOptions::makeJobsTenantAware()); $this->assertFalse($tenancy->check()); diff --git a/tests/Overrides/CookieOverrideTest.php b/tests/Overrides/CookieOverrideTest.php index 42d5415..e69901c 100644 --- a/tests/Overrides/CookieOverrideTest.php +++ b/tests/Overrides/CookieOverrideTest.php @@ -14,7 +14,10 @@ use PHPUnit\Framework\Attributes\Test; use Sprout\Attributes\CurrentTenant; use Sprout\Contracts\Tenant; +use Sprout\Overrides\AuthOverride; use Sprout\Overrides\CacheOverride; +use Sprout\Overrides\CookieOverride; +use Sprout\Overrides\JobOverride; use Sprout\Overrides\SessionOverride; use Sprout\Overrides\StorageOverride; use Workbench\App\Models\TenantModel; @@ -38,8 +41,10 @@ protected function noCookieOverride($app): void { tap($app['config'], static function (Repository $config) { $config->set('sprout.services', [ - CacheOverride::class, StorageOverride::class, + JobOverride::class, + CacheOverride::class, + AuthOverride::class, SessionOverride::class, ]); }); diff --git a/tests/Overrides/StorageOverrideTest.php b/tests/Overrides/StorageOverrideTest.php index 8a6a2ac..b7acb62 100644 --- a/tests/Overrides/StorageOverrideTest.php +++ b/tests/Overrides/StorageOverrideTest.php @@ -12,11 +12,13 @@ use Orchestra\Testbench\TestCase; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use RuntimeException; +use Sprout\Exceptions\MisconfigurationException; use Sprout\Exceptions\TenantMissing; use Sprout\Managers\TenancyManager; +use Sprout\Overrides\AuthOverride; use Sprout\Overrides\CacheOverride; use Sprout\Overrides\CookieOverride; +use Sprout\Overrides\JobOverride; use Sprout\Overrides\SessionOverride; use Workbench\App\Models\NoResourcesTenantModel; use Workbench\App\Models\TenantModel; @@ -50,7 +52,9 @@ protected function noStorageOverride($app): void { tap($app['config'], static function (Repository $config) { $config->set('sprout.services', [ + JobOverride::class, CacheOverride::class, + AuthOverride::class, CookieOverride::class, SessionOverride::class, ]); @@ -97,8 +101,8 @@ public function throwsExceptionIfThereIsNoTenant(): void #[Test, DefineEnvironment('createTenantDisk')] public function throwsExceptionIfTheTenantDoesNotHaveResources(): void { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Current tenant isn\t configured for resources'); + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The current tenant [' . NoResourcesTenantModel::class . '] is not configured correctly for resources'); config()->set('multitenancy.providers.tenants.model', NoResourcesTenantModel::class); diff --git a/tests/Resolvers/HeaderResolverTest.php b/tests/Resolvers/HeaderResolverTest.php index 17cc596..8b9e420 100644 --- a/tests/Resolvers/HeaderResolverTest.php +++ b/tests/Resolvers/HeaderResolverTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Attributes\Test; use Sprout\Attributes\CurrentTenant; use Sprout\Contracts\Tenant; +use Sprout\Http\Middleware\AddTenantHeaderToResponse; use Workbench\App\Models\TenantModel; class HeaderResolverTest extends TestCase @@ -66,4 +67,13 @@ public function throwsExceptionWithoutHeader(): void $result->assertInternalServerError(); } + + #[Test] + public function addTenantHeaderQueueingMiddleware(): void + { + $route = app(Router::class)->getRoutes()->getByName('header.route'); + + $this->assertNotNull($route); + $this->assertContains(AddTenantHeaderToResponse::class . ':header,tenants', $route->middleware()); + } } diff --git a/tests/Resolvers/SessionResolverTest.php b/tests/Resolvers/SessionResolverTest.php index 612c5f1..4081da1 100644 --- a/tests/Resolvers/SessionResolverTest.php +++ b/tests/Resolvers/SessionResolverTest.php @@ -13,8 +13,11 @@ use PHPUnit\Framework\Attributes\Test; use Sprout\Attributes\CurrentTenant; use Sprout\Contracts\Tenant; +use Sprout\Overrides\AuthOverride; use Sprout\Overrides\CacheOverride; use Sprout\Overrides\CookieOverride; +use Sprout\Overrides\JobOverride; +use Sprout\Overrides\SessionOverride; use Sprout\Overrides\StorageOverride; use Workbench\App\Models\TenantModel; @@ -36,7 +39,9 @@ protected function defineEnvironment($app): void ]); $config->set('sprout.services', [ StorageOverride::class, + JobOverride::class, CacheOverride::class, + AuthOverride::class, CookieOverride::class, ]); }); diff --git a/tests/TenancyOptionsTest.php b/tests/TenancyOptionsTest.php index a451bd3..9e17d48 100644 --- a/tests/TenancyOptionsTest.php +++ b/tests/TenancyOptionsTest.php @@ -39,12 +39,6 @@ public function throwIfNotRelatedOption(): void $this->assertSame('tenant-relation.strict', TenancyOptions::throwIfNotRelated()); } - #[Test] - public function makeJobsTenantAwareOption(): void - { - $this->assertSame('tenant-aware.jobs', TenancyOptions::makeJobsTenantAware()); - } - #[Test] public function correctlyReportsHydrateTenantRelationOptionPresence(): void { @@ -70,17 +64,4 @@ public function correctlyReportsThrowIfNotRelatedOptionPresence(): void $this->assertTrue(TenancyOptions::shouldThrowIfNotRelated($tenancy)); } - - #[Test] - public function correctlyReportsMakeJobsTenantAwareOptionPresence(): void - { - $tenancy = app(TenancyManager::class)->get('tenants'); - $tenancy->removeOption(TenancyOptions::makeJobsTenantAware()); - - $this->assertFalse(TenancyOptions::shouldJobsBeTenantAware($tenancy)); - - $tenancy->addOption(TenancyOptions::makeJobsTenantAware()); - - $this->assertTrue(TenancyOptions::shouldJobsBeTenantAware($tenancy)); - } }