From 4953c69fd8c22646e5449cbbd1f2f3d3f351a18e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Sun, 26 Nov 2023 21:08:41 +0100 Subject: [PATCH] Update path identification and Fortify integration-related logic (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add commented UrlBinding + FortifyRouteTenancy bootstrappers to the config * Improve FortifyRoute bootstrapper docblock * Rename bootstrappers * Complete renaming * Pass defaults of the original URL generator to the new one * Fix URL generator-related test (query string id test WIP) * Fix code style (php-cs-fixer) * Make Fortify bootstrapper not depend on the UrlGenerator bootstrapper, update comments * Fix testing UrlGenerator bootstrapper * Update TenancyUrlGenerator annotations * Pass tenant parameter manually in Fortify bootstrapper * Properly test TenancyUrlGenerator functionality * Get rid of query string in Fortify bootstrapper * Fix code style (php-cs-fixer) * Delete outdated comment * Improve comment * Improve before/afterEach * Encourage passing parameters using TenancyUrlGenerator instead of URL::defaults() * Delete rest of defaulting logic * Fix code style (php-cs-fixer) * Delete test group * Update ForgetTenantParameter docblock * Update passTenantParameterToRoutes annotation * Complete todo in test * Improve test * Update comment * Improve comment * Add keepQueryParameters bool to Fortify bootstrapper * Test keepQueryParameters * minor docblock update * minor docblock changes * Delete extra import * Update src/Overrides/TenancyUrlGenerator.php Co-authored-by: Samuel Štancl * Improve comment * Rename test * Update bypass parameter-related test comments * Fix merge * Rename $keepQueryParameters * Add docblock * Add comment * Refactor Fortify bootstrapper * Fix code style (php-cs-fixer) * Fix comment * Skip Fortify bootstrapper test * minor code improvements * Improve fortify bootstrapper test * Add Fortify bootstrapper annotation, improve code * Fix code style (php-cs-fixer) * Add commenet * Complete resource syncing todo (cleanup not needed) * Delete incorrect namespace * Complete route context trait name todo * Fix code style (php-cs-fixer) --------- Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- assets/TenancyServiceProvider.stub.php | 2 +- assets/config.php | 4 +- .../FortifyRouteTenancyBootstrapper.php | 84 +++++-- ...otstrapper.php => RootUrlBootstrapper.php} | 2 +- ...apper.php => UrlGeneratorBootstrapper.php} | 4 +- ...ication.php => DealsWithRouteContexts.php} | 3 +- src/Listeners/ForgetTenantParameter.php | 11 +- src/Middleware/InitializeTenancyByPath.php | 18 -- src/Overrides/TenancyUrlGenerator.php | 14 +- src/Tenancy.php | 4 +- tests/BootstrapperTest.php | 208 +++++++++++++----- tests/EarlyIdentificationTest.php | 4 - tests/PathIdentificationTest.php | 20 -- tests/ResourceSyncingUsingPolymorphicTest.php | 3 - tests/TenantAssetTest.php | 7 +- tests/TestCase.php | 8 +- 16 files changed, 255 insertions(+), 141 deletions(-) rename src/Bootstrappers/{UrlTenancyBootstrapper.php => RootUrlBootstrapper.php} (94%) rename src/Bootstrappers/{UrlBindingBootstrapper.php => UrlGeneratorBootstrapper.php} (93%) rename src/Concerns/{DealsWithEarlyIdentification.php => DealsWithRouteContexts.php} (98%) diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 5bd85ce0..2840b07c 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -128,7 +128,7 @@ class TenancyServiceProvider extends ServiceProvider /** * Example of CLI tenant URL root override: * - * UrlTenancyBootstrapper::$rootUrlOverride = function (Tenant $tenant) { + * RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant) { * $baseUrl = env('APP_URL'); * $scheme = str($baseUrl)->before('://'); * $hostname = str($baseUrl)->after($scheme . '://'); diff --git a/assets/config.php b/assets/config.php index 3dfea4fe..a7374a34 100644 --- a/assets/config.php +++ b/assets/config.php @@ -125,7 +125,9 @@ return [ Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class, // Stancl\Tenancy\Bootstrappers\PrefixCacheTenancyBootstrapper::class, - // Stancl\Tenancy\Bootstrappers\UrlTenancyBootstrapper::class, + // Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::class, + // Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper::class, + // Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteTenancyBootstrapper, // Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper::class, // Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper::class, // Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed diff --git a/src/Bootstrappers/Integrations/FortifyRouteTenancyBootstrapper.php b/src/Bootstrappers/Integrations/FortifyRouteTenancyBootstrapper.php index cf3a082e..d2e5b6ef 100644 --- a/src/Bootstrappers/Integrations/FortifyRouteTenancyBootstrapper.php +++ b/src/Bootstrappers/Integrations/FortifyRouteTenancyBootstrapper.php @@ -7,23 +7,56 @@ namespace Stancl\Tenancy\Bootstrappers\Integrations; use Illuminate\Config\Repository; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Stancl\Tenancy\Enums\Context; +use Stancl\Tenancy\Resolvers\PathTenantResolver; /** - * Allows customizing Fortify redirect URLs. - * Intended to be used with UrlBindingBootstrapper. + * Allows customizing Fortify action redirects + * so that they can also redirect to tenant routes instead of just the central routes. * - * @see \Stancl\Tenancy\Bootstrappers\UrlBindingBootstrapper + * Works with path and query string identification. */ class FortifyRouteTenancyBootstrapper implements TenancyBootstrapper { - // 'fortify_action' => 'tenant_route_name' - public static array $fortifyRedirectTenantMap = [ - // 'logout' => 'welcome', - ]; + /** + * Make Fortify actions redirect to custom routes. + * + * For each route redirect, specify the intended route context (central or tenant). + * Based on the provided context, we pass the tenant parameter to the route (or not). + * The tenant parameter is only passed to the route when you specify its context as tenant. + * + * The route redirects should be in the following format: + * + * 'fortify_action' => [ + * 'route_name' => 'tenant.route', + * 'context' => Context::TENANT, + * ] + * + * For example: + * + * FortifyRouteTenancyBootstrapper::$fortifyRedirectMap = [ + * // On logout, redirect the user to the "bye" route in the central app + * 'logout' => [ + * 'route_name' => 'bye', + * 'context' => Context::CENTRAL, + * ], + * + * // On login, redirect the user to the "welcome" route in the tenant app + * 'login' => [ + * 'route_name' => 'welcome', + * 'context' => Context::TENANT, + * ], + *]; + */ + public static array $fortifyRedirectMap = []; - // Fortify home route name - public static string|null $fortifyHome = 'dashboard'; - protected array|null $originalFortifyConfig = null; + /** + * Tenant route that serves as Fortify's home (e.g. a tenant dashboard route). + * This route will always receive the tenant parameter. + */ + public static string $fortifyHome = 'tenant.dashboard'; + + protected array $originalFortifyConfig = []; public function __construct( protected Repository $config, @@ -32,9 +65,9 @@ class FortifyRouteTenancyBootstrapper implements TenancyBootstrapper public function bootstrap(Tenant $tenant): void { - $this->originalFortifyConfig = $this->config->get('fortify'); + $this->originalFortifyConfig = $this->config->get('fortify') ?? []; - $this->useTenantRoutesInFortify(); + $this->useTenantRoutesInFortify($tenant); } public function revert(): void @@ -42,16 +75,31 @@ class FortifyRouteTenancyBootstrapper implements TenancyBootstrapper $this->config->set('fortify', $this->originalFortifyConfig); } - protected function useTenantRoutesInFortify(): void + protected function useTenantRoutesInFortify(Tenant $tenant): void { - // Regenerate the URLs after the behavior of the route() helper has been modified - // in UrlBindingBootstrapper to generate URLs specific to the current tenant - $tenantRoutes = array_map(fn (string $routeName) => route($routeName), static::$fortifyRedirectTenantMap); + $tenantKey = $tenant->getTenantKey(); + $tenantParameterName = PathTenantResolver::tenantParameterName(); + + $generateLink = function (array $redirect) use ($tenantKey, $tenantParameterName) { + // Specifying the context is only required with query string identification + // because with path identification, the tenant parameter should always present + $passTenantParameter = $redirect['context'] === Context::TENANT; + + // Only pass the tenant parameter when the user should be redirected to a tenant route + return route($redirect['route_name'], $passTenantParameter ? [$tenantParameterName => $tenantKey] : []); + }; + + // Get redirect URLs for the configured redirect routes + $redirects = array_merge( + $this->originalFortifyConfig['redirects'] ?? [], // Fortify config redirects + array_map(fn (array $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects + ); if (static::$fortifyHome) { - $this->config->set('fortify.home', route(static::$fortifyHome)); + // Generate the home route URL with the tenant parameter and make it the Fortify home route + $this->config->set('fortify.home', route(static::$fortifyHome, [$tenantParameterName => $tenantKey])); } - $this->config->set('fortify.redirects', array_merge($this->config->get('fortify.redirects') ?? [], $tenantRoutes)); + $this->config->set('fortify.redirects', $redirects); } } diff --git a/src/Bootstrappers/UrlTenancyBootstrapper.php b/src/Bootstrappers/RootUrlBootstrapper.php similarity index 94% rename from src/Bootstrappers/UrlTenancyBootstrapper.php rename to src/Bootstrappers/RootUrlBootstrapper.php index db27c8c5..c623667d 100644 --- a/src/Bootstrappers/UrlTenancyBootstrapper.php +++ b/src/Bootstrappers/RootUrlBootstrapper.php @@ -10,7 +10,7 @@ use Illuminate\Contracts\Routing\UrlGenerator; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -class UrlTenancyBootstrapper implements TenancyBootstrapper +class RootUrlBootstrapper implements TenancyBootstrapper { public static Closure|null $rootUrlOverride = null; protected string|null $originalRootUrl = null; diff --git a/src/Bootstrappers/UrlBindingBootstrapper.php b/src/Bootstrappers/UrlGeneratorBootstrapper.php similarity index 93% rename from src/Bootstrappers/UrlBindingBootstrapper.php rename to src/Bootstrappers/UrlGeneratorBootstrapper.php index db29a925..73ec1f0b 100644 --- a/src/Bootstrappers/UrlBindingBootstrapper.php +++ b/src/Bootstrappers/UrlGeneratorBootstrapper.php @@ -21,7 +21,7 @@ use Stancl\Tenancy\Overrides\TenancyUrlGenerator; * @see TenancyUrlGenerator * @see \Stancl\Tenancy\Resolvers\PathTenantResolver */ -class UrlBindingBootstrapper implements TenancyBootstrapper +class UrlGeneratorBootstrapper implements TenancyBootstrapper { public function __construct( protected Application $app, @@ -55,6 +55,8 @@ class UrlBindingBootstrapper implements TenancyBootstrapper $app['config']->get('app.asset_url'), ); + $newGenerator->defaults($urlGenerator->getDefaultParameters()); + $newGenerator->setSessionResolver(function () { return $this->app['session'] ?? null; }); diff --git a/src/Concerns/DealsWithEarlyIdentification.php b/src/Concerns/DealsWithRouteContexts.php similarity index 98% rename from src/Concerns/DealsWithEarlyIdentification.php rename to src/Concerns/DealsWithRouteContexts.php index 0fa87950..4246fcb3 100644 --- a/src/Concerns/DealsWithEarlyIdentification.php +++ b/src/Concerns/DealsWithRouteContexts.php @@ -13,8 +13,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Route as RouteFacade; use Stancl\Tenancy\Enums\RouteMode; -// todo1 Name – maybe DealsWithMiddlewareContexts? -trait DealsWithEarlyIdentification +trait DealsWithRouteContexts { /** * Get route's middleware context (tenant, central or universal). diff --git a/src/Listeners/ForgetTenantParameter.php b/src/Listeners/ForgetTenantParameter.php index be8024ba..19738615 100644 --- a/src/Listeners/ForgetTenantParameter.php +++ b/src/Listeners/ForgetTenantParameter.php @@ -11,12 +11,13 @@ use Stancl\Tenancy\PathIdentificationManager; /** * Remove the tenant parameter from the matched route when path identification is used globally. * - * The tenant parameter gets forgotten using PathTenantResolver so that the route actions don't have to accept it. - * Then, tenancy gets initialized, and URL::defaults() is used to give the tenant parameter to the next matched route. - * But with kernel identification, the route gets matched AFTER the point when URL::defaults() is used, - * and because of that, the matched route gets the tenant parameter again, so we forget the parameter again on RouteMatched. + * While initializing tenancy, we forget the tenant parameter (in PathTenantResolver), + * so that the route actions don't have to accept it. * - * We remove the {tenant} parameter from the hydrated route when + * With kernel identification, tenancy gets initialized before the route gets matched. + * The matched route gets the tenant parameter again, so we have to forget the parameter again on RouteMatched. + * + * We remove the {tenant} parameter from the matched route when * 1) the InitializeTenancyByPath middleware is in the global stack, AND * 2) the matched route does not have identification middleware (so that {tenant} isn't forgotten when using route-level identification), AND * 3) the route isn't in the central context (so that {tenant} doesn't get accidentally removed from central routes). diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 60b5ab7e..3a149322 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -6,13 +6,9 @@ namespace Stancl\Tenancy\Middleware; use Closure; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\URL; use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification; use Stancl\Tenancy\Concerns\UsableWithUniversalRoutes; -use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Enums\RouteMode; -use Stancl\Tenancy\Events\InitializingTenancy; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Overrides\TenancyUrlGenerator; use Stancl\Tenancy\Resolvers\PathTenantResolver; @@ -49,8 +45,6 @@ class InitializeTenancyByPath extends IdentificationMiddleware implements Usable // We don't want to initialize tenancy if the tenant is // simply injected into some route controller action. if (in_array(PathTenantResolver::tenantParameterName(), $route->parameterNames())) { - $this->setDefaultTenantForRouteParametersWhenInitializingTenancy(); - return $this->initializeTenancy( $request, $next, @@ -61,18 +55,6 @@ class InitializeTenancyByPath extends IdentificationMiddleware implements Usable } } - protected function setDefaultTenantForRouteParametersWhenInitializingTenancy(): void - { - Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) { - /** @var Tenant $tenant */ - $tenant = $event->tenancy->tenant; - - URL::defaults([ - PathTenantResolver::tenantParameterName() => $tenant->getTenantKey(), - ]); - }); - } - /** * Path identification request has a tenant if the middleware context is tenant. * diff --git a/src/Overrides/TenancyUrlGenerator.php b/src/Overrides/TenancyUrlGenerator.php index 5566d74e..bc3a1d24 100644 --- a/src/Overrides/TenancyUrlGenerator.php +++ b/src/Overrides/TenancyUrlGenerator.php @@ -9,7 +9,7 @@ use Illuminate\Support\Arr; use Stancl\Tenancy\PathIdentificationManager; /** - * This class is used in place of the default UrlGenerator when UrlBindingBootstrapper is enabled. + * This class is used in place of the default UrlGenerator when UrlGeneratorBootstrapper is enabled. * * TenancyUrlGenerator does two extra things: * 1. Autofill the {tenant} parameter in the tenant context with the current tenant if $passTenantParameterToRoutes is enabled (enabled by default) @@ -28,16 +28,20 @@ class TenancyUrlGenerator extends UrlGenerator public static string $bypassParameter = 'central'; /** - * Determine if the route names of routes generated using - * `route()` or `temporarySignedRoute()` should get prefixed with the tenant route name prefix. + * Determine if the route names passed to `route()` or `temporarySignedRoute()` + * should get prefixed with the tenant route name prefix. * - * Set this to true when using path identification. + * This is useful when using path identification with packages that generate URLs, + * like Jetstream, so that you don't have to manually prefix route names passed to each route() call. */ public static bool $prefixRouteNames = false; /** * Determine if the tenant parameter should get passed - * to the links generated by `route()` or `temporarySignedRoute()`. + * to the links generated by `route()` or `temporarySignedRoute()` whenever available + * (enabled by default – works with both path and query string identification). + * + * With path identification, you can disable this and use URL::defaults() instead (as an alternative solution). */ public static bool $passTenantParameterToRoutes = true; diff --git a/src/Tenancy.php b/src/Tenancy.php index 8d3a2504..ee37abb5 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -8,14 +8,14 @@ use Closure; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Traits\Macroable; -use Stancl\Tenancy\Concerns\DealsWithEarlyIdentification; +use Stancl\Tenancy\Concerns\DealsWithRouteContexts; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException; class Tenancy { - use Macroable, DealsWithEarlyIdentification; + use Macroable, DealsWithRouteContexts; /** * The current tenant. diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 7491a3bd..d6a8e29b 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -3,10 +3,10 @@ declare(strict_types=1); use Illuminate\Support\Str; +use Stancl\Tenancy\Enums\Context; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\URL; use Stancl\JobPipeline\JobPipeline; -use Illuminate\Broadcasting\Channel; use Illuminate\Support\Facades\File; use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Facades\Cache; @@ -20,9 +20,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Events\TenantDeleted; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Broadcast; use Stancl\Tenancy\Events\DeletingTenant; -use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Broadcasting\BroadcastManager; use Stancl\Tenancy\Events\TenancyInitialized; @@ -30,25 +28,26 @@ use Illuminate\Contracts\Routing\UrlGenerator; use Stancl\Tenancy\Jobs\CreateStorageSymlinks; use Stancl\Tenancy\Jobs\RemoveStorageSymlinks; use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tests\Etc\TestingBroadcaster; use Stancl\Tenancy\Listeners\DeleteTenantStorage; use Stancl\Tenancy\Overrides\TenancyUrlGenerator; use Stancl\Tenancy\Listeners\RevertToCentralContext; +use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper; use Stancl\Tenancy\Overrides\TenancyBroadcastManager; -use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper; -use Stancl\Tenancy\Bootstrappers\UrlBindingBootstrapper; -use Stancl\Tenancy\Bootstrappers\UrlTenancyBootstrapper; +use Illuminate\Routing\Exceptions\UrlGenerationException; use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; -use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; use Stancl\Tenancy\Bootstrappers\PrefixCacheTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteTenancyBootstrapper; beforeEach(function () { @@ -59,7 +58,7 @@ beforeEach(function () { // Reset static properties of classes used in this test file to their default values BroadcastingConfigBootstrapper::$credentialsMap = []; TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably']; - UrlTenancyBootstrapper::$rootUrlOverride = null; + RootUrlBootstrapper::$rootUrlOverride = null; Event::listen( TenantCreated::class, @@ -74,11 +73,12 @@ beforeEach(function () { afterEach(function () { // Reset static properties of classes used in this test file to their default values - UrlTenancyBootstrapper::$rootUrlOverride = null; + RootUrlBootstrapper::$rootUrlOverride = null; PrefixCacheTenancyBootstrapper::$tenantCacheStores = []; TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably']; BroadcastingConfigBootstrapper::$credentialsMap = []; TenancyUrlGenerator::$prefixRouteNames = false; + TenancyUrlGenerator::$passTenantParameterToRoutes = true; }); test('database data is separated', function () { @@ -501,7 +501,7 @@ test('MailTenancyBootstrapper reverts the config and mailer credentials to defau }); test('url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one after tenancy ends', function() { - config(['tenancy.bootstrappers' => [UrlTenancyBootstrapper::class]]); + config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]); Route::group([ 'middleware' => InitializeTenancyBySubdomain::class, @@ -521,7 +521,7 @@ test('url bootstrapper overrides the root url when tenancy gets initialized and return $scheme . '://' . $tenant->getTenantKey() . '.' . $hostname; }; - UrlTenancyBootstrapper::$rootUrlOverride = $rootUrlOverride; + RootUrlBootstrapper::$rootUrlOverride = $rootUrlOverride; $tenant = Tenant::create(); $tenantUrl = $rootUrlOverride($tenant); @@ -546,36 +546,43 @@ test('url bootstrapper overrides the root url when tenancy gets initialized and }); test('url binding tenancy bootstrapper swaps the url generator instance correctly', function() { - config(['tenancy.bootstrappers' => [UrlBindingBootstrapper::class]]); + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); tenancy()->initialize(Tenant::create()); expect(app('url'))->toBeInstanceOf(TenancyUrlGenerator::class); expect(url())->toBeInstanceOf(TenancyUrlGenerator::class); tenancy()->end(); - expect(app('url'))->toBeInstanceOf(UrlGenerator::class); - expect(url())->toBeInstanceOf(UrlGenerator::class); + expect(app('url'))->toBeInstanceOf(UrlGenerator::class) + ->not()->toBeInstanceOf(TenancyUrlGenerator::class); + expect(url())->toBeInstanceOf(UrlGenerator::class) + ->not()->toBeInstanceOf(TenancyUrlGenerator::class); }); -test('url binding tenancy bootstrapper changes route helper behavior correctly', function() { +test('url generator bootstrapper can prefix route names passed to the route helper', function() { Route::get('/central/home', fn () => route('home'))->name('home'); // Tenant route name prefix is 'tenant.' by default Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]); - Route::get('/query-string', fn () => route('query-string'))->name('query-string')->middleware(['tenant', InitializeTenancyByRequestData::class]); $tenant = Tenant::create(); $tenantKey = $tenant->getTenantKey(); $centralRouteUrl = route('home'); $tenantRouteUrl = route('tenant.home', ['tenant' => $tenantKey]); - $queryStringCentralUrl = route('query-string'); - $queryStringTenantUrl = route('query-string', ['tenant' => $tenantKey]); TenancyUrlGenerator::$bypassParameter = 'bypassParameter'; - $bypassParameter = TenancyUrlGenerator::$bypassParameter; - config(['tenancy.bootstrappers' => [UrlBindingBootstrapper::class]]); - TenancyUrlGenerator::$prefixRouteNames = true; + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); tenancy()->initialize($tenant); + + // Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false + expect(route('home'))->not()->toBe($centralRouteUrl); + // When TenancyUrlGenerator::$passTenantParameterToRoutes is true (default) + // The route helper receives the tenant parameter + // So in order to generate central URL, we have to pass the bypass parameter + expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl); + + + TenancyUrlGenerator::$prefixRouteNames = true; // The $prefixRouteNames property is true // The route name passed to the route() helper ('home') gets prefixed prefixed with 'tenant.' automatically expect(route('home'))->toBe($tenantRouteUrl); @@ -584,58 +591,149 @@ test('url binding tenancy bootstrapper changes route helper behavior correctly', // Also, the route receives the tenant parameter automatically expect(route('tenant.home'))->toBe($tenantRouteUrl); - // The $bypassParameter parameter ('central' by default) can bypass the route name prefixing - // When the bypass parameter is true, the generated route URL points to the route named 'home' - // Also, check if the bypass parameter gets removed from the generated URL query string - expect(route('home', [$bypassParameter => true]))->toBe($centralRouteUrl) - ->not()->toContain($bypassParameter); - // When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home') - expect(route('home', [$bypassParameter => false]))->toBe($tenantRouteUrl) - ->not()->toContain($bypassParameter); - - TenancyUrlGenerator::$prefixRouteNames = false; - // Route names don't get prefixed – TenancyUrlGenerator::$prefixRouteNames is false - expect(route('home', [$bypassParameter => true]))->toBe($centralRouteUrl); - expect(route('query-string'))->toBe($queryStringTenantUrl); - - TenancyUrlGenerator::$passTenantParameterToRoutes = false; - expect(route('query-string'))->toBe($queryStringCentralUrl); - - TenancyUrlGenerator::$passTenantParameterToRoutes = true; - expect(route('query-string'))->toBe($queryStringTenantUrl); - // Ending tenancy reverts route() behavior changes tenancy()->end(); expect(route('home'))->toBe($centralRouteUrl); - expect(route('query-string'))->toBe($queryStringCentralUrl); - expect(route('tenant.home', ['tenant' => $tenantKey]))->toBe($tenantRouteUrl); +}); + +test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () { + $tenantParameterName = PathTenantResolver::tenantParameterName(); + + Route::get('/central/home', fn () => route('home'))->name('home'); + // Tenant route name prefix is 'tenant.' by default + Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]); + + $tenant = Tenant::create(); + $centralRouteUrl = route('home'); + $tenantRouteUrl = route('tenant.home', ['tenant' => $tenant->getTenantKey()]); + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + TenancyUrlGenerator::$prefixRouteNames = true; + TenancyUrlGenerator::$bypassParameter = 'bypassParameter'; + + tenancy()->initialize($tenant); + + // The $bypassParameter parameter ('central' by default) can bypass the route name prefixing + // When the bypass parameter is true, the generated route URL points to the route named 'home' + expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl) + // Bypass parameter prevents passing the tenant parameter directly + ->not()->toContain($tenantParameterName . '=') + // Bypass parameter gets removed from the generated URL automatically + ->not()->toContain('bypassParameter'); + + // When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home') + expect(route('home', ['bypassParameter' => false]))->toBe($tenantRouteUrl) + ->not()->toContain('bypassParameter'); +}); + +test('url generator bootstrapper can make route helper generate links with the tenant parameter', function() { + Route::get('/query_string', fn () => route('query_string'))->name('query_string')->middleware(['universal', InitializeTenancyByRequestData::class]); + Route::get('/path', fn () => route('path'))->name('path'); + Route::get('/{tenant}/path', fn () => route('tenant.path'))->name('tenant.path')->middleware([InitializeTenancyByPath::class]); + + $tenant = Tenant::create(); + $tenantKey = $tenant->getTenantKey(); + $queryStringCentralUrl = route('query_string'); + $queryStringTenantUrl = route('query_string', ['tenant' => $tenantKey]); + $pathCentralUrl = route('path'); + $pathTenantUrl = route('tenant.path', ['tenant' => $tenantKey]); + + // Makes the route helper receive the tenant parameter whenever available + // Unless the bypass parameter is true + TenancyUrlGenerator::$passTenantParameterToRoutes = true; + + TenancyUrlGenerator::$bypassParameter = 'bypassParameter'; + + config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); + + expect(route('path'))->toBe($pathCentralUrl); + // Tenant parameter required, but not passed since tenancy wasn't initialized + expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class); + + tenancy()->initialize($tenant); + + // Tenant parameter is passed automatically + expect(route('path'))->not()->toBe($pathCentralUrl); // Parameter added as query string – bypassParameter needed + expect(route('path', ['bypassParameter' => true]))->toBe($pathCentralUrl); + expect(route('tenant.path'))->toBe($pathTenantUrl); + + expect(route('query_string'))->toBe($queryStringTenantUrl)->toContain('tenant='); + expect(route('query_string', ['bypassParameter' => 'true']))->toBe($queryStringCentralUrl)->not()->toContain('tenant='); + + tenancy()->end(); + + expect(route('query_string'))->toBe($queryStringCentralUrl); + + // Tenant parameter required, but shouldn't be passed since tenancy isn't initialized + expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class); // Route-level identification - pest()->get("http://localhost/central/home")->assertSee($centralRouteUrl); - pest()->get("http://localhost/$tenantKey/home")->assertSee($tenantRouteUrl); - pest()->get("http://localhost/query-string?tenant=$tenantKey")->assertSee($queryStringTenantUrl); + pest()->get("http://localhost/query_string")->assertSee($queryStringCentralUrl); + pest()->get("http://localhost/query_string?tenant=$tenantKey")->assertSee($queryStringTenantUrl); + pest()->get("http://localhost/path")->assertSee($pathCentralUrl); + pest()->get("http://localhost/$tenantKey/path")->assertSee($pathTenantUrl); }); test('fortify route tenancy bootstrapper updates fortify config correctly', function() { config(['tenancy.bootstrappers' => [FortifyRouteTenancyBootstrapper::class]]); - Route::get('/', function () { - return true; - })->name($tenantHomeRouteName = 'tenant.home'); - - FortifyRouteTenancyBootstrapper::$fortifyHome = $tenantHomeRouteName; - FortifyRouteTenancyBootstrapper::$fortifyRedirectTenantMap = ['logout' => FortifyRouteTenancyBootstrapper::$fortifyHome]; $originalFortifyHome = config('fortify.home'); $originalFortifyRedirects = config('fortify.redirects'); - tenancy()->initialize(Tenant::create()); - expect(config('fortify.home'))->toBe($homeUrl = route($tenantHomeRouteName)); - expect(config('fortify.redirects'))->toBe(['logout' => $homeUrl]); + Route::get('/home', function () { + return true; + })->name($homeRouteName = 'home'); + + Route::get('/{tenant}/home', function () { + return true; + })->name($pathIdHomeRouteName = 'tenant.home'); + + Route::get('/welcome', function () { + return true; + })->name($welcomeRouteName = 'welcome'); + + Route::get('/{tenant}/welcome', function () { + return true; + })->name($pathIdWelcomeRouteName = 'path.welcome'); + + FortifyRouteTenancyBootstrapper::$fortifyHome = $homeRouteName; + + // Make login redirect to the central welcome route + FortifyRouteTenancyBootstrapper::$fortifyRedirectMap['login'] = [ + 'route_name' => $welcomeRouteName, + 'context' => Context::CENTRAL, + ]; + + tenancy()->initialize($tenant = Tenant::create()); + // The bootstraper makes fortify.home always receive the tenant parameter + expect(config('fortify.home'))->toBe('http://localhost/home?tenant=' . $tenant->getTenantKey()); + + // The login redirect route has the central context specified, so it doesn't receive the tenant parameter + expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome']); tenancy()->end(); expect(config('fortify.home'))->toBe($originalFortifyHome); expect(config('fortify.redirects'))->toBe($originalFortifyRedirects); + + // Making a route's context will pass the tenant parameter to the route + FortifyRouteTenancyBootstrapper::$fortifyRedirectMap['login']['context'] = Context::TENANT; + + tenancy()->initialize($tenant); + + expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]); + + // Make the home and login route accept the tenant as a route parameter + // To confirm that tenant route parameter gets filled automatically too (path identification works as well as query string) + FortifyRouteTenancyBootstrapper::$fortifyHome = $pathIdHomeRouteName; + FortifyRouteTenancyBootstrapper::$fortifyRedirectMap['login']['route_name'] = $pathIdWelcomeRouteName; + + tenancy()->end(); + + tenancy()->initialize($tenant); + + expect(config('fortify.home'))->toBe("http://localhost/{$tenant->getTenantKey()}/home"); + expect(config('fortify.redirects'))->toEqual(['login' => "http://localhost/{$tenant->getTenantKey()}/welcome"]); }); test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) { diff --git a/tests/EarlyIdentificationTest.php b/tests/EarlyIdentificationTest.php index 6dc0aafb..532b6995 100644 --- a/tests/EarlyIdentificationTest.php +++ b/tests/EarlyIdentificationTest.php @@ -116,10 +116,6 @@ test('early identification works with path identification', function (bool $useK ->assertOk() ->assertContent($tenantPost->title . '-' . $tenantComment->comment); assertTenancyInitializedInEarlyIdentificationRequest(); - - // Tenant routes that use path identification receive the tenant parameter automatically - // (setDefaultTenantForRouteParametersWhenInitializingTenancy() in Stancl\Tenancy\Middleware\InitializeTenancyByPath) - expect(route('tenant-route'))->toBe(route('tenant-route', ['tenant' => $tenant->getTenantKey()])); })->with([ 'route-level identification' => false, 'kernel identification' => true, diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index f1998ce4..c0819e0b 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -129,26 +129,6 @@ test('tenant parameter name can be customized', function () { ->get('/acme/foo/abc/xyz'); }); -test('tenant parameter is set for all routes as the default parameter once the tenancy initialized', function () { - Tenant::create([ - 'id' => 'acme', - ]); - - expect(tenancy()->initialized)->toBeFalse(); - - // make a request that will initialize tenancy - pest()->get(route('foo', ['tenant' => 'acme', 'a' => 1, 'b' => 2])); - - expect(tenancy()->initialized)->toBeTrue(); - expect(tenant('id'))->toBe('acme'); - - // assert that the route WITHOUT the tenant parameter matches the route WITH the tenant parameter - expect(route('baz', ['a' => 1, 'b' => 2]))->toBe(route('baz', ['tenant' => 'acme', 'a' => 1, 'b' => 2])); - - expect(route('baz', ['a' => 1, 'b' => 2]))->toBe('http://localhost/acme/baz/1/2'); // assert the full route string - pest()->get(route('baz', ['a' => 1, 'b' => 2]))->assertOk(); // Assert route don't need tenant parameter -}); - test('tenant parameter does not have to be the first in order to initialize tenancy', function() { Tenant::create([ 'id' => $tenantId = 'another-tenant', diff --git a/tests/ResourceSyncingUsingPolymorphicTest.php b/tests/ResourceSyncingUsingPolymorphicTest.php index 5af56ec3..f3d5b30a 100644 --- a/tests/ResourceSyncingUsingPolymorphicTest.php +++ b/tests/ResourceSyncingUsingPolymorphicTest.php @@ -43,9 +43,6 @@ beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); - - // todo1 Is this cleanup needed? - UpdateSyncedResource::$shouldQueue = false; // Global state cleanup Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class); // Run migrations on central connection diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 94e733b8..3f8bd8e4 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -13,7 +13,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\UrlBindingBootstrapper; +use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper; use Stancl\Tenancy\Overrides\TenancyUrlGenerator; beforeEach(function () { @@ -22,6 +22,7 @@ beforeEach(function () { ]]); TenancyUrlGenerator::$prefixRouteNames = false; + TenancyUrlGenerator::$passTenantParameterToRoutes = true; /** @var CloneRoutesAsTenant $cloneAction */ $cloneAction = app(CloneRoutesAsTenant::class); @@ -78,9 +79,11 @@ test('asset helper returns a link to an external url when asset url is not null' test('asset helper works correctly with path identification', function (bool $kernelIdentification) { TenancyUrlGenerator::$prefixRouteNames = true; + TenancyUrlGenerator::$passTenantParameterToRoutes = true; + config(['tenancy.filesystem.asset_helper_tenancy' => true]); config(['tenancy.identification.default_middleware' => InitializeTenancyByPath::class]); - config(['tenancy.bootstrappers' => array_merge([UrlBindingBootstrapper::class], config('tenancy.bootstrappers'))]); + config(['tenancy.bootstrappers' => array_merge([UrlGeneratorBootstrapper::class], config('tenancy.bootstrappers'))]); $tenantAssetRoute = Route::prefix('{tenant}')->get('/tenant_helper', function () { return tenant_asset('foo'); diff --git a/tests/TestCase.php b/tests/TestCase.php index 854b6da3..f33a2803 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -14,9 +14,10 @@ use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Facades\GlobalCache; use Stancl\Tenancy\TenancyServiceProvider; use Stancl\Tenancy\Facades\Tenancy as TenancyFacade; -use Stancl\Tenancy\Bootstrappers\UrlTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper; use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; @@ -113,7 +114,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'tenancy.bootstrappers' => [ DatabaseTenancyBootstrapper::class, FilesystemTenancyBootstrapper::class, - UrlTenancyBootstrapper::class, + RootUrlBootstrapper::class, ], 'queue.connections.central' => [ 'driver' => 'sync', @@ -128,7 +129,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase $app->singleton(BroadcastingConfigBootstrapper::class); $app->singleton(BroadcastChannelPrefixBootstrapper::class); $app->singleton(MailTenancyBootstrapper::class); - $app->singleton(UrlTenancyBootstrapper::class); + $app->singleton(RootUrlBootstrapper::class); + $app->singleton(UrlGeneratorBootstrapper::class); } protected function getPackageProviders($app)