Skip to content

Commit eb7150b

Browse files
feat(session): add redis session manager (#1790)
Co-authored-by: Márk Magyar <[email protected]>
1 parent 0b52ffd commit eb7150b

File tree

3 files changed

+465
-0
lines changed

3 files changed

+465
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session\Config;
6+
7+
use Tempest\Container\Container;
8+
use Tempest\DateTime\Duration;
9+
use Tempest\Http\Session\Managers\RedisSessionManager;
10+
use Tempest\Http\Session\SessionConfig;
11+
12+
final class RedisSessionConfig implements SessionConfig
13+
{
14+
/**
15+
* @param Duration $expiration Time required for a session to expire.
16+
*/
17+
public function __construct(
18+
private(set) Duration $expiration,
19+
readonly string $prefix = 'session:',
20+
) {}
21+
22+
public function createManager(Container $container): RedisSessionManager
23+
{
24+
return $container->get(RedisSessionManager::class);
25+
}
26+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session\Managers;
6+
7+
use Tempest\Clock\Clock;
8+
use Tempest\Http\Session\Session;
9+
use Tempest\Http\Session\SessionConfig;
10+
use Tempest\Http\Session\SessionDestroyed;
11+
use Tempest\Http\Session\SessionId;
12+
use Tempest\Http\Session\SessionManager;
13+
use Tempest\KeyValue\Redis\Redis;
14+
use Tempest\Support\Str\ImmutableString;
15+
use Throwable;
16+
17+
use function Tempest\event;
18+
19+
final readonly class RedisSessionManager implements SessionManager
20+
{
21+
public function __construct(
22+
private Clock $clock,
23+
private Redis $redis,
24+
private SessionConfig $sessionConfig,
25+
) {}
26+
27+
public function create(SessionId $id): Session
28+
{
29+
return $this->persist($id);
30+
}
31+
32+
public function set(SessionId $id, string $key, mixed $value): void
33+
{
34+
$this->persist($id, [...$this->getData($id), ...[$key => $value]]);
35+
}
36+
37+
public function get(SessionId $id, string $key, mixed $default = null): mixed
38+
{
39+
return $this->getData($id)[$key] ?? $default;
40+
}
41+
42+
public function remove(SessionId $id, string $key): void
43+
{
44+
$data = $this->getData($id);
45+
46+
unset($data[$key]);
47+
48+
$this->persist($id, $data);
49+
}
50+
51+
public function destroy(SessionId $id): void
52+
{
53+
$this->redis->command('UNLINK', $this->getKey($id));
54+
55+
event(new SessionDestroyed($id));
56+
}
57+
58+
public function isValid(SessionId $id): bool
59+
{
60+
$session = $this->resolve($id);
61+
62+
if ($session === null) {
63+
return false;
64+
}
65+
66+
if (! ($session->lastActiveAt ?? null)) {
67+
return false;
68+
}
69+
70+
return $this->clock->now()->before(
71+
other: $session->lastActiveAt->plus($this->sessionConfig->expiration),
72+
);
73+
}
74+
75+
private function resolve(SessionId $id): ?Session
76+
{
77+
try {
78+
$content = $this->redis->get($this->getKey($id));
79+
return unserialize($content, ['allowed_classes' => true]);
80+
} catch (Throwable) {
81+
return null;
82+
}
83+
}
84+
85+
public function all(SessionId $id): array
86+
{
87+
return $this->getData($id);
88+
}
89+
90+
/**
91+
* @return array<mixed>
92+
*/
93+
private function getData(SessionId $id): array
94+
{
95+
return $this->resolve($id)->data ?? [];
96+
}
97+
98+
/**
99+
* @param array<mixed>|null $data
100+
*/
101+
private function persist(SessionId $id, ?array $data = null): Session
102+
{
103+
$now = $this->clock->now();
104+
$session = $this->resolve($id) ?? new Session(
105+
id: $id,
106+
createdAt: $now,
107+
lastActiveAt: $now,
108+
);
109+
110+
$session->lastActiveAt = $now;
111+
112+
if ($data !== null) {
113+
$session->data = $data;
114+
}
115+
116+
$this->redis->set($this->getKey($id), serialize($session), $this->sessionConfig->expiration);
117+
118+
return $session;
119+
}
120+
121+
private function getKey(SessionId $id): string
122+
{
123+
return sprintf('%s%s', $this->sessionConfig->prefix, $id);
124+
}
125+
126+
private function getSessionIdFromKey(string $key): SessionId
127+
{
128+
return new SessionId(
129+
new ImmutableString($key)
130+
->afterFirst($this->sessionConfig->prefix)
131+
->toString(),
132+
);
133+
}
134+
135+
public function cleanup(): void
136+
{
137+
$cursor = '0';
138+
139+
do {
140+
$result = $this->redis->command('SCAN', $cursor, 'MATCH', $this->getKey(new SessionId('*')), 'COUNT', '100');
141+
$cursor = $result[0];
142+
foreach ($result[1] as $key) {
143+
$sessionId = $this->getSessionIdFromKey($key);
144+
145+
if ($this->isValid($sessionId)) {
146+
continue;
147+
}
148+
149+
$this->destroy($sessionId);
150+
}
151+
} while ($cursor !== '0');
152+
}
153+
}

0 commit comments

Comments
 (0)