Skip to content

Commit

Permalink
feat(resolvers): Add the session and cookie identity resolvers (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
ollieread authored Sep 10, 2024
2 parents dcf982c + 78f5060 commit 8f30155
Show file tree
Hide file tree
Showing 17 changed files with 703 additions and 90 deletions.
11 changes: 10 additions & 1 deletion resources/config/multitenancy.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@
],

'header' => [
'driver' => 'header',
'driver' => 'header',
],

'cookie' => [
'driver' => 'cookie',
],

'session' => [
'driver' => 'session',
'session' => 'multitenancy.{tenancy}',
],

],
Expand Down
17 changes: 17 additions & 0 deletions src/Contracts/IdentityResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
use Illuminate\Routing\RouteRegistrar;
use Sprout\Support\ResolutionHook;

/**
* Identity Resolver Contract
Expand Down Expand Up @@ -73,4 +74,20 @@ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy):
* @return void
*/
public function setup(Tenancy $tenancy, ?Tenant $tenant): void;

/**
* 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<TenantClass> $tenancy
* @param \Sprout\Support\ResolutionHook $hook
*
* @return bool
*/
public function canResolve(Request $request, Tenancy $tenancy, ResolutionHook $hook): bool;
}
18 changes: 18 additions & 0 deletions src/Contracts/Tenancy.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Sprout\Contracts;

use Sprout\Support\ResolutionHook;

/**
* Tenancy Contract
*
Expand Down Expand Up @@ -108,13 +110,29 @@ public function provider(): TenantProvider;
*/
public function resolvedVia(IdentityResolver $resolver): static;

/**
* Set the hook where the tenant was resolved
*
* @param \Sprout\Support\ResolutionHook $hook
*
* @return $this
*/
public function resolvedAt(ResolutionHook $hook): static;

/**
* Get the used identity resolver
*
* @return \Sprout\Contracts\IdentityResolver|null
*/
public function resolver(): ?IdentityResolver;

/**
* Get the hook where the tenant was resolved
*
* @return \Sprout\Support\ResolutionHook|null
*/
public function hook(): ?ResolutionHook;

/**
* Check if the current tenant was resolved
*
Expand Down
19 changes: 8 additions & 11 deletions src/Http/Middleware/TenantRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use Sprout\Managers\IdentityResolverManager;
use Sprout\Managers\TenancyManager;
use Sprout\Sprout;
use Sprout\Support\ResolutionHelper;
use Sprout\Support\ResolutionHook;

/**
* Tenant Routes Middleware
Expand Down Expand Up @@ -51,17 +53,12 @@ public function handle(Request $request, Closure $next, string ...$options): Res
$resolverName = $tenancyName = null;
}

$resolver = $this->sprout->resolvers()->get($resolverName);
$tenancy = $this->sprout->tenancies()->get($tenancyName);

/**
* @var \Sprout\Contracts\IdentityResolver $resolver
* @var \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy
*/

if (! $tenancy->check()) {
throw NoTenantFound::make($resolver->getName(), $tenancy->getName());
}
ResolutionHelper::handleResolution(
$request,
ResolutionHook::Middleware,
$resolverName,
$tenancyName
);

// TODO: Decide whether to do anything with the following conditions
//if (! $tenancy->wasResolved()) {
Expand Down
161 changes: 161 additions & 0 deletions src/Http/Resolvers/CookieIdentityResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);

namespace Sprout\Http\Resolvers;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Router;
use Illuminate\Routing\RouteRegistrar;
use Illuminate\Support\Facades\Cookie;
use Sprout\Contracts\IdentityResolverTerminates;
use Sprout\Contracts\Tenancy;
use Sprout\Http\Middleware\TenantRoutes;
use Sprout\Support\BaseIdentityResolver;

final class CookieIdentityResolver extends BaseIdentityResolver implements IdentityResolverTerminates
{
private string $cookie;

/**
* @var array<string, mixed>
*/
private array $options;

/**
* @param string $name
* @param string|null $cookie
* @param array<string, mixed> $options
* @param array<\Sprout\Support\ResolutionHook> $hooks
*/
public function __construct(string $name, ?string $cookie = null, array $options = [], array $hooks = [])
{
parent::__construct($name, $hooks);

$this->cookie = $cookie ?? '{Tenancy}-Identifier';
$this->options = $options;
}

public function getCookie(): string
{
return $this->cookie;
}

/**
* @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy
*
* @return string
*/
public function getRequestCookieName(Tenancy $tenancy): string
{
return str_replace(
['{tenancy}', '{resolver}', '{Tenancy}', '{Resolver}'],
[$tenancy->getName(), $this->getName(), ucfirst($tenancy->getName()), ucfirst($this->getName())],
$this->getCookie()
);
}

/**
* 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<TenantClass> $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 $cookie
*/
$cookie = $request->cookie($this->getRequestCookieName($tenancy));

return $cookie;
}

/**
* 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<TenantClass> $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);
}

/**
* @param \Sprout\Contracts\Tenancy<\Sprout\Contracts\Tenant> $tenancy
* @param \Illuminate\Http\Response $response
*
* @return void
*/
public function terminate(Tenancy $tenancy, Response $response): void
{
if ($tenancy->check()) {
/**
* @var array{name:string, value:string} $details
*/
$details = $this->getCookieDetails(
[
'name' => $this->getRequestCookieName($tenancy),
'value' => $tenancy->identifier(),
]
);

$response->withCookie(Cookie::make(...$details));
}
}

/**
* @param array<string, mixed> $details
*
* @return array<string, mixed>
*
* @codeCoverageIgnore
*/
private function getCookieDetails(array $details): array
{
if (isset($this->options['minutes'])) {
$details['minutes'] = $this->options['minutes'];
}

if (isset($this->options['path'])) {
$details['path'] = $this->options['path'];
}

if (isset($this->options['domain'])) {
$details['domain'] = $this->options['domain'];
}

if (isset($this->options['secure'])) {
$details['secure'] = $this->options['secure'];
}

if (isset($this->options['httpOnly'])) {
$details['httpOnly'] = $this->options['httpOnly'];
}

if (isset($this->options['sameSite'])) {
$details['sameSite'] = $this->options['sameSite'];
}

return $details;
}
}
4 changes: 2 additions & 2 deletions src/Http/Resolvers/HeaderIdentityResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class HeaderIdentityResolver extends BaseIdentityResolver
{
private string $header;

public function __construct(string $name, ?string $header = null)
public function __construct(string $name, ?string $header = null, array $hooks = [])
{
parent::__construct($name);
parent::__construct($name, $hooks);

$this->header = $header ?? '{Tenancy}-Identifier';
}
Expand Down
4 changes: 2 additions & 2 deletions src/Http/Resolvers/PathIdentityResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ final class PathIdentityResolver extends BaseIdentityResolver implements Identit

private int $segment = 1;

public function __construct(string $name, ?int $segment = null, ?string $pattern = null, ?string $parameter = null)
public function __construct(string $name, ?int $segment = null, ?string $pattern = null, ?string $parameter = null, array $hooks = [])
{
parent::__construct($name);
parent::__construct($name, $hooks);

if ($segment !== null) {
$this->segment = max(1, $segment);
Expand Down
Loading

0 comments on commit 8f30155

Please sign in to comment.