Skip to content

Commit 06a2250

Browse files
authored
Merge pull request #177 from patchlevel/add-store-cache
add cryptography store cache
2 parents 5b79db6 + bdc788c commit 06a2250

4 files changed

Lines changed: 397 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Store;
6+
7+
use DateInterval;
8+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
9+
use Psr\SimpleCache\CacheInterface;
10+
11+
final readonly class Psr16CacheStoreDecorator implements CipherKeyStore
12+
{
13+
public function __construct(
14+
private CipherKeyStore $cipherKeyStore,
15+
private CacheInterface $cache,
16+
private DateInterval|int|null $ttl = null,
17+
) {
18+
}
19+
20+
public function currentKeyFor(string $subjectId): CipherKey
21+
{
22+
$key = 'subjectId:' . $subjectId;
23+
$entry = $this->cache->get($key);
24+
25+
if ($entry instanceof CipherKey) {
26+
return $entry;
27+
}
28+
29+
$entry = $this->cipherKeyStore->currentKeyFor($subjectId);
30+
31+
$this->cache->set($key, $entry, $this->ttl);
32+
33+
return $entry;
34+
}
35+
36+
public function get(string $id): CipherKey
37+
{
38+
$key = 'id:' . $id;
39+
$entry = $this->cache->get($key);
40+
41+
if ($entry instanceof CipherKey) {
42+
return $entry;
43+
}
44+
45+
$entry = $this->cipherKeyStore->get($id);
46+
47+
$this->cache->set($key, $entry, $this->ttl);
48+
49+
return $entry;
50+
}
51+
52+
public function store(CipherKey $key): void
53+
{
54+
$this->cipherKeyStore->store($key);
55+
56+
$this->cache->set('id:' . $key->id, $key, $this->ttl);
57+
$this->cache->set('subjectId:' . $key->subjectId, $key, $this->ttl);
58+
}
59+
60+
public function remove(string $id): void
61+
{
62+
$this->cipherKeyStore->remove($id);
63+
64+
$this->cache->delete('id:' . $id);
65+
}
66+
67+
public function removeWithSubjectId(string $subjectId): void
68+
{
69+
$this->cipherKeyStore->removeWithSubjectId($subjectId);
70+
71+
$this->cache->delete('subjectId:' . $subjectId);
72+
}
73+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Store;
6+
7+
use DateInterval;
8+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
9+
use Psr\Cache\CacheItemPoolInterface;
10+
11+
final readonly class Psr6CacheStoreDecorator implements CipherKeyStore
12+
{
13+
public function __construct(
14+
private CipherKeyStore $cipherKeyStore,
15+
private CacheItemPoolInterface $cache,
16+
private DateInterval|int|null $expiresAfter = null,
17+
) {
18+
}
19+
20+
public function currentKeyFor(string $subjectId): CipherKey
21+
{
22+
$key = 'subjectId:' . $subjectId;
23+
$item = $this->cache->getItem($key);
24+
$entry = $item->get();
25+
26+
if ($item->isHit() && $entry instanceof CipherKey) {
27+
return $entry;
28+
}
29+
30+
$entry = $this->cipherKeyStore->currentKeyFor($subjectId);
31+
32+
$item->set($entry);
33+
$item->expiresAfter($this->expiresAfter);
34+
$this->cache->save($item);
35+
36+
return $entry;
37+
}
38+
39+
public function get(string $id): CipherKey
40+
{
41+
$key = 'id:' . $id;
42+
$item = $this->cache->getItem($key);
43+
$entry = $item->get();
44+
45+
if ($item->isHit() && $entry instanceof CipherKey) {
46+
return $entry;
47+
}
48+
49+
$entry = $this->cipherKeyStore->get($id);
50+
51+
$item->set($entry);
52+
$item->expiresAfter($this->expiresAfter);
53+
$this->cache->save($item);
54+
55+
return $entry;
56+
}
57+
58+
public function store(CipherKey $key): void
59+
{
60+
$this->cipherKeyStore->store($key);
61+
}
62+
63+
public function remove(string $id): void
64+
{
65+
$this->cipherKeyStore->remove($id);
66+
67+
$this->cache->deleteItem('id:' . $id);
68+
}
69+
70+
public function removeWithSubjectId(string $subjectId): void
71+
{
72+
$this->cipherKeyStore->removeWithSubjectId($subjectId);
73+
74+
$this->cache->deleteItem('subjectId:' . $subjectId);
75+
}
76+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Store;
6+
7+
use DateTimeImmutable;
8+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
9+
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyStore;
10+
use Patchlevel\Hydrator\Extension\Cryptography\Store\Psr16CacheStoreDecorator;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
use PHPUnit\Framework\TestCase;
13+
use Psr\SimpleCache\CacheInterface;
14+
15+
#[CoversClass(Psr16CacheStoreDecorator::class)]
16+
final class Psr16CacheStoreDecoratorTest extends TestCase
17+
{
18+
public function testCurrentKeyForWithCacheHit(): void
19+
{
20+
$key = $this->createKey();
21+
22+
$cache = $this->createMock(CacheInterface::class);
23+
$cache->expects(self::once())->method('get')->with('subjectId:subject-1')->willReturn($key);
24+
$cache->expects(self::never())->method('set');
25+
26+
$innerStore = $this->createMock(CipherKeyStore::class);
27+
$innerStore->expects(self::never())->method('currentKeyFor');
28+
29+
$store = new Psr16CacheStoreDecorator($innerStore, $cache);
30+
31+
self::assertSame($key, $store->currentKeyFor('subject-1'));
32+
}
33+
34+
public function testCurrentKeyForWithCacheMiss(): void
35+
{
36+
$key = $this->createKey();
37+
38+
$cache = $this->createMock(CacheInterface::class);
39+
$cache->expects(self::once())->method('get')->with('subjectId:subject-1')->willReturn(null);
40+
$cache->expects(self::once())->method('set')->with('subjectId:subject-1', $key, 42);
41+
42+
$innerStore = $this->createMock(CipherKeyStore::class);
43+
$innerStore->expects(self::once())->method('currentKeyFor')->with('subject-1')->willReturn($key);
44+
45+
$store = new Psr16CacheStoreDecorator($innerStore, $cache, 42);
46+
47+
self::assertSame($key, $store->currentKeyFor('subject-1'));
48+
}
49+
50+
public function testGetWithCacheMiss(): void
51+
{
52+
$key = $this->createKey();
53+
54+
$cache = $this->createMock(CacheInterface::class);
55+
$cache->expects(self::once())->method('get')->with('id:key-1')->willReturn(false);
56+
$cache->expects(self::once())->method('set')->with('id:key-1', $key, null);
57+
58+
$innerStore = $this->createMock(CipherKeyStore::class);
59+
$innerStore->expects(self::once())->method('get')->with('key-1')->willReturn($key);
60+
61+
$store = new Psr16CacheStoreDecorator($innerStore, $cache);
62+
63+
self::assertSame($key, $store->get('key-1'));
64+
}
65+
66+
public function testStoreWritesInnerStoreAndBothCacheEntries(): void
67+
{
68+
$key = $this->createKey();
69+
70+
$cache = $this->createMock(CacheInterface::class);
71+
$cache->expects(self::exactly(2))->method('set')->willReturnMap([
72+
['id:key-1', $key, 17, true],
73+
['subjectId:subject-1', $key, 17, true],
74+
]);
75+
76+
$innerStore = $this->createMock(CipherKeyStore::class);
77+
$innerStore->expects(self::once())->method('store')->with($key);
78+
79+
$store = new Psr16CacheStoreDecorator($innerStore, $cache, 17);
80+
$store->store($key);
81+
}
82+
83+
public function testRemoveDeletesIdCacheEntry(): void
84+
{
85+
$cache = $this->createMock(CacheInterface::class);
86+
$cache->expects(self::once())->method('delete')->with('id:key-1');
87+
88+
$innerStore = $this->createMock(CipherKeyStore::class);
89+
$innerStore->expects(self::once())->method('remove')->with('key-1');
90+
91+
$store = new Psr16CacheStoreDecorator($innerStore, $cache);
92+
$store->remove('key-1');
93+
}
94+
95+
public function testRemoveWithSubjectIdDeletesSubjectCacheEntry(): void
96+
{
97+
$cache = $this->createMock(CacheInterface::class);
98+
$cache->expects(self::once())->method('delete')->with('subjectId:subject-1');
99+
100+
$innerStore = $this->createMock(CipherKeyStore::class);
101+
$innerStore->expects(self::once())->method('removeWithSubjectId')->with('subject-1');
102+
103+
$store = new Psr16CacheStoreDecorator($innerStore, $cache);
104+
$store->removeWithSubjectId('subject-1');
105+
}
106+
107+
private function createKey(): CipherKey
108+
{
109+
return new CipherKey(
110+
'key-1',
111+
'subject-1',
112+
'secret',
113+
'aes-256-gcm',
114+
new DateTimeImmutable(),
115+
);
116+
}
117+
}

0 commit comments

Comments
 (0)