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));
- }
}