From 16861d25998d9b281a9a8901e3bb41d119595a20 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 9 Mar 2026 02:07:02 +0100 Subject: [PATCH] [4.x] Make `URL::temporarySignedRoute()` respect the bypass parameter (#1438) Using `URL::temporarySignedRoute()` in tenant context with `UrlGeneratorBootstrapper` enabled doesn't work the same as `route()`. The bypass parameter doesn't actually bypass the route name prefixing. `route()` is called in the `parent::temporarySignedRoute()` call, and because the bypass parameter is removed before calling `parent::temporarySignedRoute()`, the underlying `route()` call doesn't get the bypass parameter and it ends up attempting to generate URL for a route with the name prefixed with 'tenant.'. This PR adds the bypass parameter back after `prepareRouteInputs()`, so that `parent::temporarySignedRoute()` receives it, and the underlying `route()` call respects it. Also added basic tests for the `URL::temporarySignedRoute()` behavior (the new bypass parameter test works as a regression test). --- src/Overrides/TenancyUrlGenerator.php | 10 ++++- .../UrlGeneratorBootstrapperTest.php | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index 88ae54f3..f7ed9a84 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -129,7 +129,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 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); } diff --git a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php index 647422da..f089207a 100644 --- a/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php +++ b/tests/Bootstrappers/UrlGeneratorBootstrapperTest.php @@ -2,6 +2,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Routing\UrlGenerator; +use Illuminate\Support\Facades\URL; use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; @@ -25,12 +26,16 @@ beforeEach(function () { Event::listen(TenancyEnded::class, RevertToCentralContext::class); TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$passTenantParameterToRoutes = false; + TenancyUrlGenerator::$overrides = []; + TenancyUrlGenerator::$bypassParameter = 'central'; UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false; }); afterEach(function () { TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$passTenantParameterToRoutes = false; + TenancyUrlGenerator::$overrides = []; + TenancyUrlGenerator::$bypassParameter = 'central'; UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false; }); @@ -359,3 +364,40 @@ test('both the name prefixing and the tenant parameter logic gets skipped when b expect(route('home', ['bypassParameter' => false, 'tenant' => $tenant->getTenantKey()]))->toBe($tenantRouteUrl) ->not()->toContain('bypassParameter'); }); + +test('the temporarySignedRoute method can automatically prefix the passed route name', function() { + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + Route::get('/{tenant}/foo', fn () => 'foo')->name('tenant.foo')->middleware([InitializeTenancyByPath::class]); + + TenancyUrlGenerator::$prefixRouteNames = true; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + // Route name ('foo') gets prefixed automatically (will be 'tenant.foo') + $tenantSignedUrl = URL::temporarySignedRoute('foo', now()->addMinutes(2), ['tenant' => $tenantKey = $tenant->getTenantKey()]); + + expect($tenantSignedUrl)->toContain("localhost/{$tenantKey}/foo"); +}); + +test('the bypass parameter works correctly with temporarySignedRoute', function() { + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + Route::get('/foo', fn () => 'foo')->name('central.foo'); + + TenancyUrlGenerator::$prefixRouteNames = true; + TenancyUrlGenerator::$bypassParameter = 'central'; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + // Bypass parameter allows us to generate URL for the 'central.foo' route in tenant context + $centralSignedUrl = URL::temporarySignedRoute('central.foo', now()->addMinutes(2), ['central' => true]); + + expect($centralSignedUrl) + ->toContain('localhost/foo') + ->not()->toContain('central='); // Bypass parameter gets removed from the generated URL +});