From f7d9f02fd405f0570a83c3922a8eaf87d2bfd43a Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 28 Aug 2023 13:17:17 +0200 Subject: [PATCH] Improve route cloning action (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow cloning routes when only kernel identification is used, explicitly enable specific cloning modes * Explicitly enable needed clone modes in tests, use "clone" instead of "reregister" * Fix code style (php-cs-fixer) * Use "cloning" instead of "re-registration" in UniversalRouteTest * Only clone routes using path identification * Revert clone mode changes * Fix code style (php-cs-fixer) * Update comment * Skip cloning 'stancl.tenancy.asset' by default * Decide which routes should get cloned in the filtering step, improve method organization * Return `RouteMode::UNIVERSAL` in getMiddlewareContext if route is universal * Give universal route the path ID MW so that it gets cloned * Fix code style (php-cs-fixer) * Simplify UsableWithEarlyIdentification code * Handle universal route mode in ForgetTenantParameter * Fix code style (php-cs-fixer) * Rename getMiddlewareContext to getRouteMode * Append '/' to the route prefix * Rename variable * Wrap part of condition in parentheses * Refresh name lookups after cloning routes * Test giving tenant flag to cloned routes * Add routeIsUniversal method * Correct ForgetTenantParameter condition * Improve tenant flag giving logic * Improve test name * Delete leftover testing code * Put part of condition into `()` * Improve CloneRoutesAsTenant code + comments * Extract route mode-related code into methods, refactor and improve code * Improve ForgetTenantParameter, test tenant parameter removing in universal routes * Fix code style (php-cs-fixer) * Fix test * Simplify adding tenant flag * Don't skip stancl.tenancy.asset route cloning * clean up comment * fix in_array() argument * Fix code style (php-cs-fixer) --------- Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- assets/TenancyServiceProvider.stub.php | 2 +- src/Actions/CloneRoutesAsTenant.php | 103 ++++++++++-------- src/Concerns/DealsWithEarlyIdentification.php | 70 ++++++++++-- .../UsableWithEarlyIdentification.php | 28 ++--- src/Listeners/ForgetTenantParameter.php | 11 +- src/Middleware/InitializeTenancyByPath.php | 6 +- .../PreventAccessFromUnwantedDomains.php | 7 +- tests/EarlyIdentificationTest.php | 22 +++- tests/UniversalRouteTest.php | 84 +++++++++----- 9 files changed, 222 insertions(+), 111 deletions(-) diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index eba11d52..7dee869d 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -191,7 +191,7 @@ class TenancyServiceProvider extends ServiceProvider * $route->middleware('tenant'); * }); * - * To see the default behavior of re-registering the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant. + * To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant. * @see CloneRoutesAsTenant */ diff --git a/src/Actions/CloneRoutesAsTenant.php b/src/Actions/CloneRoutesAsTenant.php index 9428db18..30313a40 100644 --- a/src/Actions/CloneRoutesAsTenant.php +++ b/src/Actions/CloneRoutesAsTenant.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Actions; use Closure; -use Illuminate\Config\Repository; use Illuminate\Routing\Route; use Illuminate\Routing\Router; use Illuminate\Support\Collection; @@ -42,30 +41,16 @@ class CloneRoutesAsTenant public function __construct( protected Router $router, - protected Repository $config, ) { } public function handle(): void { - $tenantParameterName = PathIdentificationManager::getTenantParameterName(); - $routePrefix = '/{' . $tenantParameterName . '}'; + $this->router + ->prefix('/{' . PathIdentificationManager::getTenantParameterName() . '}/') + ->group(fn () => $this->getRoutesToClone()->each(fn (Route $route) => $this->cloneRoute($route))); - /** @var Collection $routesToClone Only clone non-skipped routes without the tenant parameter. */ - $routesToClone = collect($this->router->getRoutes()->get())->filter(function (Route $route) use ($tenantParameterName) { - return ! (in_array($tenantParameterName, $route->parameterNames()) || in_array($route->getName(), $this->skippedRoutes)); - }); - - if ($this->config->get('tenancy.default_route_mode') !== RouteMode::UNIVERSAL) { - // Only clone routes with route-level path identification and universal routes - $routesToClone = $routesToClone->where(function (Route $route) { - $routeIsUniversal = tenancy()->routeHasMiddleware($route, 'universal'); - - return PathIdentificationManager::pathIdentificationOnRoute($route) || $routeIsUniversal; - }); - } - - $this->router->prefix($routePrefix)->group(fn () => $routesToClone->each(fn (Route $route) => $this->cloneRoute($route))); + $this->router->getRoutes()->refreshNameLookups(); } /** @@ -88,6 +73,56 @@ class CloneRoutesAsTenant return $this; } + protected function getRoutesToClone(): Collection + { + $tenantParameterName = PathIdentificationManager::getTenantParameterName(); + + /** + * Clone all routes that: + * - don't have the tenant parameter + * - aren't in the $skippedRoutes array + * - are using path identification (kernel or route-level). + * + * Non-universal cloned routes will only be available in the tenant context, + * universal routes will be available in both contexts. + */ + return collect($this->router->getRoutes()->get())->filter(function (Route $route) use ($tenantParameterName) { + if (in_array($tenantParameterName, $route->parameterNames(), true) || in_array($route->getName(), $this->skippedRoutes, true)) { + return false; + } + + $routeHasPathIdentificationMiddleware = PathIdentificationManager::pathIdentificationOnRoute($route); + $pathIdentificationMiddlewareInGlobalStack = PathIdentificationManager::pathIdentificationInGlobalStack(); + $routeHasNonPathIdentificationMiddleware = tenancy()->routeHasIdentificationMiddleware($route) && ! $routeHasPathIdentificationMiddleware; + + /** + * The route should get cloned if: + * - it has route-level path identification middleware, OR + * - it uses kernel path identification (it doesn't have any route-level identification middleware) and the route is tenant or universal. + * + * The route is considered tenant if: + * - it's flagged as tenant, OR + * - it's not flagged as tenant or universal, but it has the identification middleware + * + * The route is considered universal if it's flagged as universal, and it doesn't have the tenant flag + * (it's still considered universal if it has route-level path identification middleware + the universal flag). + * + * If the route isn't flagged, the context is determined using the default route mode. + */ + $pathIdentificationUsed = (! $routeHasNonPathIdentificationMiddleware) && + ($routeHasPathIdentificationMiddleware || $pathIdentificationMiddlewareInGlobalStack); + + $routeMode = tenancy()->getRouteMode($route); + $routeIsUniversalOrTenant = $routeMode === RouteMode::TENANT || $routeMode === RouteMode::UNIVERSAL; + + if ($pathIdentificationUsed && $routeIsUniversalOrTenant) { + return true; + } + + return false; + }); + } + /** * Clone a route using a callback specified in the $cloneRouteUsing property (using the cloneUsing method). * If there's no callback specified for the route, use the default way of cloning routes. @@ -104,33 +139,13 @@ class CloneRoutesAsTenant return; } - $routesAreUniversalByDefault = $this->config->get('tenancy.default_route_mode') === RouteMode::UNIVERSAL; - $routeHasPathIdentification = PathIdentificationManager::pathIdentificationOnRoute($route); - $pathIdentificationMiddlewareInGlobalStack = PathIdentificationManager::pathIdentificationInGlobalStack(); - $routeHasNonPathIdentificationMiddleware = tenancy()->routeHasIdentificationMiddleware($route) && ! $routeHasPathIdentification; + $newRoute = $this->createNewRoute($route); - // Determine if the passed route should get cloned - // The route should be cloned if it has path identification middleware - // Or if the route doesn't have identification middleware and path identification middleware - // Is not used globally or the routes are universal by default - $shouldCloneRoute = ! $routeHasNonPathIdentificationMiddleware && - ($routesAreUniversalByDefault || $routeHasPathIdentification || $pathIdentificationMiddlewareInGlobalStack); - - if ($shouldCloneRoute) { - $newRoute = $this->createNewRoute($route); - $routeConsideredUniversal = tenancy()->routeHasMiddleware($newRoute, 'universal') || $routesAreUniversalByDefault; - - if ($routeHasPathIdentification && ! $routeConsideredUniversal && ! tenancy()->routeHasMiddleware($newRoute, 'tenant')) { - // Skip adding tenant flag - // Non-universal routes with identification middleware are already considered tenant - // Also skip adding the flag if the route already has the flag - // So that the route only has the 'tenant' middleware group once - } else { - $newRoute->middleware('tenant'); - } - - $this->copyMiscRouteProperties($route, $newRoute); + if (! tenancy()->routeHasMiddleware($route, 'tenant')) { + $newRoute->middleware('tenant'); } + + $this->copyMiscRouteProperties($route, $newRoute); } protected function createNewRoute(Route $route): Route diff --git a/src/Concerns/DealsWithEarlyIdentification.php b/src/Concerns/DealsWithEarlyIdentification.php index 99993112..3c7328be 100644 --- a/src/Concerns/DealsWithEarlyIdentification.php +++ b/src/Concerns/DealsWithEarlyIdentification.php @@ -5,42 +5,61 @@ declare(strict_types=1); namespace Stancl\Tenancy\Concerns; use Closure; +use Illuminate\Contracts\Http\Kernel; use Illuminate\Http\Request; use Illuminate\Routing\Route; use Illuminate\Routing\Router; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Route as RouteFacade; -use Stancl\Tenancy\Enums\Context; use Stancl\Tenancy\Enums\RouteMode; // todo1 Name – maybe DealsWithMiddlewareContexts? trait DealsWithEarlyIdentification { /** - * Get route's middleware context (tenant or central). + * Get route's middleware context (tenant, central or universal). * The context is determined by the route's middleware. * * If the route has the 'central' middleware, the context is central. - * If the route has the 'tenant' middleware, or any tenancy identification middleware, the context is tenant. + * If the route has the 'tenant' middleware, or any tenancy identification middleware (and the route isn't flagged as universal), the context is tenant. * * If the route doesn't have any of the mentioned middleware, * the context is determined by the `tenancy.default_route_mode` config. */ - public static function getMiddlewareContext(Route $route): RouteMode + public static function getRouteMode(Route $route): RouteMode { if (static::routeHasMiddleware($route, 'central')) { return RouteMode::CENTRAL; } - $defaultRouteMode = config('tenancy.default_route_mode'); - $routeIsUniversal = $defaultRouteMode === RouteMode::UNIVERSAL || static::routeHasMiddleware($route, 'universal'); + $routeIsUniversal = static::routeIsUniversal($route); - // If a route has identification middleware AND the route isn't universal, don't consider the context tenant - if (static::routeHasMiddleware($route, 'tenant') || static::routeHasIdentificationMiddleware($route) && ! $routeIsUniversal) { + // If the route is flagged as tenant, consider it tenant + // If the route has an identification middleware and the route is not universal, consider it tenant + if ( + static::routeHasMiddleware($route, 'tenant') || + (static::routeHasIdentificationMiddleware($route) && ! $routeIsUniversal) + ) { return RouteMode::TENANT; } - return $defaultRouteMode; + // If the route is universal, you have to determine its actual context using + // The identification middleware's determineUniversalRouteContextFromRequest + if ($routeIsUniversal) { + return RouteMode::UNIVERSAL; + } + + return config('tenancy.default_route_mode'); + } + + public static function routeIsUniversal(Route $route): bool + { + $routeFlaggedAsTenantOrCentral = static::routeHasMiddleware($route, 'tenant') || static::routeHasMiddleware($route, 'central'); + $routeFlaggedAsUniversal = static::routeHasMiddleware($route, 'universal'); + $universalFlagUsedInGlobalStack = app(Kernel::class)->hasMiddleware('universal'); + $defaultRouteModeIsUniversal = config('tenancy.default_route_mode') === RouteMode::UNIVERSAL; + + return ! $routeFlaggedAsTenantOrCentral && ($routeFlaggedAsUniversal || $universalFlagUsedInGlobalStack || $defaultRouteModeIsUniversal); } /** @@ -93,6 +112,31 @@ trait DealsWithEarlyIdentification return in_array($middleware, static::getRouteMiddleware($route)); } + public function routeIdentificationMiddleware(Route $route): string|null + { + foreach (static::getRouteMiddleware($route) as $routeMiddleware) { + if (in_array($routeMiddleware, static::middleware())) { + return $routeMiddleware; + } + } + + return null; + } + + public static function kernelIdentificationMiddleware(): string|null + { + /** @var Kernel $kernel */ + $kernel = app(Kernel::class); + + foreach (static::middleware() as $identificationMiddleware) { + if ($kernel->hasMiddleware($identificationMiddleware)) { + return $identificationMiddleware; + } + } + + return null; + } + /** * Check if a route has identification middleware. */ @@ -107,6 +151,14 @@ trait DealsWithEarlyIdentification return false; } + /** + * Check if route uses kernel identification (identification middleare is in the global stack and the route doesn't have route-level identification middleware). + */ + public static function routeUsesKernelIdentification(Route $route): bool + { + return ! static::routeHasIdentificationMiddleware($route) && static::kernelIdentificationMiddleware(); + } + /** * Check if a route uses domain identification. */ diff --git a/src/Concerns/UsableWithEarlyIdentification.php b/src/Concerns/UsableWithEarlyIdentification.php index beb405d5..4b3cc8a2 100644 --- a/src/Concerns/UsableWithEarlyIdentification.php +++ b/src/Concerns/UsableWithEarlyIdentification.php @@ -29,21 +29,13 @@ trait UsableWithEarlyIdentification { /** * Skip middleware if the route is universal and uses path identification or if the route is universal and the context should be central. - * Universal routes using path identification should get re-registered using ReregisterRoutesAsTenant. + * Universal routes using path identification should get cloned using CloneRoutesAsTenant. * * @see \Stancl\Tenancy\Actions\CloneRoutesAsTenant */ protected function shouldBeSkipped(Route $route): bool { - $routeMiddleware = tenancy()->getRouteMiddleware($route); - $universalFlagUsed = in_array('universal', $routeMiddleware); - $defaultToUniversalRoutes = config('tenancy.default_route_mode') === RouteMode::UNIVERSAL; - - // Route is universal only if it doesn't have the central/tenant flag - $routeIsUniversal = ($universalFlagUsed || $defaultToUniversalRoutes) && - ! (in_array('central', $routeMiddleware) || in_array('tenant', $routeMiddleware)); - - if ($routeIsUniversal && $this instanceof IdentificationMiddleware) { + if (tenancy()->routeIsUniversal($route) && $this instanceof IdentificationMiddleware) { /** @phpstan-ignore-next-line */ throw_unless($this instanceof UsableWithUniversalRoutes, MiddlewareNotUsableWithUniversalRoutesException::class); @@ -71,10 +63,15 @@ trait UsableWithEarlyIdentification // Check if this is the identification middleware the route should be using // Route-level identification middleware is prioritized - $middlewareUsed = tenancy()->routeHasMiddleware($route, static::class) || ! tenancy()->routeHasIdentificationMiddleware($route) && static::inGlobalStack(); + $globalIdentificationUsed = ! tenancy()->routeHasIdentificationMiddleware($route) && static::inGlobalStack(); + $routeLevelIdentificationUsed = tenancy()->routeHasMiddleware($route, static::class); /** @var UsableWithUniversalRoutes $this */ - return $middlewareUsed && $this->requestHasTenant($request) ? Context::TENANT : Context::CENTRAL; + if (($globalIdentificationUsed || $routeLevelIdentificationUsed) && $this->requestHasTenant($request)) { + return Context::TENANT; + } + + return Context::CENTRAL; } protected function shouldIdentificationMiddlewareBeSkipped(Route $route): bool @@ -88,10 +85,9 @@ trait UsableWithEarlyIdentification if (! $request->attributes->get('_tenancy_kernel_identification_skipped')) { if ( // Skip identification if the current route is central - // The route is central if defaulting is set to central and the route isn't flagged as tenant or it doesn't have identification middleware - tenancy()->getMiddlewareContext($route) === RouteMode::CENTRAL - // Don't skip identification if the central route is considered universal - && (config('tenancy.default_route_mode') !== RouteMode::UNIVERSAL || ! tenancy()->routeHasMiddleware($route, 'universal')) + // The route is central if it's flagged as central + // Or if it isn't flagged and the default route mode is set to central + tenancy()->getRouteMode($route) === RouteMode::CENTRAL ) { return true; } diff --git a/src/Listeners/ForgetTenantParameter.php b/src/Listeners/ForgetTenantParameter.php index a4eb32cd..be8024ba 100644 --- a/src/Listeners/ForgetTenantParameter.php +++ b/src/Listeners/ForgetTenantParameter.php @@ -19,17 +19,16 @@ use Stancl\Tenancy\PathIdentificationManager; * We remove the {tenant} parameter from the hydrated route when * 1) the InitializeTenancyByPath middleware is in the global stack, AND * 2) the matched route does not have identification middleware (so that {tenant} isn't forgotten when using route-level identification), AND - * 3) the route has tenant middleware context (so that {tenant} doesn't get accidentally removed from central routes). + * 3) the route isn't in the central context (so that {tenant} doesn't get accidentally removed from central routes). */ class ForgetTenantParameter { public function handle(RouteMatched $event): void { - if ( - PathIdentificationManager::pathIdentificationInGlobalStack() && - ! tenancy()->routeHasIdentificationMiddleware($event->route) && - tenancy()->getMiddlewareContext($event->route) === RouteMode::TENANT - ) { + $kernelPathIdentificationUsed = PathIdentificationManager::pathIdentificationInGlobalStack() && ! tenancy()->routeHasIdentificationMiddleware($event->route); + $routeModeIsTenant = tenancy()->getRouteMode($event->route) === RouteMode::TENANT; + + if ($kernelPathIdentificationUsed && $routeModeIsTenant) { $event->route->forgetParameter(PathIdentificationManager::getTenantParameterName()); } } diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 2d6ffe57..60b5ab7e 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -76,15 +76,15 @@ class InitializeTenancyByPath extends IdentificationMiddleware implements Usable /** * Path identification request has a tenant if the middleware context is tenant. * - * With path identification, we can just check the MW context because we're re-registering the universal routes, + * With path identification, we can just check the MW context because we're cloning the universal routes, * and the routes are flagged with the 'tenant' MW group (= their MW context is tenant). * * With other identification middleware, we have to determine the context differently because we only have one * truly universal route available ('truly universal' because with path identification, applying 'universal' to a route just means that - * it should get re-registered, whereas with other ID MW, it means that the route you apply the 'universal' flag to will be accessible in both contexts). + * it should get cloned, whereas with other ID MW, it means that the route you apply the 'universal' flag to will be accessible in both contexts). */ public function requestHasTenant(Request $request): bool { - return tenancy()->getMiddlewareContext(tenancy()->getRoute($request)) === RouteMode::TENANT; + return tenancy()->getRouteMode(tenancy()->getRoute($request)) === RouteMode::TENANT; } } diff --git a/src/Middleware/PreventAccessFromUnwantedDomains.php b/src/Middleware/PreventAccessFromUnwantedDomains.php index 5e28b1c2..be505e06 100644 --- a/src/Middleware/PreventAccessFromUnwantedDomains.php +++ b/src/Middleware/PreventAccessFromUnwantedDomains.php @@ -33,9 +33,8 @@ class PreventAccessFromUnwantedDomains public function handle(Request $request, Closure $next): mixed { $route = tenancy()->getRoute($request); - $routeIsUniversal = tenancy()->routeHasMiddleware($route, 'universal') || config('tenancy.default_route_mode') === RouteMode::UNIVERSAL; - if ($this->shouldBeSkipped($route) || $routeIsUniversal) { + if ($this->shouldBeSkipped($route) || tenancy()->routeIsUniversal($route)) { return $next($request); } @@ -52,13 +51,13 @@ class PreventAccessFromUnwantedDomains protected function accessingTenantRouteFromCentralDomain(Request $request, Route $route): bool { - return tenancy()->getMiddlewareContext($route) === RouteMode::TENANT // Current route's middleware context is tenant + return tenancy()->getRouteMode($route) === RouteMode::TENANT // Current route's middleware context is tenant && $this->isCentralDomain($request); // The request comes from a domain that IS present in the configured `tenancy.central_domains` } protected function accessingCentralRouteFromTenantDomain(Request $request, Route $route): bool { - return tenancy()->getMiddlewareContext($route) === RouteMode::CENTRAL // Current route's middleware context is central + return tenancy()->getRouteMode($route) === RouteMode::CENTRAL // Current route's middleware context is central && ! $this->isCentralDomain($request); // The request comes from a domain that ISN'T present in the configured `tenancy.central_domains` } diff --git a/tests/EarlyIdentificationTest.php b/tests/EarlyIdentificationTest.php index 39f31bba..58bee05f 100644 --- a/tests/EarlyIdentificationTest.php +++ b/tests/EarlyIdentificationTest.php @@ -263,6 +263,14 @@ test('the tenant parameter is only removed from tenant routes when using path id ->middleware('tenant') ->name('tenant-route'); + RouteFacade::get($pathIdentification ? '/universal-route' : '/universal-route/{tenant?}', [ControllerWithMiddleware::class, 'routeHasTenantParameter']) + ->middleware('universal') + ->name('universal-route'); + + /** @var CloneRoutesAsTenant */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); + $cloneRoutesAction->handle(); + $tenant = Tenant::create(); $tenantKey = $tenant->getTenantKey(); @@ -274,12 +282,24 @@ test('the tenant parameter is only removed from tenant routes when using path id // Tenant parameter is removed from tenant routes using kernel path identification (Stancl\Tenancy\Listeners\ForgetTenantParameter) $response = pest()->get($tenantKey . '/tenant-route')->assertOk(); expect((bool) $response->getContent())->toBeFalse(); + + // The tenant parameter gets removed from the cloned universal route + $response = pest()->get($tenantKey . '/universal-route')->assertOk(); + expect((bool) $response->getContent())->toBeFalse(); } else { // Tenant parameter is not removed from tenant routes using other kernel identification MW $tenant->domains()->create(['domain' => $domain = $tenantKey . '.localhost']); $response = pest()->get("http://{$domain}/{$tenantKey}/tenant-route")->assertOk(); expect((bool) $response->getContent())->toBeTrue(); + + // The tenant parameter does not get removed from the universal route when accessing it through the central domain + $response = pest()->get("http://localhost/universal-route/$tenantKey")->assertOk(); + expect((bool) $response->getContent())->toBeTrue(); + + // The tenant parameter gets removed from the universal route when accessing it through the tenant domain + $response = pest()->get("http://{$domain}/universal-route")->assertOk(); + expect((bool) $response->getContent())->toBeFalse(); } } else { RouteFacade::middlewareGroup('tenant', [$pathIdentification ? InitializeTenancyByPath::class : InitializeTenancyByDomain::class]); @@ -370,7 +390,7 @@ test('route level identification is prioritized over kernel identification', fun 'default to central routes' => RouteMode::CENTRAL, ]); -test('routes with path identification middleware can get prefixed using the reregister action', function() { +test('routes with path identification middleware can get prefixed using the clone action', function() { $tenantKey = Tenant::create()->getTenantKey(); RouteFacade::get('/home', fn () => tenant()?->getTenantKey())->name('home')->middleware(InitializeTenancyByPath::class); diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php index c44f9c3e..18ea330d 100644 --- a/tests/UniversalRouteTest.php +++ b/tests/UniversalRouteTest.php @@ -169,10 +169,10 @@ test('a route can be universal using path identification', function (array $rout : 'Tenancy is not initialized.'; })->middleware($routeMiddleware); - /** @var CloneRoutesAsTenant $reregisterRoutesAction */ - $reregisterRoutesAction = app(CloneRoutesAsTenant::class); + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); - $reregisterRoutesAction->handle(); + $cloneRoutesAction->handle(); $tenantKey = Tenant::create()->getTenantKey(); @@ -234,10 +234,10 @@ test('correct exception is thrown when route is universal and tenant could not b RouteFacade::get('/foo', fn () => tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.')->middleware($routeMiddleware)->name('foo'); - /** @var CloneRoutesAsTenant $reregisterRoutesAction */ - $reregisterRoutesAction = app(CloneRoutesAsTenant::class); + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); - $reregisterRoutesAction->handle(); + $cloneRoutesAction->handle(); pest()->expectException(TenantCouldNotBeIdentifiedByPathException::class); $this->withoutExceptionHandling()->get('http://localhost/non_existent/foo'); @@ -309,7 +309,7 @@ test('a route can be flagged as universal in both route modes', function (RouteM 'default to central routes' => RouteMode::CENTRAL, ]); -test('ReregisterRoutesAsTenant registers prefixed duplicates of universal routes correctly', function (bool $kernelIdentification, bool $useController) { +test('CloneRoutesAsTenant registers prefixed duplicates of universal routes correctly', function (bool $kernelIdentification, bool $useController) { $routeMiddleware = ['universal']; if ($kernelIdentification) { @@ -328,10 +328,10 @@ test('ReregisterRoutesAsTenant registers prefixed duplicates of universal routes expect($routes = RouteFacade::getRoutes()->get())->toContain($universalRoute); expect($routes)->toContain($centralRoute); - /** @var CloneRoutesAsTenant $reregisterRoutesAction */ - $reregisterRoutesAction = app(CloneRoutesAsTenant::class); + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); - $reregisterRoutesAction->handle(); + $cloneRoutesAction->handle(); expect($routesAfterRegisteringDuplicates = RouteFacade::getRoutes()->get()) ->toContain($universalRoute) @@ -340,6 +340,7 @@ test('ReregisterRoutesAsTenant registers prefixed duplicates of universal routes $newRoute = collect($routesAfterRegisteringDuplicates)->filter(fn ($route) => ! in_array($route, $routes))->first(); expect($newRoute->uri())->toBe('{' . $tenantParameterName . '}' . '/' . $universalRoute->uri()); + expect(tenancy()->getRouteMiddleware($newRoute))->toBe(array_merge(tenancy()->getRouteMiddleware($universalRoute), ['tenant'])); $tenant = Tenant::create(); @@ -386,51 +387,55 @@ test('CloneRoutesAsTenant only clones routes with path identification by default expect($currentRouteCount())->toBe($newRouteCount = $initialRouteCount + 2); - /** @var CloneRoutesAsTenant $reregisterRoutesAction */ - $reregisterRoutesAction = app(CloneRoutesAsTenant::class); + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); - $reregisterRoutesAction->handle(); + $cloneRoutesAction->handle(); // Only one of the two routes gets cloned expect($currentRouteCount())->toBe($newRouteCount + 1); }); -test('custom callbacks can be used for reregistering universal routes', function () { - RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal')->name($routeName = 'home'); +test('custom callbacks can be used for cloning universal routes', function () { + RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware(['universal', InitializeTenancyByPath::class])->name($routeName = 'home'); - /** @var CloneRoutesAsTenant $reregisterRoutesAction */ - $reregisterRoutesAction = app(CloneRoutesAsTenant::class); + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); $initialRouteCount = $currentRouteCount(); + $cloneRoutesAction; + // Skip cloning the 'home' route - $reregisterRoutesAction->cloneUsing($routeName, function (Route $route) { + $cloneRoutesAction->cloneUsing($routeName, function (Route $route) { return; })->handle(); - // Expect route count to stay the same because the 'home' route re-registration gets skipped + // Expect route count to stay the same because the 'home' route cloning gets skipped expect($initialRouteCount)->toEqual($currentRouteCount()); - // Modify the 'home' route re-registration so that a different route is registered - $reregisterRoutesAction->cloneUsing($routeName, function (Route $route) { - RouteFacade::get('/newly-registered-route', fn() => true)->name('new.home'); + // Modify the 'home' route cloning so that a different route is cloned + $cloneRoutesAction->cloneUsing($routeName, function (Route $route) { + RouteFacade::get('/cloned-route', fn () => true)->name('new.home'); })->handle(); expect($currentRouteCount())->toEqual($initialRouteCount + 1); }); -test('reregistration of specific routes can get skipped', function () { +test('cloning of specific routes can get skipped', function () { RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.')->middleware('universal')->name($routeName = 'home'); - /** @var CloneRoutesAsTenant $reregisterRoutesAction */ - $reregisterRoutesAction = app(CloneRoutesAsTenant::class); + /** @var CloneRoutesAsTenant $cloneRoutesAction */ + $cloneRoutesAction = app(CloneRoutesAsTenant::class); $currentRouteCount = fn () => count(RouteFacade::getRoutes()->get()); $initialRouteCount = $currentRouteCount(); // Skip cloning the 'home' route - $reregisterRoutesAction->skipRoute($routeName)->handle(); + $cloneRoutesAction->skipRoute($routeName); - // Expect route count to stay the same because the 'home' route re-registration gets skipped + $cloneRoutesAction->handle(); + + // Expect route count to stay the same because the 'home' route cloning gets skipped expect($initialRouteCount)->toEqual($currentRouteCount()); }); @@ -458,6 +463,31 @@ test('identification middleware works with universal routes only when it impleme $this->withoutExceptionHandling()->get('http://localhost/custom-mw-universal-route'); }); +test('routes except nonuniversal routes with path id mw are given the tenant flag after cloning', function (array $routeMiddleware, array $globalMiddleware) { + foreach ($globalMiddleware as $middleware) { + if ($middleware === 'universal') { + config(['tenancy.default_route_mode' => RouteMode::UNIVERSAL]); + } else { + app(Kernel::class)->pushMiddleware($middleware); + } + } + + $route = RouteFacade::get('/home', fn () => tenant() ? 'Tenancy initialized.' : 'Tenancy not initialized.') + ->middleware($routeMiddleware) + ->name($routeName = 'home'); + + app(CloneRoutesAsTenant::class)->handle(); + + $clonedRoute = RouteFacade::getRoutes()->getByName('tenant.' . $routeName); + + // Non-universal routes with identification middleware are already considered tenant, so they don't get the tenant flag + if (! tenancy()->routeIsUniversal($route) && tenancy()->routeHasIdentificationMiddleware($clonedRoute)) { + expect($clonedRoute->middleware())->not()->toContain('tenant'); + } else { + expect($clonedRoute->middleware())->toContain('tenant'); + } +})->with('path identification types'); + foreach ([ 'domain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class], 'subdomain identification types' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyBySubdomain::class],