From 883b91805a3f534d0759dad1dc74d9735e76f06a Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 9 Mar 2026 11:12:21 +0100 Subject: [PATCH 1/4] Add `toRoute()` override to TenancyUrlGenerator Also update `route()` override since `parent::route()` calls `toRoute()` under the hood (similarly to how `parent::temporarySignedRoute()` calls `route()`) --- src/Overrides/TenancyUrlGenerator.php | 29 ++++++++++++++++++- .../UrlGeneratorBootstrapperTest.php | 22 ++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index f7ed9a84..d89117fb 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -114,7 +114,15 @@ class TenancyUrlGenerator extends UrlGenerator throw new InvalidArgumentException('Attribute [name] expects a string backed enum.'); } - [$name, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type + $wrappedParameters = Arr::wrap($parameters); + + [$name, $parameters] = $this->prepareRouteInputs($name, $wrappedParameters); // @phpstan-ignore argument.type + + if (isset($wrappedParameters[static::$bypassParameter])) { + // If the bypass parameter was passed, we need to add it back to the parameters after prepareRouteInputs() removes it, + // so that the underlying toRoute() call in parent::route() can bypass the behavior modification as well. + $parameters[static::$bypassParameter] = $wrappedParameters[static::$bypassParameter]; + } return parent::route($name, $parameters, $absolute); } @@ -142,6 +150,25 @@ class TenancyUrlGenerator extends UrlGenerator return parent::temporarySignedRoute($name, $expiration, $parameters, $absolute); } + /** + * Override the toRoute() method so that the route name gets prefixed + * and the tenant parameter gets added when in tenant context. + */ + public function toRoute($route, $parameters, $absolute) + { + $name = $route->getName(); + + if ($name) { + [$prefixedName, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type + + if ($prefixedName !== $name && $tenantRoute = $this->routes->getByName($prefixedName)) { + $route = $tenantRoute; + } + } + + return parent::toRoute($route, $parameters, $absolute); + } + /** * Return bool indicating if the bypass parameter was in $parameters. */ diff --git a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php index f089207a..97da0d07 100644 --- a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php +++ b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php @@ -401,3 +401,25 @@ test('the bypass parameter works correctly with temporarySignedRoute', function( ->toContain('localhost/foo') ->not()->toContain('central='); // Bypass parameter gets removed from the generated URL }); + +test('the toRoute method can automatically prefix the passed route name', function () { + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + Route::get('/central/home', fn () => 'central')->name('home'); + Route::get('/tenant/home', fn () => 'tenant')->name('tenant.home'); + + TenancyUrlGenerator::$prefixRouteNames = true; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $centralRoute = Route::getRoutes()->getByName('home'); + + // url()->toRoute() prefixes the name of the passed route ('home') with the tenant prefix + // and generates the URL for the tenant route (as if the 'tenant.home' route was passed to the method) + expect(url()->toRoute($centralRoute, [], true))->toBe('http://localhost/tenant/home'); + + // Passing the bypass parameter skips the name prefixing, so the method returns the central route URL + expect(url()->toRoute($centralRoute, ['central' => true], true))->toBe('http://localhost/central/home'); +}); From 0e2c73d6386a44e8eab9bf64dd982f0272874cbc Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 9 Mar 2026 11:50:11 +0100 Subject: [PATCH 2/4] Remove phpstan ignore --- src/Overrides/TenancyUrlGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index d89117fb..851f174c 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -159,7 +159,7 @@ class TenancyUrlGenerator extends UrlGenerator $name = $route->getName(); if ($name) { - [$prefixedName, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type + [$prefixedName, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); if ($prefixedName !== $name && $tenantRoute = $this->routes->getByName($prefixedName)) { $route = $tenantRoute; From 8345d648123547c8a9cab588bef358ec1e44ca0e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 10 Mar 2026 12:17:09 +0100 Subject: [PATCH 3/4] Simplify TenancyUrlGenerator The `toRoute()` and `route()` overrides cover `temporarySignedRoute())`, so we don't need to specifically override `temporarySIgnedRoute()` anymore. `route()` override got simplified since everything except for the name prefixing is delegated to the lower-level `toRoute()` method. --- src/Overrides/TenancyUrlGenerator.php | 45 +++++++-------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index 851f174c..b8930fe4 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -105,8 +105,16 @@ class TenancyUrlGenerator extends UrlGenerator public static bool $passQueryParameter = true; /** - * Override the route() method so that the route name gets prefixed - * and the tenant parameter gets added when in tenant context. + * Override the route() method to prefix the route name before $this->routes->getByName($name) is called + * in the parent route() call. + * + * This is necessary because $this->routes->getByName($name) is called to retrieve the route + * before passing it to toRoute(). If only the prefixed route (e.g. 'tenant.foo') is registered + * and the original ('foo') isn't, route() would throw a RouteNotFoundException. + * So route() has to be overridden to prefix the passed route name, even though toRoute() is overridden already. + * + * Only the name is taken from prepareRouteInputs() here — parameter handling + * (adding tenant parameter, removing bypass parameter) is delegated to toRoute(). */ public function route($name, $parameters = [], $absolute = true) { @@ -114,42 +122,11 @@ class TenancyUrlGenerator extends UrlGenerator throw new InvalidArgumentException('Attribute [name] expects a string backed enum.'); } - $wrappedParameters = Arr::wrap($parameters); - - [$name, $parameters] = $this->prepareRouteInputs($name, $wrappedParameters); // @phpstan-ignore argument.type - - if (isset($wrappedParameters[static::$bypassParameter])) { - // If the bypass parameter was passed, we need to add it back to the parameters after prepareRouteInputs() removes it, - // so that the underlying toRoute() call in parent::route() can bypass the behavior modification as well. - $parameters[static::$bypassParameter] = $wrappedParameters[static::$bypassParameter]; - } + [$name] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type return parent::route($name, $parameters, $absolute); } - /** - * Override the temporarySignedRoute() method so that the route name gets prefixed - * and the tenant parameter gets added when in tenant context. - */ - public function temporarySignedRoute($name, $expiration, $parameters = [], $absolute = true) - { - if ($name instanceof BackedEnum && ! is_string($name = $name->value)) { - throw new InvalidArgumentException('Attribute [name] expects a string backed enum.'); - } - - $wrappedParameters = Arr::wrap($parameters); - - [$name, $parameters] = $this->prepareRouteInputs($name, $wrappedParameters); // @phpstan-ignore argument.type - - if (isset($wrappedParameters[static::$bypassParameter])) { - // If the bypass parameter was passed, we need to add it back to the parameters after prepareRouteInputs() removes it, - // so that the underlying route() call in parent::temporarySignedRoute() can bypass the behavior modification as well. - $parameters[static::$bypassParameter] = $wrappedParameters[static::$bypassParameter]; - } - - return parent::temporarySignedRoute($name, $expiration, $parameters, $absolute); - } - /** * Override the toRoute() method so that the route name gets prefixed * and the tenant parameter gets added when in tenant context. From f0ff717acb5f0c223fdfd9bddfb0471c9b7da328 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 10 Mar 2026 13:27:35 +0100 Subject: [PATCH 4/4] Update comments in TenancyUrlGenerator and UrlGeneratorBootstrapper --- .../UrlGeneratorBootstrapper.php | 2 +- src/Overrides/TenancyUrlGenerator.php | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Bootstrappers/UrlGeneratorBootstrapper.php b/src/Bootstrappers/UrlGeneratorBootstrapper.php index 3708d636..ba1a6d05 100644 --- a/src/Bootstrappers/UrlGeneratorBootstrapper.php +++ b/src/Bootstrappers/UrlGeneratorBootstrapper.php @@ -16,7 +16,7 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver; /** * Makes the app use TenancyUrlGenerator (instead of Illuminate\Routing\UrlGenerator) which: * - prefixes route names with the tenant route name prefix (PathTenantResolver::tenantRouteNamePrefix() by default) - * - passes the tenant parameter to the link generated by route() and temporarySignedRoute() (PathTenantResolver::tenantParameterName() by default). + * - passes the tenant parameter (PathTenantResolver::tenantParameterName() by default) to the link generated by the affected methods like route() and temporarySignedRoute(). * * Used with path and query string identification. * diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index b8930fe4..d9894b3e 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -22,16 +22,17 @@ use Stancl\Tenancy\Resolvers\RequestDataTenantResolver; * - Automatically passing ['tenant' => ...] to each route() call -- if TenancyUrlGenerator::$passTenantParameterToRoutes is enabled * This is a more universal solution since it supports both path identification and query parameter identification. * - * - Prepends route names passed to route() and URL::temporarySignedRoute() - * with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled. + * - Prepends route names with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled. * This is primarily useful when using route cloning with path identification. * - * To bypass this behavior on any single route() call, pass the $bypassParameter as true (['central' => true] by default). + * Affected methods: route(), toRoute(), temporarySignedRoute(), signedRoute() (the last two via the route() override). + * + * To bypass this behavior on any single affected method call, pass the $bypassParameter as true (['central' => true] by default). */ class TenancyUrlGenerator extends UrlGenerator { /** - * Parameter which works as a flag for bypassing the behavior modification of route() and temporarySignedRoute(). + * Parameter which works as a flag for bypassing the behavior modification of the affected methods. * * For example, in tenant context: * Route::get('/', ...)->name('home'); @@ -44,12 +45,12 @@ class TenancyUrlGenerator extends UrlGenerator * Note: UrlGeneratorBootstrapper::$addTenantParameterToDefaults is not affected by this, though * it doesn't matter since it doesn't pass any extra parameters when not needed. * - * @see UrlGeneratorBootstrapper + * @see Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper */ public static string $bypassParameter = 'central'; /** - * Should route names passed to route() or temporarySignedRoute() + * Should route names passed to the affected methods * get prefixed with the tenant route name prefix. * * This is useful when using e.g. path identification with third-party packages @@ -59,12 +60,12 @@ class TenancyUrlGenerator extends UrlGenerator public static bool $prefixRouteNames = false; /** - * Should the tenant parameter be passed to route() or temporarySignedRoute() calls. + * Should the tenant parameter be passed to the affected methods. * * This is useful with path or query parameter identification. The former can be handled * more elegantly using UrlGeneratorBootstrapper::$addTenantParameterToDefaults. * - * @see UrlGeneratorBootstrapper + * @see Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper */ public static bool $passTenantParameterToRoutes = false; @@ -115,6 +116,8 @@ class TenancyUrlGenerator extends UrlGenerator * * Only the name is taken from prepareRouteInputs() here — parameter handling * (adding tenant parameter, removing bypass parameter) is delegated to toRoute(). + * + * Affects temporarySignedRoute() and signedRoute() as well since they call route() under the hood. */ public function route($name, $parameters = [], $absolute = true) { @@ -130,6 +133,8 @@ class TenancyUrlGenerator extends UrlGenerator /** * Override the toRoute() method so that the route name gets prefixed * and the tenant parameter gets added when in tenant context. + * + * Also affects route(). Even though route() is overridden separately, it delegates parameter handling to toRoute(). */ public function toRoute($route, $parameters, $absolute) {