Skip to content

Commit

Permalink
feat(resolvers): Add session-based identity resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
ollieread committed Sep 10, 2024
1 parent 933d630 commit e0757d5
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 0 deletions.
5 changes: 5 additions & 0 deletions resources/config/multitenancy.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
'driver' => 'cookie',
],

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

],

];
111 changes: 111 additions & 0 deletions src/Http/Resolvers/SessionIdentityResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);

namespace Sprout\Http\Resolvers;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
use Illuminate\Routing\RouteRegistrar;
use Sprout\Contracts\Tenancy;
use Sprout\Http\Middleware\TenantRoutes;
use Sprout\Support\BaseIdentityResolver;
use Sprout\Support\ResolutionHook;

final class SessionIdentityResolver extends BaseIdentityResolver
{
private string $session;

/**
* @param string $name
* @param string|null $session
*/
public function __construct(string $name, ?string $session = null)
{
parent::__construct($name, [ResolutionHook::Middleware]);

$this->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<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 $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<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);
}

/**
* 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
{
return $request->hasSession() && $hook === ResolutionHook::Middleware;
}
}
19 changes: 19 additions & 0 deletions src/Managers/IdentityResolverManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,23 @@ protected function createCookieResolver(array $config, string $name): CookieIden
$config['hooks'] ?? []
);
}

/**
* Create the session identity resolver
*
* @param array<string, mixed> $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
);
}
}
78 changes: 78 additions & 0 deletions tests/Resolvers/SessionResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);

namespace Sprout\Tests\Resolvers;

use Illuminate\Config\Repository;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Router;
use Illuminate\Session\Middleware\StartSession;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Sprout\Attributes\CurrentTenant;
use Sprout\Contracts\Tenant;
use Workbench\App\Models\TenantModel;

#[Group('resolvers'), Group('sessions')]
class SessionResolverTest extends TestCase
{
use WithWorkbench, RefreshDatabase;

protected $enablesPackageDiscoveries = true;

protected function defineEnvironment($app): void
{
tap($app['config'], static function (Repository $config) {
$config->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();
}
}

0 comments on commit e0757d5

Please sign in to comment.