Skip to content

Commit f9ccb59

Browse files
feat: use PSR20 ClockInterface for session handling
- Introduced a new `SystemClock` class implementing `ClockInterface` for time management. - Updated `ArraySessionHandler` and `CacheSessionHandler` to utilize the `SystemClock` for timestamping, enhancing testability and flexibility. - Added a `FixedClock` mock for testing purposes, allowing controlled time manipulation in tests.
1 parent 5b4fb27 commit f9ccb59

File tree

5 files changed

+127
-10
lines changed

5 files changed

+127
-10
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"opis/json-schema": "^2.4",
1616
"php-mcp/schema": "dev-main",
1717
"phpdocumentor/reflection-docblock": "^5.6",
18+
"psr/clock": "^1.0",
1819
"psr/container": "^1.0 || ^2.0",
1920
"psr/event-dispatcher": "^1.0",
2021
"psr/log": "^1.0 || ^2.0 || ^3.0",

src/Defaults/SystemClock.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Defaults;
6+
7+
use DateTimeImmutable;
8+
use Psr\Clock\ClockInterface;
9+
10+
class SystemClock implements ClockInterface
11+
{
12+
public function now(): \DateTimeImmutable
13+
{
14+
return new \DateTimeImmutable();
15+
}
16+
}

src/Session/ArraySessionHandler.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace PhpMcp\Server\Session;
66

77
use PhpMcp\Server\Contracts\SessionHandlerInterface;
8+
use PhpMcp\Server\Defaults\SystemClock;
9+
use Psr\Clock\ClockInterface;
810

