diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index a3626225..62ab8d0a 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -144,16 +144,8 @@ class TenancyServiceProvider extends ServiceProvider protected function makeTenancyMiddlewareHighestPriority() { - $tenancyMiddleware = [ - // Even higher priority than the initialization middleware - Middleware\PreventAccessFromCentralDomains::class, - - Middleware\InitializeTenancyByDomain::class, - Middleware\InitializeTenancyBySubdomain::class, - Middleware\InitializeTenancyByDomainOrSubdomain::class, - Middleware\InitializeTenancyByPath::class, - Middleware\InitializeTenancyByRequestData::class, - ]; + // Even higher priority than the initialization middleware + $tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware')); foreach (array_reverse($tenancyMiddleware) as $middleware) { $this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware); diff --git a/assets/config.php b/assets/config.php index 2827532a..6130bade 100644 --- a/assets/config.php +++ b/assets/config.php @@ -4,6 +4,8 @@ declare(strict_types=1); use Stancl\Tenancy\Database\Models\Domain; use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Middleware; +use Stancl\Tenancy\Resolvers; return [ 'tenant_model' => Tenant::class, @@ -21,6 +23,56 @@ return [ 'localhost', ], + 'identification' => [ + /** + * The default middleware used for tenant identification. + * + * If you use multiple forms of identification, you can set this to the "main" approach you use. + */ + 'default_middleware' => Middleware\InitializeTenancyByDomain::class,// todo@identification add this to a 'tenancy' mw group + + /** + * All of the identification middleware used by the package. + * + * If you write your own, make sure to add them to this array. + */ + 'middleware' => [ + Middleware\InitializeTenancyByDomain::class, + Middleware\InitializeTenancyBySubdomain::class, + Middleware\InitializeTenancyByDomainOrSubdomain::class, + Middleware\InitializeTenancyByPath::class, + Middleware\InitializeTenancyByRequestData::class, + ], + + /** + * Tenant resolvers used by the package. + * + * Resolvers which implement the CachedTenantResolver contract have options for configuring the caching details. + * If you add your own resolvers, do not add the 'cache' key unless your resolver is based on CachedTenantResolver. + */ + 'resolvers' => [ + Resolvers\DomainTenantResolver::class => [ + 'cache' => false, + 'cache_ttl' => 3600, // seconds + 'cache_store' => null, // default + ], + Resolvers\PathTenantResolver::class => [ + 'tenant_parameter_name' => 'tenant', + + 'cache' => false, + 'cache_ttl' => 3600, // seconds + 'cache_store' => null, // default + ], + Resolvers\RequestDataTenantResolver::class => [ + 'cache' => false, + 'cache_ttl' => 3600, // seconds + 'cache_store' => null, // default + ], + ], + + // todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware() + ], + /** * Tenancy bootstrappers are executed when tenancy is initialized. * Their responsibility is making Laravel features tenant-aware. diff --git a/assets/routes.php b/assets/routes.php index 9223c099..a27f782d 100644 --- a/assets/routes.php +++ b/assets/routes.php @@ -3,7 +3,8 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; +use Stancl\Tenancy\Controllers\TenantAssetController; -Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') +Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset']) ->where('path', '(.*)') ->name('stancl.tenancy.asset'); diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetController.php similarity index 65% rename from src/Controllers/TenantAssetsController.php rename to src/Controllers/TenantAssetController.php index 615f8054..7a95dffe 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetController.php @@ -4,18 +4,16 @@ declare(strict_types=1); namespace Stancl\Tenancy\Controllers; -use Closure; use Illuminate\Routing\Controller; +use Stancl\Tenancy\Tenancy; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Throwable; -class TenantAssetsController extends Controller // todo rename this to TenantAssetController & update references in docs +class TenantAssetController extends Controller // todo@docs this was renamed from TenantAssetsController { - public static string|array|Closure $tenancyMiddleware = \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class; - public function __construct() { - $this->middleware(static::$tenancyMiddleware); + $this->middleware(Tenancy::defaultMiddleware()); } /** diff --git a/src/Database/Concerns/InvalidatesResolverCache.php b/src/Database/Concerns/InvalidatesResolverCache.php index fee3a076..21894f41 100644 --- a/src/Database/Concerns/InvalidatesResolverCache.php +++ b/src/Database/Concerns/InvalidatesResolverCache.php @@ -5,22 +5,15 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Tenant; -use Stancl\Tenancy\Resolvers; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; +use Stancl\Tenancy\Tenancy; trait InvalidatesResolverCache { - /** @var array> */ - public static $resolvers = [ // todo@deprecated, move this to a config key? related to a todo in InvalidatesTenantsResolverCache - Resolvers\DomainTenantResolver::class, - Resolvers\PathTenantResolver::class, - Resolvers\RequestDataTenantResolver::class, - ]; - public static function bootInvalidatesResolverCache(): void { static::saved(function (Tenant $tenant) { - foreach (static::$resolvers as $resolver) { + foreach (Tenancy::cachedResolvers() as $resolver) { /** @var CachedTenantResolver $resolver */ $resolver = app($resolver); diff --git a/src/Database/Concerns/InvalidatesTenantsResolverCache.php b/src/Database/Concerns/InvalidatesTenantsResolverCache.php index aa7fac4b..d954567f 100644 --- a/src/Database/Concerns/InvalidatesTenantsResolverCache.php +++ b/src/Database/Concerns/InvalidatesTenantsResolverCache.php @@ -5,25 +5,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Illuminate\Database\Eloquent\Model; -use Stancl\Tenancy\Resolvers; use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver; +use Stancl\Tenancy\Tenancy; /** * Meant to be used on models that belong to tenants. */ trait InvalidatesTenantsResolverCache { - /** @var array> */ - public static array $resolvers = [ // todo single source of truth for this here and in InvalidatesResolverCache - Resolvers\DomainTenantResolver::class, - Resolvers\PathTenantResolver::class, - Resolvers\RequestDataTenantResolver::class, - ]; - public static function bootInvalidatesTenantsResolverCache(): void { static::saved(function (Model $model) { - foreach (static::$resolvers as $resolver) { + foreach (Tenancy::cachedResolvers() as $resolver) { /** @var CachedTenantResolver $resolver */ $resolver = app($resolver); diff --git a/src/Exceptions/RouteIsMissingTenantParameterException.php b/src/Exceptions/RouteIsMissingTenantParameterException.php index b979c819..afe56ea7 100644 --- a/src/Exceptions/RouteIsMissingTenantParameterException.php +++ b/src/Exceptions/RouteIsMissingTenantParameterException.php @@ -11,7 +11,7 @@ class RouteIsMissingTenantParameterException extends Exception { public function __construct() { - $parameter = PathTenantResolver::$tenantParameterName; + $parameter = PathTenantResolver::tenantParameterName(); parent::__construct("The route's first argument is not the tenant id (configured paramter name: $parameter)."); } diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index d4733f62..3e484f87 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -34,14 +34,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware // Only initialize tenancy if tenant is the first parameter // We don't want to initialize tenancy if the tenant is // simply injected into some route controller action. - if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) { - // Set tenant as a default parameter for the URLs in the current request - Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) { - /** @var Tenant $tenant */ - $tenant = $event->tenancy->tenant; - - URL::defaults([PathTenantResolver::$tenantParameterName => $tenant->getTenantKey()]); - }); + if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) { + $this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized(); return $this->initializeTenancy( $request, @@ -54,4 +48,16 @@ class InitializeTenancyByPath extends IdentificationMiddleware return $next($request); } + + protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void + { + Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) { + /** @var Tenant $tenant */ + $tenant = $event->tenancy->tenant; + + URL::defaults([ + PathTenantResolver::tenantParameterName() => $tenant->getTenantKey(), + ]); + }); + } } diff --git a/src/Resolvers/Contracts/CachedTenantResolver.php b/src/Resolvers/Contracts/CachedTenantResolver.php index d4d5ba6e..b6a4b15c 100644 --- a/src/Resolvers/Contracts/CachedTenantResolver.php +++ b/src/Resolvers/Contracts/CachedTenantResolver.php @@ -11,23 +11,17 @@ use Stancl\Tenancy\Contracts\TenantResolver; abstract class CachedTenantResolver implements TenantResolver { - public static bool $shouldCache = false; // todo docblocks for these - - public static int $cacheTTL = 3600; // seconds - - public static string|null $cacheStore = null; // default - /** @var Repository */ protected $cache; public function __construct(Factory $cache) { - $this->cache = $cache->store(static::$cacheStore); + $this->cache = $cache->store(static::cacheStore()); } public function resolve(mixed ...$args): Tenant { - if (! static::$shouldCache) { + if (! static::shouldCache()) { return $this->resolveWithoutCache(...$args); } @@ -42,14 +36,14 @@ abstract class CachedTenantResolver implements TenantResolver } $tenant = $this->resolveWithoutCache(...$args); - $this->cache->put($key, $tenant, static::$cacheTTL); + $this->cache->put($key, $tenant, static::cacheTTL()); return $tenant; } public function invalidateCache(Tenant $tenant): void { - if (! static::$shouldCache) { + if (! static::shouldCache()) { return; } @@ -75,4 +69,19 @@ abstract class CachedTenantResolver implements TenantResolver * @return array[] */ abstract public function getArgsForTenant(Tenant $tenant): array; + + public static function shouldCache(): bool + { + return config('tenancy.identification.resolvers.' . static::class . '.cache') ?? false; + } + + public static function cacheTTL(): int + { + return config('tenancy.identification.resolvers.' . static::class . '.cache_ttl') ?? 3600; + } + + public static function cacheStore(): string|null + { + return config('tenancy.identification.resolvers.' . static::class . '.cache_store'); + } } diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index d2970bb5..cf88f579 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -14,12 +14,6 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver /** The model representing the domain that the tenant was identified on. */ public static Domain $currentDomain; // todo |null? - public static bool $shouldCache = false; - - public static int $cacheTTL = 3600; // seconds - - public static string|null $cacheStore = null; // default - public function resolveWithoutCache(mixed ...$args): Tenant { $domain = $args[0]; diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index c98ac37e..1359e9c1 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -10,21 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; class PathTenantResolver extends Contracts\CachedTenantResolver { - public static string $tenantParameterName = 'tenant'; - - public static bool $shouldCache = false; - - public static int $cacheTTL = 3600; // seconds - - public static string|null $cacheStore = null; // default - public function resolveWithoutCache(mixed ...$args): Tenant { /** @var Route $route */ $route = $args[0]; - if ($id = (string) $route->parameter(static::$tenantParameterName)) { - $route->forgetParameter(static::$tenantParameterName); + if ($id = (string) $route->parameter(static::tenantParameterName())) { + $route->forgetParameter(static::tenantParameterName()); if ($tenant = tenancy()->find($id)) { return $tenant; @@ -40,4 +32,9 @@ class PathTenantResolver extends Contracts\CachedTenantResolver [$tenant->getTenantKey()], ]; } + + public static function tenantParameterName(): string + { + return config('tenancy.identification.resolvers.' . static::class . '.tenant_parameter_name') ?? 'tenant'; + } } diff --git a/src/Tenancy.php b/src/Tenancy.php index 5fe6bd52..271d36b9 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -42,7 +42,7 @@ class Tenancy } } - // todo0 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property + // todo1 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) { return; } @@ -157,7 +157,7 @@ class Tenancy $tenants = is_string($tenants) ? [$tenants] : $tenants; // Use all tenants if $tenants is falsey - $tenants = $tenants ?: $this->model()->cursor(); // todo0 phpstan thinks this isn't needed, but tests fail without it + $tenants = $tenants ?: $this->model()->cursor(); // todo1 phpstan thinks this isn't needed, but tests fail without it $originalTenant = $this->tenant; @@ -177,4 +177,41 @@ class Tenancy $this->end(); } } + + /** + * Cached tenant resolvers used by the package. + * + * @return array> + */ + public static function cachedResolvers(): array + { + $resolvers = config('tenancy.identification.resolvers', []); + + $cachedResolvers = array_filter($resolvers, function (array $options) { + // Resolvers based on CachedTenantResolver have the 'cache' option in the resolver config + return isset($options['cache']); + }); + + return array_keys($cachedResolvers); + } + + /** + * Tenant identification middleware used by the package. + * + * @return array> + */ + public static function middleware(): array + { + return config('tenancy.identification.middleware', []); + } + + /** + * Default tenant identification middleware used by the package. + * + * @return class-string + */ + public static function defaultMiddleware(): string + { + return config('tenancy.identification.default_middleware', Middleware\InitializeTenancyByDomain::class); + } } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index d9556283..7e12a857 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -120,7 +120,7 @@ class TenancyServiceProvider extends ServiceProvider if ($event instanceof TenancyEvent) { match (tenancy()->logMode()) { LogMode::SILENT => tenancy()->logEvent($event), - LogMode::INSTANT => dump($event), // todo0 perhaps still log + LogMode::INSTANT => dump($event), // todo1 perhaps still log default => null, }; } diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index d71375be..fa624b04 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -6,9 +6,7 @@ use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; -afterEach(function () { - DomainTenantResolver::$shouldCache = false; -}); +// todo@v4 test this with other resolvers as well? test('tenants can be resolved using the cached resolver', function () { $tenant = Tenant::create(); @@ -27,14 +25,14 @@ test('the underlying resolver is not touched when using the cached resolver', fu DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = false; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => false]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); pest()->assertNotEmpty(DB::getQueryLog()); // not empty - DomainTenantResolver::$shouldCache = true; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); @@ -50,7 +48,7 @@ test('cache is invalidated when the tenant is updated', function () { DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = true; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); @@ -74,7 +72,7 @@ test('cache is invalidated when a tenants domain is changed', function () { DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = true; + config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => true]); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index 517fa396..32880c4f 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -10,8 +10,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; beforeEach(function () { - PathTenantResolver::$tenantParameterName = 'tenant'; - Route::group([ 'prefix' => '/{tenant}', 'middleware' => InitializeTenancyByPath::class, @@ -26,11 +24,6 @@ beforeEach(function () { }); }); -afterEach(function () { - // Global state cleanup - PathTenantResolver::$tenantParameterName = 'tenant'; -}); - test('tenant can be identified by path', function () { Tenant::create([ 'id' => 'acme', @@ -101,7 +94,7 @@ test('an exception is thrown when the routes first parameter is not tenant', fun }); test('tenant parameter name can be customized', function () { - PathTenantResolver::$tenantParameterName = 'team'; + config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.tenant_parameter_name' => 'team']); Route::group([ 'prefix' => '/{team}', diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index d43b7989..a1cd0f5b 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -6,10 +6,8 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Controllers\TenantAssetsController; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; -use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Tests\Etc\Tenant; @@ -21,13 +19,8 @@ beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); }); -afterEach(function () { - // Cleanup - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class; -}); - test('asset can be accessed using the url returned by the tenant asset helper', function () { - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]); $tenant = Tenant::create(); tenancy()->initialize($tenant); @@ -95,7 +88,7 @@ test('asset helper tenancy can be disabled', function () { }); test('test asset controller returns a 404 when no path is provided', function () { - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]); $tenant = Tenant::create(); diff --git a/tests/TestCase.php b/tests/TestCase.php index f7f8b9ad..1c0ceb83 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -103,7 +103,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '--realpath' => true, '--force' => true, ], - 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo0 change this to []? two tests in TenantDatabaseManagerTest are failing with that + 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that 'queue.connections.central' => [ 'driver' => 'sync', 'central' => true,