diff --git a/resources/config/multitenancy.php b/resources/config/multitenancy.php index 63cc94e..b609326 100644 --- a/resources/config/multitenancy.php +++ b/resources/config/multitenancy.php @@ -54,6 +54,11 @@ 'driver' => 'cookie', ], + 'session' => [ + 'driver' => 'session', + 'session' => 'multitenancy.{tenancy}', + ], + ], ]; diff --git a/src/Http/Resolvers/SessionIdentityResolver.php b/src/Http/Resolvers/SessionIdentityResolver.php new file mode 100644 index 0000000..1818059 --- /dev/null +++ b/src/Http/Resolvers/SessionIdentityResolver.php @@ -0,0 +1,111 @@ +session = $session ?? 'multitenancy.{tenancy}'; + } + + public function getSession(): string + { + return $this->session; + } + + /** + * @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy + * + * @return string + */ + public function getRequestSessionName(Tenancy $tenancy): string + { + return str_replace( + ['{tenancy}', '{resolver}', '{Tenancy}', '{Resolver}'], + [$tenancy->getName(), $this->getName(), ucfirst($tenancy->getName()), ucfirst($this->getName())], + $this->getSession() + ); + } + + /** + * Get an identifier from the request + * + * Locates a tenant identifier within the provided request and returns it. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Illuminate\Http\Request $request + * @param \Sprout\Contracts\Tenancy $tenancy + * + * @return string|null + */ + public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string + { + /** + * This is unfortunately here because of the ludicrous return type + * + * @var string|null $identity + */ + $identity = $request->session()->get($this->getRequestSessionName($tenancy)); + + return $identity; + } + + /** + * Create a route group for the resolver + * + * Creates and configures a route group with the necessary settings to + * support identity resolution. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Illuminate\Routing\Router $router + * @param \Closure $groupRoutes + * @param \Sprout\Contracts\Tenancy $tenancy + * + * @return \Illuminate\Routing\RouteRegistrar + */ + public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): RouteRegistrar + { + return $router->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]) + ->group($groupRoutes); + } + + /** + * Can the resolver run on the request + * + * This method allows a resolver to prevent resolution with the request in + * its current state, whether that means it's too early, or too late. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Illuminate\Http\Request $request + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Support\ResolutionHook $hook + * + * @return bool + */ + public function canResolve(Request $request, Tenancy $tenancy, ResolutionHook $hook): bool + { + return $request->hasSession() && $hook === ResolutionHook::Middleware; + } +} diff --git a/src/Managers/IdentityResolverManager.php b/src/Managers/IdentityResolverManager.php index e343b8e..4bed29b 100644 --- a/src/Managers/IdentityResolverManager.php +++ b/src/Managers/IdentityResolverManager.php @@ -132,4 +132,23 @@ protected function createCookieResolver(array $config, string $name): CookieIden $config['hooks'] ?? [] ); } + + /** + * Create the session identity resolver + * + * @param array $config + * @param string $name + * + * @phpstan-param array{session?: string|null, hooks?: array<\Sprout\Support\ResolutionHook>} $config + * + * @return \Sprout\Http\Resolvers\SessionIdentityResolver + */ + protected function createSessionResolver(array $config, string $name): SessionIdentityResolver + { + + return new SessionIdentityResolver( + $name, + $config['session'] ?? null + ); + } } diff --git a/tests/Resolvers/SessionResolverTest.php b/tests/Resolvers/SessionResolverTest.php new file mode 100644 index 0000000..ad8a22b --- /dev/null +++ b/tests/Resolvers/SessionResolverTest.php @@ -0,0 +1,78 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + $config->set('multitenancy.defaults.resolver', 'session'); + $config->set('multitenancy.resolvers.session', [ + 'driver' => 'session', + 'session' => 'multitenancy.{tenancy}', + ]); + }); + } + + protected function defineRoutes($router) + { + $router->middleware(StartSession::class)->group(function (Router $router) { + $router->get('/', function () { + return 'no'; + }); + + $router->tenanted(function (Router $router) { + $router->get('/session-route', function (#[CurrentTenant] Tenant $tenant) { + return $tenant->getTenantKey(); + })->name('session.route'); + }, 'session', 'tenants'); + }); + } + + #[Test] + public function resolvesFromRoute(): void + { + $tenant = TenantModel::first(); + + $result = $this->withSession(['multitenancy' => ['tenants' => $tenant->getTenantIdentifier()]])->get(route('session.route')); + + $result->assertOk(); + $result->assertContent((string)$tenant->getTenantKey()); + $result->assertSessionHas('multitenancy.tenants', $tenant->getTenantIdentifier()); + } + + #[Test] + public function throwsExceptionForInvalidTenant(): void + { + $result = $this->withSession(['multitenancy' => ['tenants' => 'i-am-not-real']])->get(route('session.route')); + $result->assertInternalServerError(); + } + + #[Test] + public function throwsExceptionWithoutHeader(): void + { + $result = $this->get(route('session.route')); + + $result->assertInternalServerError(); + } +}