diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 46f35515..e0b69e6e 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -242,7 +242,24 @@ class TenancyServiceProvider extends ServiceProvider /** @var CloneRoutesAsTenant $cloneRoutes */ $cloneRoutes = $this->app->make(CloneRoutesAsTenant::class); - /** See CloneRoutesAsTenant for usage details. */ + // The cloning action has two modes: + // 1. Clone all routes that have the middleware present in the action's $cloneRoutesWithMiddleware property. + // You can customize the middleware that triggers cloning by using cloneRoutesWithMiddleware() on the action. + // + // By default, the middleware is ['clone'], but using $cloneRoutes->cloneRoutesWithMiddleware(['clone', 'universal'])->handle() + // will clone all routes that have either 'clone' or 'universal' middleware (mentioning 'universal' since that's a common use case). + // + // Also, you can use the shouldClone() method to provide a custom closure that determines if a route should be cloned. + // + // 2. Clone only the routes that were manually added to the action using cloneRoute(). + // + // Regardless of the mode, you can provide a custom closure for defining the cloned route, e.g.: + // $cloneRoutesAction->cloneUsing(function (Route $route) { + // RouteFacade::get('/cloned/' . $route->uri(), fn () => 'cloned route')->name('cloned.' . $route->getName()); + // })->handle(); + // This will make all cloned routes use the custom closure to define the cloned route instead of the default behavior. + // See Stancl\Tenancy\Actions\CloneRoutesAsTenant for more details. + $cloneRoutes->handle(); } diff --git a/src/Actions/CloneRoutesAsTenant.php b/src/Actions/CloneRoutesAsTenant.php index 6e988907..f1cb1450 100644 --- a/src/Actions/CloneRoutesAsTenant.php +++ b/src/Actions/CloneRoutesAsTenant.php @@ -30,8 +30,6 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; * By providing a callback to shouldClone(), you can change how it's determined if a route should be cloned if you don't want to use middleware flags. * * Cloned routes are prefixed with '/{tenant}', flagged with 'tenant' middleware, and have their names prefixed with 'tenant.'. - * The addition of the 'tenant' middleware can be controlled using addTenantMiddleware(array). You can specify the identification - * middleware to be used on the cloned route using that method -- instead of using the approach that "inherits" it from a universal route. * * The addition of the tenant parameter can be controlled using addTenantParameter(true|false). Note that if you decide to disable * tenant parameter addition, the routes MUST differ in domains. This can be controlled using the domain(string|null) method. The @@ -41,7 +39,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; * Routes with names that are already prefixed won't be cloned - but that's just the default behavior. * The cloneUsing() method allows you to completely override the default behavior and customize how the cloned routes will be defined. * - * After cloning, only top-level middleware in $cloneRoutesWithMiddleware (as well as any route context flags) will be removed + * After cloning, only top-level middleware in $cloneRoutesWithMiddleware will be removed * from the new route (so by default, 'clone' will be omitted from the new route's MW). * Middleware groups are preserved as-is, even if they contain cloning middleware. * @@ -73,7 +71,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; * // cloned route can be customized using domain(string|null). By default, the cloned route will not be scoped to a domain, * // unless a domain() call is used. It's important to keep in mind that: * // 1. When addTenantParameter(false) is used, the paths will be the same, thus domains must differ. - * // 2. If the original route has no domain, the cloned route will override the original route as they will directly conflict. + * // 2. If the original route (with the same path) has no domain, the cloned route will never be used due to registration order. * $cloneAction->addTenantParameter(false)->cloneRoutesWithMiddleware(['clone'])->cloneRoute('no-tenant-parameter')->handle(); * ``` * @@ -86,50 +84,27 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; */ class CloneRoutesAsTenant { - /** @var list */ protected array $routesToClone = []; - protected bool $addTenantParameter = true; protected bool $tenantParameterBeforePrefix = true; protected string|null $domain = null; - - /** - * The callback should accept a Route instance or the route name (string). - * - * @var ?Closure(Route|string): void - */ - protected Closure|null $cloneUsing = null; - - /** @var ?Closure(Route): bool */ + protected Closure|null $cloneUsing = null; // The callback should accept Route instance or the route name (string) protected Closure|null $shouldClone = null; - - /** @var list */ protected array $cloneRoutesWithMiddleware = ['clone']; - /** @var list */ - protected array $addTenantMiddleware = ['tenant']; - public function __construct( protected Router $router, ) {} - public static function make(): static - { - return app(static::class); - } - /** Clone routes. This resets routesToClone() but not other config. */ public function handle(): void { // If no routes were specified using cloneRoute(), get all routes // and for each, determine if it should be cloned if (! $this->routesToClone) { - /** @var list */ - $routesToClone = collect($this->router->getRoutes()->get()) + $this->routesToClone = collect($this->router->getRoutes()->get()) ->filter(fn (Route $route) => $this->shouldBeCloned($route)) ->all(); - - $this->routesToClone = $routesToClone; } foreach ($this->routesToClone as $route) { @@ -143,9 +118,7 @@ class CloneRoutesAsTenant if (is_string($route)) { $this->router->getRoutes()->refreshNameLookups(); - $routeName = $route; - $route = $this->router->getRoutes()->getByName($routeName); - assert(! is_null($route), "Route [{$routeName}] was meant to be cloned but does not exist."); + $route = $this->router->getRoutes()->getByName($route); } $this->createNewRoute($route); @@ -170,20 +143,6 @@ class CloneRoutesAsTenant return $this; } - /** - * The tenant middleware to be added to the cloned route. - * - * If used with early identification, make sure to include 'tenant' in this array. - * - * @param list $middleware - */ - public function addTenantMiddleware(array $middleware): static - { - $this->addTenantMiddleware = $middleware; - - return $this; - } - /** The domain the cloned route should use. Set to null if it shouldn't be scoped to a domain. */ public function domain(string|null $domain): static { @@ -192,11 +151,7 @@ class CloneRoutesAsTenant return $this; } - /** - * Provide a custom callback for cloning routes, instead of the default behavior. - * - * @param ?Closure(Route|string): void $cloneUsing - */ + /** Provide a custom callback for cloning routes, instead of the default behavior. */ public function cloneUsing(Closure|null $cloneUsing): static { $this->cloneUsing = $cloneUsing; @@ -204,11 +159,7 @@ class CloneRoutesAsTenant return $this; } - /** - * Specify which middleware should serve as "flags" telling this action to clone those routes. - * - * @param list $middleware - */ + /** Specify which middleware should serve as "flags" telling this action to clone those routes. */ public function cloneRoutesWithMiddleware(array $middleware): static { $this->cloneRoutesWithMiddleware = $middleware; @@ -219,9 +170,7 @@ class CloneRoutesAsTenant /** * Provide a custom callback for determining whether a route should be cloned. * Overrides the default middleware-based detection. - * - * @param Closure(Route): bool $shouldClone - */ + * */ public function shouldClone(Closure|null $shouldClone): static { $this->shouldClone = $shouldClone; @@ -244,18 +193,6 @@ class CloneRoutesAsTenant return $this; } - /** - * Clone individual routes. - * - * @param list $routes - */ - public function cloneRoutes(array $routes): static - { - $this->routesToClone = array_merge($this->routesToClone, $routes); - - return $this; - } - protected function shouldBeCloned(Route $route): bool { // Don't clone routes that already have tenant parameter or prefix @@ -321,15 +258,17 @@ class CloneRoutesAsTenant return $newRoute; } - /** Removes top-level cloneRoutesWithMiddleware and context flags, adds 'tenant' middleware. */ + /** Removes top-level cloneRoutesWithMiddleware and adds 'tenant' middleware. */ protected function processMiddlewareForCloning(array $middleware): array { $processedMiddleware = array_filter( $middleware, - fn ($mw) => ! in_array($mw, $this->cloneRoutesWithMiddleware) && ! in_array($mw, ['central', 'tenant', 'universal']) + fn ($mw) => ! in_array($mw, $this->cloneRoutesWithMiddleware) ); - return array_unique(array_merge($processedMiddleware, $this->addTenantMiddleware)); + $processedMiddleware[] = 'tenant'; + + return array_unique($processedMiddleware); } /** Check if route already has tenant parameter or name prefix. */ diff --git a/tests/CloneActionTest.php b/tests/CloneActionTest.php index ab9c5e9b..28a8ccd3 100644 --- a/tests/CloneActionTest.php +++ b/tests/CloneActionTest.php @@ -401,77 +401,3 @@ test('tenant parameter addition can be controlled by setting addTenantParameter' $this->withoutExceptionHandling()->get('http://central.localhost/foo')->assertSee('central'); } })->with([true, false]); - -test('existing context flags are removed during cloning', function () { - RouteFacade::get('/foo', fn () => true)->name('foo')->middleware(['clone', 'central']); - RouteFacade::get('/bar', fn () => true)->name('bar')->middleware(['clone', 'universal']); - - $cloneAction = app(CloneRoutesAsTenant::class); - - // Clone foo route - $cloneAction->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName()) - ->toContain('tenant.foo'); - expect(tenancy()->getRouteMiddleware(RouteFacade::getRoutes()->getByName('tenant.foo'))) - ->not()->toContain('central'); - - // Clone bar route - $cloneAction->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName()) - ->toContain('tenant.foo', 'tenant.bar'); - expect(tenancy()->getRouteMiddleware(RouteFacade::getRoutes()->getByName('tenant.foo'))) - ->not()->toContain('universal'); -}); - -test('cloning a route without a prefix or differing domains overrides the original route', function () { - RouteFacade::get('/foo', fn () => true)->name('foo')->middleware(['clone']); - - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('foo'); - - $cloneAction = CloneRoutesAsTenant::make(); - $cloneAction->cloneRoute('foo') - ->addTenantParameter(false) - ->tenantParameterBeforePrefix(false) - ->handle(); - - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.foo'); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->not()->toContain('foo'); -}); - -test('addTenantMiddleware can be used to specify the tenant middleware for the cloned route', function () { - RouteFacade::get('/foo', fn () => true)->name('foo')->middleware(['clone']); - RouteFacade::get('/bar', fn () => true)->name('bar')->middleware(['clone']); - - $cloneAction = app(CloneRoutesAsTenant::class); - - $cloneAction->cloneRoute('foo')->addTenantMiddleware([InitializeTenancyByPath::class])->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.foo'); - $cloned = RouteFacade::getRoutes()->getByName('tenant.foo'); - expect($cloned->uri())->toBe('{tenant}/foo'); - expect($cloned->getName())->toBe('tenant.foo'); - expect(tenancy()->getRouteMiddleware($cloned))->toBe([InitializeTenancyByPath::class]); - - $cloneAction->cloneRoute('bar') - ->addTenantMiddleware([InitializeTenancyByDomain::class]) - ->domain('foo.localhost') - ->addTenantParameter(false) - ->tenantParameterBeforePrefix(false) - ->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.bar'); - $cloned = RouteFacade::getRoutes()->getByName('tenant.bar'); - expect($cloned->uri())->toBe('bar'); - expect($cloned->getName())->toBe('tenant.bar'); - expect($cloned->getDomain())->toBe('foo.localhost'); - expect(tenancy()->getRouteMiddleware($cloned))->toBe([InitializeTenancyByDomain::class]); -}); - -test('cloneRoutes can be used to clone multiple routes', function () { - RouteFacade::get('/foo', fn () => true)->name('foo'); - $bar = RouteFacade::get('/bar', fn () => true)->name('bar'); - RouteFacade::get('/baz', fn () => true)->name('baz'); - - CloneRoutesAsTenant::make()->cloneRoutes(['foo', $bar])->handle(); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.foo'); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->toContain('tenant.bar'); - expect(collect(RouteFacade::getRoutes()->get())->map->getName())->not()->toContain('tenant.baz'); -});