Skip to content

Commit a54d529

Browse files
committed
perf(auth): memoize current authenticatable
1 parent efe6c32 commit a54d529

2 files changed

Lines changed: 213 additions & 7 deletions

File tree

packages/auth/src/Authentication/SessionAuthenticator.php

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,52 @@
77
use Tempest\Http\Session\Session;
88
use Tempest\Http\Session\SessionManager;
99

10-
final readonly class SessionAuthenticator implements Authenticator
10+
final class SessionAuthenticator implements Authenticator
1111
{
1212
public const string AUTHENTICATABLE_KEY = '#authenticatable:id';
1313

1414
public const string AUTHENTICATABLE_CLASS = '#authenticatable:class';
1515

16+
private bool $currentResolved = false;
17+
18+
private int|string|null $currentId = null;
19+
20+
private ?string $currentClass = null;
21+
22+
private ?Authenticatable $current = null;
23+
1624
public function __construct(
17-
private SessionManager $sessionManager,
18-
private Session $session,
19-
private AuthenticatableResolver $authenticatableResolver,
25+
private readonly SessionManager $sessionManager,
26+
private readonly Session $session,
27+
private readonly AuthenticatableResolver $authenticatableResolver,
2028
) {}
2129

2230
public function authenticate(Authenticatable $authenticatable): void
2331
{
32+
$id = $this->authenticatableResolver->resolveId($authenticatable);
33+
$class = $authenticatable::class;
34+
2435
$this->session->set(
2536
key: self::AUTHENTICATABLE_CLASS,
26-
value: $authenticatable::class,
37+
value: $class,
2738
);
2839

2940
$this->session->set(
3041
key: self::AUTHENTICATABLE_KEY,
31-
value: $this->authenticatableResolver->resolveId($authenticatable),
42+
value: $id,
3243
);
44+
45+
$this->currentId = $id;
46+
$this->currentClass = $class;
47+
$this->current = $authenticatable;
48+
$this->currentResolved = true;
3349
}
3450

3551
public function deauthenticate(): void
3652
{
3753
$this->session->remove(self::AUTHENTICATABLE_KEY);
3854
$this->session->remove(self::AUTHENTICATABLE_CLASS);
55+
$this->clearCurrent();
3956

4057
$this->sessionManager->save($this->session);
4158
}
@@ -46,9 +63,28 @@ public function current(): ?Authenticatable
4663
$class = $this->session->get(self::AUTHENTICATABLE_CLASS);
4764

4865
if (! $id || ! $class) {
66+
$this->clearCurrent();
67+
4968
return null;
5069
}
5170

52-
return $this->authenticatableResolver->resolve($id, $class);
71+
if ($this->currentResolved && $this->currentId === $id && $this->currentClass === $class) {
72+
return $this->current;
73+
}
74+
75+
$this->currentId = $id;
76+
$this->currentClass = $class;
77+
$this->current = $this->authenticatableResolver->resolve($id, $class);
78+
$this->currentResolved = true;
79+
80+
return $this->current;
81+
}
82+
83+
private function clearCurrent(): void
84+
{
85+
$this->currentResolved = false;
86+
$this->currentId = null;
87+
$this->currentClass = null;
88+
$this->current = null;
5389
}
5490
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Auth\Tests;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
use Tempest\Auth\Authentication\Authenticatable;
10+
use Tempest\Auth\Authentication\AuthenticatableResolver;
11+
use Tempest\Auth\Authentication\SessionAuthenticator;
12+
use Tempest\DateTime\DateTime;
13+
use Tempest\Http\Session\Session;
14+
use Tempest\Http\Session\SessionId;
15+
use Tempest\Http\Session\SessionManager;
16+
17+
final class SessionAuthenticatorTest extends TestCase
18+
{
19+
#[Test]
20+
public function current_memoizes_the_resolved_authenticatable_for_the_current_session_identity(): void
21+
{
22+
$authenticatable = new MemoizedAuthenticatable(id: 1);
23+
$resolver = new CountingAuthenticatableResolver($authenticatable);
24+
$session = $this->createSession();
25+
$session->set(SessionAuthenticator::AUTHENTICATABLE_KEY, 1);
26+
$session->set(SessionAuthenticator::AUTHENTICATABLE_CLASS, MemoizedAuthenticatable::class);
27+
28+
$authenticator = new SessionAuthenticator(
29+
sessionManager: new TestingSessionManager(),
30+
session: $session,
31+
authenticatableResolver: $resolver,
32+
);
33+
34+
$this->assertSame($authenticatable, $authenticator->current());
35+
$this->assertSame($authenticatable, $authenticator->current());
36+
$this->assertSame(1, $resolver->resolveCalls);
37+
}
38+
39+
#[Test]
40+
public function current_re_resolves_when_the_session_identity_changes(): void
41+
{
42+
$resolver = new CountingAuthenticatableResolver(
43+
new MemoizedAuthenticatable(id: 1),
44+
new MemoizedAuthenticatable(id: 2),
45+
);
46+
$session = $this->createSession();
47+
$session->set(SessionAuthenticator::AUTHENTICATABLE_KEY, 1);
48+
$session->set(SessionAuthenticator::AUTHENTICATABLE_CLASS, MemoizedAuthenticatable::class);
49+
50+
$authenticator = new SessionAuthenticator(
51+
sessionManager: new TestingSessionManager(),
52+
session: $session,
53+
authenticatableResolver: $resolver,
54+
);
55+
56+
$current = $authenticator->current();
57+
$this->assertInstanceOf(MemoizedAuthenticatable::class, $current);
58+
$this->assertSame(1, $current->id);
59+
60+
$session->set(SessionAuthenticator::AUTHENTICATABLE_KEY, 2);
61+
62+
$current = $authenticator->current();
63+
$this->assertInstanceOf(MemoizedAuthenticatable::class, $current);
64+
$this->assertSame(2, $current->id);
65+
$this->assertSame(2, $resolver->resolveCalls);
66+
}
67+
68+
#[Test]
69+
public function authenticate_replaces_a_cached_current_authenticatable(): void
70+
{
71+
$resolver = new CountingAuthenticatableResolver(
72+
new MemoizedAuthenticatable(id: 1),
73+
new MemoizedAuthenticatable(id: 2),
74+
);
75+
$session = $this->createSession();
76+
$session->set(SessionAuthenticator::AUTHENTICATABLE_KEY, 1);
77+
$session->set(SessionAuthenticator::AUTHENTICATABLE_CLASS, MemoizedAuthenticatable::class);
78+
79+
$authenticator = new SessionAuthenticator(
80+
sessionManager: new TestingSessionManager(),
81+
session: $session,
82+
authenticatableResolver: $resolver,
83+
);
84+
85+
$current = $authenticator->current();
86+
$this->assertInstanceOf(MemoizedAuthenticatable::class, $current);
87+
$this->assertSame(1, $current->id);
88+
89+
$authenticator->authenticate(new MemoizedAuthenticatable(id: 2));
90+
91+
$current = $authenticator->current();
92+
$this->assertInstanceOf(MemoizedAuthenticatable::class, $current);
93+
$this->assertSame(2, $current->id);
94+
}
95+
96+
private function createSession(): Session
97+
{
98+
$now = DateTime::now();
99+
100+
return new Session(
101+
id: new SessionId('test-session'),
102+
createdAt: $now,
103+
lastActiveAt: $now,
104+
);
105+
}
106+
}
107+
108+
final readonly class MemoizedAuthenticatable implements Authenticatable
109+
{
110+
public function __construct(
111+
public int $id,
112+
) {}
113+
}
114+
115+
final class CountingAuthenticatableResolver implements AuthenticatableResolver
116+
{
117+
public int $resolveCalls = 0;
118+
119+
/** @var array<int|string, MemoizedAuthenticatable> */
120+
private array $authenticatables = [];
121+
122+
public function __construct(MemoizedAuthenticatable ...$authenticatables)
123+
{
124+
foreach ($authenticatables as $authenticatable) {
125+
$this->authenticatables[$authenticatable->id] = $authenticatable;
126+
}
127+
}
128+
129+
public function resolve(int|string $id, string $class): ?Authenticatable
130+
{
131+
$this->resolveCalls++;
132+
133+
if ($class !== MemoizedAuthenticatable::class) {
134+
return null;
135+
}
136+
137+
return $this->authenticatables[$id] ?? null;
138+
}
139+
140+
public function resolveId(Authenticatable $authenticatable): int
141+
{
142+
return $authenticatable instanceof MemoizedAuthenticatable ? $authenticatable->id : 0;
143+
}
144+
}
145+
146+
final class TestingSessionManager implements SessionManager
147+
{
148+
public int $savedSessions = 0;
149+
150+
public function getOrCreate(SessionId $id): Session
151+
{
152+
$now = DateTime::now();
153+
154+
return new Session($id, $now, $now);
155+
}
156+
157+
public function save(Session $session): void
158+
{
159+
$this->savedSessions++;
160+
}
161+
162+
public function delete(Session $session): void {}
163+
164+
public function isValid(Session $session): bool
165+
{
166+
return true;
167+
}
168+
169+
public function deleteExpiredSessions(): void {}
170+
}

0 commit comments

Comments
 (0)