1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 19:14:04 +00:00

Improve route cloning action (#8)

* 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 <phpcsfixer@example.com>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
lukinovec 2023-08-28 13:17:17 +02:00 committed by GitHub
parent 4d4639450e
commit f7d9f02fd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 222 additions and 111 deletions

View file

@ -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<Route> $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

View file

@ -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.
*/

View file

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

View file

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

View file

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

View file

@ -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`
}