911
class ArraySessionHandler implements SessionHandlerInterface
1012
{
@@ -13,8 +15,13 @@ class ArraySessionHandler implements SessionHandlerInterface
1315
*/
1416
protected array $store = [];
1517

16-
public function __construct(public readonly int $ttl = 3600)
17-
{
18+
private ClockInterface $clock;
19+
20+
public function __construct(
21+
public readonly int $ttl = 3600,
22+
?ClockInterface $clock = null
23+
) {
24+
$this->clock = $clock ?? new SystemClock();
1825
}
1926

2027
public function read(string $sessionId): string|false
@@ -24,7 +31,7 @@ public function read(string $sessionId): string|false
2431
return false;
2532
}
2633

27-
$currentTimestamp = time();
34+
$currentTimestamp = $this->clock->now()->getTimestamp();
2835

2936
if ($currentTimestamp - $session['timestamp'] > $this->ttl) {
3037
unset($this->store[$sessionId]);
@@ -38,7 +45,7 @@ public function write(string $sessionId, string $data): bool
3845
{
3946
$this->store[$sessionId] = [
4047
'data' => $data,
41-
'timestamp' => time(),
48+
'timestamp' => $this->clock->now()->getTimestamp(),
4249
];
4350

4451
return true;
@@ -55,7 +62,7 @@ public function destroy(string $sessionId): bool
5562

5663
public function gc(int $maxLifetime): array
5764
{
58-
$currentTimestamp = time();
65+
$currentTimestamp = $this->clock->now()->getTimestamp();
5966
$deletedSessions = [];
6067

6168
foreach ($this->store as $sessionId => $session) {

src/Session/CacheSessionHandler.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,45 @@
55
namespace PhpMcp\Server\Session;
66

77
use PhpMcp\Server\Contracts\SessionHandlerInterface;
8+
use PhpMcp\Server\Defaults\SystemClock;
89
use Psr\SimpleCache\CacheInterface;
10+
use Psr\Clock\ClockInterface;
911

1012
class CacheSessionHandler implements SessionHandlerInterface
1113
{
1214
private const SESSION_INDEX_KEY = 'mcp_session_index';
1315
private array $sessionIndex = [];
16+
private ClockInterface $clock;
1417

1518
public function __construct(
1619
public readonly CacheInterface $cache,
17-
public readonly int $ttl = 3600
20+
public readonly int $ttl = 3600,
21+
?ClockInterface $clock = null
1822
) {
1923
$this->sessionIndex = $this->cache->get(self::SESSION_INDEX_KEY, []);
24+
$this->clock = $clock ?? new SystemClock();
2025
}
2126

2227
public function read(string $sessionId): string|false
2328
{
24-
return $this->cache->get($sessionId, false);
29+
$session = $this->cache->get($sessionId, false);
30+
if ($session === false) {
31+
return false;
32+
}
33+
34+
if ($this->clock->now()->getTimestamp() - $this->sessionIndex[$sessionId] > $this->ttl) {
35+
$this->cache->delete($sessionId);
36+
return false;
37+
}
38+
39+
return $session;
2540
}
2641

2742
public function write(string $sessionId, string $data): bool
2843
{
29-
$this->sessionIndex[$sessionId] = time();
44+
$this->sessionIndex[$sessionId] = $this->clock->now()->getTimestamp();
3045
$this->cache->set(self::SESSION_INDEX_KEY, $this->sessionIndex);
31-
return $this->cache->set($sessionId, $data, $this->ttl);
46+
return $this->cache->set($sessionId, $data);
3247
}
3348

3449
public function destroy(string $sessionId): bool
@@ -40,7 +55,7 @@ public function destroy(string $sessionId): bool
4055

4156
public function gc(int $maxLifetime): array
4257
{
43-
$currentTime = time();
58+
$currentTime = $this->clock->now()->getTimestamp();
4459
$deletedSessions = [];
4560

4661
foreach ($this->sessionIndex as $sessionId => $timestamp) {

tests/Mocks/Clock/FixedClock.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Tests\Mocks\Clock;
6+
7+
use DateTimeImmutable;
8+
use DateTimeZone;
9+
use Psr\Clock\ClockInterface;
10+
use DateInterval;
11+
12+
class FixedClock implements ClockInterface
13+
{
14+
private DateTimeImmutable $currentTime;
15+
16+
public function __construct(string|DateTimeImmutable $initialTime = 'now', ?DateTimeZone $timezone = null)
17+
{
18+
if ($initialTime instanceof DateTimeImmutable) {
19+
$this->currentTime = $initialTime;
20+
} else {
21+
$this->currentTime = new DateTimeImmutable($initialTime, $timezone);
22+
}
23+
}
24+
25+
public function now(): DateTimeImmutable
26+
{
27+
return $this->currentTime;
28+
}
29+
30+
public function setCurrentTime(string|DateTimeImmutable $newTime, ?DateTimeZone $timezone = null): void
31+
{
32+
if ($newTime instanceof DateTimeImmutable) {
33+
$this->currentTime = $newTime;
34+
} else {
35+
$this->currentTime = new DateTimeImmutable($newTime, $timezone);
36+
}
37+
}
38+
39+
public function advance(DateInterval $interval): void
40+
{
41+
$this->currentTime = $this->currentTime->add($interval);
42+
}
43+
44+
public function rewind(DateInterval $interval): void
45+
{
46+
$this->currentTime = $this->currentTime->sub($interval);
47+
}
48+
49+
public function addSecond(): void
50+
{
51+
$this->advance(new DateInterval("PT1S"));
52+
}
53+
54+
public function addSeconds(int $seconds): void
55+
{
56+
$this->advance(new DateInterval("PT{$seconds}S"));
57+
}
58+
59+
public function addMinutes(int $minutes): void
60+
{
61+
$this->advance(new DateInterval("PT{$minutes}M"));
62+
}
63+
64+
public function addHours(int $hours): void
65+
{
66+
$this->advance(new DateInterval("PT{$hours}H"));
67+
}
68+
69+
public function subSeconds(int $seconds): void
70+
{
71+
$this->rewind(new DateInterval("PT{$seconds}S"));
72+
}
73+
74+
public function subMinutes(int $minutes): void
75+
{
76+
$this->rewind(new DateInterval("PT{$minutes}M"));
77+
}
78+
}

0 commit comments

Comments
 (0)