Skip to content

Commit afb5438

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

2 files changed

Lines changed: 205 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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
$this->assertSame(1, $authenticator->current()->id);
57+
58+
$session->set(SessionAuthenticator::AUTHENTICATABLE_KEY, 2);
59+
60+
$this->assertSame(2, $authenticator->current()->id);
61+
$this->assertSame(2, $resolver->resolveCalls);
62+
}
63+
64+
#[Test]
65+
public function authenticate_replaces_a_cached_current_authenticatable(): void
66+
{
67+
$resolver = new CountingAuthenticatableResolver(
68+
new MemoizedAuthenticatable(id: 1),
69+
new MemoizedAuthenticatable(id: 2),
70+
);
71+
$session = $this->createSession();
72+
$session->set(SessionAuthenticator::AUTHENTICATABLE_KEY, 1);
73+
$session->set(SessionAuthenticator::AUTHENTICATABLE_CLASS, MemoizedAuthenticatable::class);
74+
75+
$authenticator = new SessionAuthenticator(
76+
sessionManager: new TestingSessionManager(),
77+
session: $session,
78+
authenticatableResolver: $resolver,
79+
);
80+
81+
$this->assertSame(1, $authenticator->current()->id);
82+
83+
$authenticator->authenticate(new MemoizedAuthenticatable(id: 2));
84+
85+
$this->assertSame(2, $authenticator->current()->id);
86+
}
87+
88+
private function createSession(): Session
89+
{
90+
$now = DateTime::now();
91+
92+
return new Session(
93+
id: new SessionId('test-session'),
94+
createdAt: $now,
95+
lastActiveAt: $now,
96+
);
97+
}
98+
}
99+
100+
final readonly class MemoizedAuthenticatable implements Authenticatable
101+
{
102+
public function __construct(
103+
public int $id,
104+
) {}
105+
}
106+
107+
final class CountingAuthenticatableResolver implements AuthenticatableResolver
108+
{
109+
public int $resolveCalls = 0;
110+
111+
/** @var array<int|string, MemoizedAuthenticatable> */
112+
private array $authenticatables = [];
113+
114+
public function __construct(MemoizedAuthenticatable ...$authenticatables)
115+
{
116+
foreach ($authenticatables as $authenticatable) {
117+
$this->authenticatables[$authenticatable->id] = $authenticatable;
118+
}
119+
}
120+
121+
public function resolve(int|string $id, string $class): ?Authenticatable
122+
{
123+
$this->resolveCalls++;
124+
125+
if ($class !== MemoizedAuthenticatable::class) {
126+
return null;
127+
}
128+
129+
return $this->authenticatables[$id] ?? null;
130+
}
131+
132+
public function resolveId(Authenticatable $authenticatable): int|string
133+
{
134+
return $authenticatable instanceof MemoizedAuthenticatable ? $authenticatable->id : 0;
135+
}
136+
}
137+
138+
final class TestingSessionManager implements SessionManager
139+
{
140+
public int $savedSessions = 0;
141+
142+
public function getOrCreate(SessionId $id): Session
143+
{
144+
$now = DateTime::now();
145+
146+
return new Session($id, $now, $now);
147+
}
148+
149+
public function save(Session $session): void
150+
{
151+
$this->savedSessions++;
152+
}
153+
154+
public function delete(Session $session): void {}
155+
156+
public function isValid(Session $session): bool
157+
{
158+
return true;
159+
}
160+
161+
public function deleteExpiredSessions(): void {}
162+
}

0 commit comments

Comments
 (0)