1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-14 03:14:03 +00:00
tenancy/src/Actions/CloneRoutesAsTenant.php
lukinovec c312156c18
Give universal flag highest priority (#27)
* Give universal flag the highest priority (wip)

* Stop forgetting tenant parameter when route-level path ID is used

* Fix PHPStan errors

* Simplify annotation

* Fix comment

* Correct annotations

* Improve requestHasTenant comment

* Make cloning logic only clone universal routes, delete the universal flag from the new (tenant) route

* Delete APP_DEBUG

* make if condition easier to read

* Update DealsWithRouteContexts.php

* Fix test

* Fix code style (php-cs-fixer)

* Move tests

* Delete incorrectly committed file

* Cloning routes update wip

* Route cloning rework WIP

* Add todo to clone routes

* Fix code style (php-cs-fixer)

* Route cloning fix WIP

* Set CloneRoutesAsTenant::$tenantMiddleware to ID MW

* Revert CloneRoutesAsTenant::$tenantMiddleware-related changes

* Simplify requestHasTenant

* Add and test 'ckone' flag

* Delete setting $skippedRoutes from CloneRoutesAsTenant

* Fix code style (php-cs-fixer)

* make config key used for testing distinct from normal tenancy config keys

* Update src/Actions/CloneRoutesAsTenant.php

* Move 'path identification types' dataset to CloneActionTest

---------

Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-01-25 15:27:17 +01:00

199 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Actions;
use Closure;
use Illuminate\Routing\Route;
use Illuminate\Routing\Router;
use Illuminate\Support\Collection;
use Stancl\Tenancy\Enums\RouteMode;
use Stancl\Tenancy\PathIdentificationManager;
/**
* The CloneRoutesAsTenant action clones
* routes flagged with the 'universal' middleware,
* all routes without a flag if the default route mode is universal,
* and routes that directly use the InitializeTenancyByPath middleware.
*
* The main purpose of this action is to make the integration
* of packages (e.g., Jetstream or Livewire) easier with path-based tenant identification.
*
* By default, universal routes are cloned as tenant routes (= they get flagged with the 'tenant' middleware)
* and prefixed with the '/{tenant}' path prefix. Their name also gets prefixed with the tenant name prefix.
*
* Routes with the path identification middleware get cloned similarly, but only if they're not universal at the same time.
* Unlike universal routes, these routes don't get the tenant flag,
* because they don't need it (they're not universal, and they have the identification MW, so they're already considered tenant).
*
* You can use the `cloneUsing()` hook to customize the route definitions,
* and the `skipRoute()` method to skip cloning of specific routes.
* You can also use the $tenantParameterName and $tenantRouteNamePrefix
* static properties to customize the tenant parameter name or the route name prefix.
*
* Note that routes already containing the tenant parameter or prefix won't be cloned.
*/
class CloneRoutesAsTenant
{
protected array $cloneRouteUsing = [];
protected array $skippedRoutes = [
'stancl.tenancy.asset',
];
public function __construct(
protected Router $router,
) {
}
public function handle(): void
{
$this->router
->prefix('/{' . PathIdentificationManager::getTenantParameterName() . '}/')
->group(fn () => $this->getRoutesToClone()->each(fn (Route $route) => $this->cloneRoute($route)));
$this->router->getRoutes()->refreshNameLookups();
}
/**
* Make the action clone a specific route using the provided callback instead of the default one.
*/
public function cloneUsing(string $routeName, Closure $callback): static
{
$this->cloneRouteUsing[$routeName] = $callback;
return $this;
}
/**
* Skip a route's cloning.
*/
public function skipRoute(string $routeName): static
{
$this->skippedRoutes[] = $routeName;
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 (
tenancy()->routeHasMiddleware($route, 'tenant') ||
in_array($route->getName(), $this->skippedRoutes, true) ||
in_array($tenantParameterName, $route->parameterNames(), 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);
if (
$pathIdentificationUsed &&
(tenancy()->getRouteMode($route) === RouteMode::UNIVERSAL || tenancy()->routeHasMiddleware($route, 'clone'))
) {
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.
*/
protected function cloneRoute(Route $route): void
{
$routeName = $route->getName();
// If the route's cloning callback exists
// Use the callback to clone the route instead of the default way of cloning routes
if ($routeName && $customRouteCallback = data_get($this->cloneRouteUsing, $routeName)) {
$customRouteCallback($route);
return;
}
$newRoute = $this->createNewRoute($route);
if (! tenancy()->routeHasMiddleware($route, 'tenant')) {
$newRoute->middleware('tenant');
}
$this->copyMiscRouteProperties($route, $newRoute);
}
protected function createNewRoute(Route $route): Route
{
$method = strtolower($route->methods()[0]);
$routeName = $route->getName();
$tenantRouteNamePrefix = PathIdentificationManager::getTenantRouteNamePrefix();
/** @var Route $newRoute */
$newRoute = $this->router->$method($route->uri(), $route->action);
// Delete middleware from the new route and
// Add original route middleware to ensure there's no duplicate middleware
unset($newRoute->action['middleware']);
// Exclude `universal` and `clone` middleware from the new route -- it should specifically be a tenant route
$newRoute->middleware(array_filter(
tenancy()->getRouteMiddleware($route),
fn (string $middleware) => ! in_array($middleware, ['universal', 'clone'])
));
if ($routeName && ! $route->named($tenantRouteNamePrefix . '*')) {
// Clear the route name so that `name()` sets the route name instead of suffixing it
unset($newRoute->action['as']);
$newRoute->name($tenantRouteNamePrefix . $routeName);
}
return $newRoute;
}
/**
* Copy misc properties of the original route to the new route.
*/
protected function copyMiscRouteProperties(Route $originalRoute, Route $newRoute): void
{
$newRoute
->setBindingFields($originalRoute->bindingFields())
->setFallback($originalRoute->isFallback)
->setWheres($originalRoute->wheres)
->block($originalRoute->locksFor(), $originalRoute->waitsFor())
->withTrashed($originalRoute->allowsTrashedBindings())
->setDefaults($originalRoute->defaults);
}
}