Skip to content

Commit 51ab8f3

Browse files
committed
feature/initial-cache-implementation - phpstan fixes
1 parent 9a64c36 commit 51ab8f3

File tree

5 files changed

+74
-46
lines changed

5 files changed

+74
-46
lines changed

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
services:
22
php:
33
image: tigitz/phpspellchecker:${PHP_VERSION:-8.4}
4-
user: 1001:1001
54
build:
65
context: docker/php
76
args:

src/Cache/CacheItem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function __construct(
1414
private readonly string $key,
1515
private mixed $value = null,
1616
public ?DateTimeInterface $expiry = null,
17-
private ?bool $isHit = false
17+
private bool $isHit = false
1818
) {
1919
}
2020

src/Cache/FileCache.php

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
final class FileCache implements FileCacheInterface
1313
{
14+
/**
15+
* @var array<string, CacheItemInterface>
16+
*/
1417
private array $deferred = [];
1518

1619
/**
@@ -27,7 +30,7 @@ public function __construct(
2730
$directory = $this->getDefaultDirectory();
2831
}
2932

30-
$this->validateNamespace($namespace);
33+
$this->validateNamespace();
3134

3235
$directory .= DIRECTORY_SEPARATOR . $namespace;
3336

@@ -57,31 +60,46 @@ public function getItem(string $key): CacheItemInterface
5760
return $item;
5861
}
5962

60-
$handle = fopen($filepath, 'r');
61-
if (flock($handle, LOCK_SH)) { // Shared lock for reading
62-
try {
63-
$data = fread($handle, filesize($filepath));
64-
$value = unserialize($data);
63+
$data = \PhpSpellcheck\file_get_contents($filepath);
64+
65+
if ($data === '') {
66+
return $item;
67+
}
68+
69+
$value = unserialize($data);
6570

66-
if ($value && (!$value->expiresAt || $value->expiresAt > time())) {
67-
$item->set($value->data);
68-
$item->setIsHit(true);
69-
if ($value->expiresAt) {
70-
$item->expiresAt(new \DateTime('@' . $value->expiresAt));
71-
}
72-
}
73-
} finally {
74-
flock($handle, LOCK_UN);
75-
fclose($handle);
76-
}
71+
if (! is_object($value)
72+
|| ! property_exists($value, 'data')
73+
|| ! property_exists($value, 'expiresAt')
74+
) {
75+
return $item;
76+
}
77+
78+
if ($value->expiresAt !== 0
79+
&& $value->expiresAt !== null
80+
&& $value->expiresAt <= time()
81+
) {
82+
unlink($filepath);
83+
84+
return $item;
85+
}
86+
87+
$item->set($value->data)->setIsHit(true);
88+
89+
if (is_int($value->expiresAt) && $value->expiresAt > 0) {
90+
$item->expiresAt(new \DateTime('@' . $value->expiresAt));
7791
}
7892

7993
return $item;
8094
}
8195

96+
/**
97+
* @param array<string> $keys
98+
* @return iterable<CacheItemInterface>
99+
*/
82100
public function getItems(array $keys = []): iterable
83101
{
84-
return array_map(fn ($key) => $this->getItem($key), $keys);
102+
return array_map(fn ($key): CacheItemInterface => $this->getItem($key), $keys);
85103
}
86104

87105
public function hasItem(string $key): bool
@@ -132,13 +150,16 @@ public function save(CacheItemInterface $item): bool
132150
{
133151
$this->validateKey($item->getKey());
134152

135-
$expiresAt = null;
136-
if ($item->expiry) {
137-
$expiresAt = $item->expiry->getTimestamp();
138-
} elseif ($this->defaultLifetime > 0) {
139-
$expiresAt = time() + $this->defaultLifetime;
153+
if (! property_exists($item, 'expiry')) {
154+
throw new InvalidArgumentException('CacheItem expiry property is required');
140155
}
141156

157+
$expiresAt = match(true) {
158+
$item->expiry instanceof \DateTimeInterface => $item->expiry->getTimestamp(),
159+
$this->defaultLifetime > 0 => time() + $this->defaultLifetime,
160+
default => null
161+
};
162+
142163
$value = (object) [
143164
'data' => $item->get(),
144165
'expiresAt' => $expiresAt,
@@ -147,22 +168,11 @@ public function save(CacheItemInterface $item): bool
147168
$serialized = serialize($value);
148169
$filepath = $this->getFilePath($item->getKey());
149170

150-
$handle = fopen($filepath, 'w');
151-
if (!$handle) {
171+
try {
172+
return (bool) \PhpSpellcheck\file_put_contents($filepath, $serialized, LOCK_EX);
173+
} catch (\Exception $e) {
152174
return false;
153175
}
154-
155-
$success = false;
156-
if (flock($handle, LOCK_EX)) { // Exclusive lock for writing
157-
try {
158-
$success = fwrite($handle, $serialized) !== false;
159-
} finally {
160-
flock($handle, LOCK_UN);
161-
fclose($handle);
162-
}
163-
}
164-
165-
return $success;
166176
}
167177

168178
public function saveDeferred(CacheItemInterface $item): bool
@@ -194,9 +204,9 @@ public function getFilePath(string $key): string
194204
return $this->directory . $key;
195205
}
196206

197-
private function validateNamespace(string $namespace): void
207+
private function validateNamespace(): void
198208
{
199-
if (\PhpSpellcheck\preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match) === 1) {
209+
if (\PhpSpellcheck\preg_match('#[^-+_.A-Za-z0-9]#', $this->namespace, $match) === 1) {
200210
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
201211
}
202212
}

src/Spellchecker/CacheableSpellchecker.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpSpellcheck\Spellchecker;
66

7+
use PhpSpellcheck\MisspellingInterface;
78
use Psr\Cache\CacheItemPoolInterface;
89

910
final readonly class CacheableSpellchecker implements SpellcheckerInterface
@@ -24,7 +25,11 @@ public function check(
2425
$cacheItem = $this->cache->getItem($cacheKey);
2526

2627
if ($cacheItem->isHit()) {
27-
yield from $cacheItem->get();
28+
foreach ((array) $cacheItem->get() as $misspelling) {
29+
if ($misspelling instanceof MisspellingInterface) {
30+
yield $misspelling;
31+
}
32+
}
2833
return;
2934
}
3035

@@ -41,7 +46,11 @@ public function getSupportedLanguages(): iterable
4146
$cacheItem = $this->cache->getItem($cacheKey);
4247

4348
if ($cacheItem->isHit()) {
44-
yield from $cacheItem->get();
49+
foreach ((array) $cacheItem->get() as $language) {
50+
if (is_string($language)) {
51+
yield $language;
52+
}
53+
}
4554
return;
4655
}
4756

tests/Cache/FileCacheTest.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function testSaveAndGetItem(): void
4242
$this->cache->save($item);
4343

4444
$newItem = $this->cache->getItem('key2');
45+
4546
$this->assertTrue($newItem->isHit());
4647
$this->assertEquals('value2', $newItem->get());
4748
}
@@ -155,9 +156,18 @@ public function testCustomDirectory(): void
155156
$cache->clear();
156157
}
157158

158-
public function testUnwriteableDirectoryThrowsException(): void
159+
public function testExpiredCachedFileIsDeletedWhenCallingGetItem(): void
159160
{
160-
$this->expectException(\PhpSpellcheck\Exception\RuntimeException::class);
161-
new FileCache('FileCacheTest', 0, '/root/.cache');
161+
$cache = new FileCache('FileCacheTest', 1, '/tmp');
162+
$item = $cache->getItem('unlinked_key');
163+
$item->set('value');
164+
$item->expiresAfter(1);
165+
$cache->save($item);
166+
167+
sleep(2);
168+
169+
$cache->getItem('unlinked_key');
170+
171+
$this->assertFalse(file_exists('/tmp/FileCacheTest/unlinked_key'));
162172
}
163173
}

0 commit comments

Comments
 (0)