From 16861d25998d9b281a9a8901e3bb41d119595a20 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 9 Mar 2026 02:07:02 +0100 Subject: [PATCH 1/2] [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 +}); From 8f3ea6297f3b24e972a7598c01f202064edfb2f7 Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Mon, 9 Mar 2026 02:11:07 +0100 Subject: [PATCH 2/2] phpstan: change InputOption syntax --- src/Commands/TenantDump.php | 2 +- src/Concerns/HasTenantOptions.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 32677efc..97f9d539 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -63,7 +63,7 @@ class TenantDump extends DumpCommand protected function getOptions(): array { return array_merge([ - ['tenant', null, InputOption::VALUE_OPTIONAL, '', null], + new InputOption('tenant', null, InputOption::VALUE_OPTIONAL, '', null), ], parent::getOptions()); } } diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index 5beb3268..c1ea221f 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -17,8 +17,8 @@ trait HasTenantOptions protected function getOptions() { return array_merge([ - ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null], - ['with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'], // todo@pending should we also offer without-pending? if we add this, mention in docs + new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null), + new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs ], parent::getOptions()); }