Skip to content

Commit 6e2052d

Browse files
finished PSR-16 implementation
1 parent 1b254b4 commit 6e2052d

File tree

5 files changed

+228
-57
lines changed

5 files changed

+228
-57
lines changed

phpunit.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
</testsuite>
1212
<testsuite name="NoDatabase">
1313
<directory>./tests/protocol</directory>
14+
<file>./tests/helpers/FileCacheTest.php</file>
1415
</testsuite>
1516
</testsuites>
1617
<php>

src/autoload.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
if (reset($parts) == 'tests')
1313
array_unshift($parts,'..');
1414

15-
//compose standart namespaced path to file
15+
//compose standard namespaced path to file
1616
$path = __DIR__ . DS . implode(DS, $parts) . '.php';
1717
if (file_exists($path)) {
1818
require_once $path;

src/helpers/FileCache.php

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace Bolt\helpers;
44

55
use Psr\SimpleCache\CacheInterface;
6+
use Psr\SimpleCache\InvalidArgumentException;
67

78
/**
89
* Class FileCache
10+
* implementation of PSR-16 Simple Cache Interface
911
*
1012
* @author Michal Stefanak
1113
* @link https://github.com/neo4j-php/Bolt
@@ -40,46 +42,55 @@ private function shutdown(): void
4042
flock($handle, LOCK_UN);
4143
fclose($handle);
4244
}
45+
$this->handles = [];
4346
}
4447

4548
/**
46-
* Fetches a value from the cache.
47-
*
48-
* @param string $key The unique key of this item in the cache.
49-
* @param mixed $default Default value to return if the key does not exist.
50-
*
51-
* @return mixed The value of the item from the cache, or $default in case of cache miss.
49+
* Validate cache key if it does conform to allowed characters
50+
*/
51+
private function validateKey(string $key): void
52+
{
53+
if (!preg_match('/^[\w\.]+$/i', $key)) {
54+
throw new class($key) extends \Exception implements InvalidArgumentException {
55+
protected $message;
56+
57+
public function __construct(string $key)
58+
{
59+
$this->message = "Invalid cache key: $key. Allowed characters are A-Za-z0-9_.";
60+
}
61+
};
62+
}
63+
}
64+
65+
/**
66+
* @inheritDoc
5267
*/
5368
public function get(string $key, mixed $default = null): mixed
5469
{
70+
$this->validateKey($key);
71+
5572
if (array_key_exists($key, $this->handles)) {
5673
rewind($this->handles[$key]);
57-
return unserialize(stream_get_contents($this->handles[$key]), ['allowed_classes' => false]);
74+
return @unserialize(stream_get_contents($this->handles[$key]), ['allowed_classes' => false]);
5875
}
5976

6077
if ($this->has($key)) {
6178
$data = file_get_contents($this->tempDir . $key);
62-
if ($data !== false) {
63-
return unserialize($data, ['allowed_classes' => false]);
79+
if (!empty($data)) {
80+
return @unserialize($data, ['allowed_classes' => false]);
6481
}
6582
}
6683

6784
return $default;
6885
}
6986

7087
/**
71-
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
72-
*
73-
* @param string $key The key of the item to store.
74-
* @param mixed $value The value of the item to store, must be serializable.
75-
* @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
76-
* the driver supports TTL then the library may set a default value
77-
* for it or let the driver take care of that.
78-
*
79-
* @return bool True on success and false on failure.
88+
* @inheritDoc
8089
*/
8190
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool
8291
{
92+
$this->validateKey($key);
93+
8394
if ($ttl) {
8495
is_writable($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR) && file_put_contents(
8596
$this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key,
@@ -97,14 +108,12 @@ public function set(string $key, mixed $value, \DateInterval|int|null $ttl = nul
97108
}
98109

99110
/**
100-
* Delete an item from the cache by its unique key.
101-
*
102-
* @param string $key The unique cache key of the item to delete.
103-
*
104-
* @return bool True if the item was successfully removed. False if there was an error.
111+
* @inheritDoc
105112
*/
106113
public function delete(string $key): bool
107114
{
115+
$this->validateKey($key);
116+
108117
if ($this->has($key)) {
109118
if (file_exists($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key)) {
110119
@unlink($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key);
@@ -126,12 +135,7 @@ public function clear(): bool
126135
}
127136

128137
/**
129-
* Obtains multiple cache items by their unique keys.
130-
*
131-
* @param iterable<string> $keys A list of keys that can be obtained in a single operation.
132-
* @param mixed $default Default value to return for keys that do not exist.
133-
*
134-
* @return iterable<string, mixed> A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
138+
* @inheritDoc
135139
*/
136140
public function getMultiple(iterable $keys, mixed $default = null): iterable
137141
{
@@ -141,14 +145,7 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable
141145
}
142146

143147
/**
144-
* Persists a set of key => value pairs in the cache, with an optional TTL.
145-
*
146-
* @param iterable $values A list of key => value pairs for a multiple-set operation.
147-
* @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
148-
* the driver supports TTL then the library may set a default value
149-
* for it or let the driver take care of that.
150-
*
151-
* @return bool True on success and false on failure.
148+
* @inheritDoc
152149
*/
153150
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool
154151
{
@@ -159,11 +156,7 @@ public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null
159156
}
160157

161158
/**
162-
* Deletes multiple cache items in a single operation.
163-
*
164-
* @param iterable<string> $keys A list of string-based keys to be deleted.
165-
*
166-
* @return bool True if the items were successfully removed. False if there was an error.
159+
* @inheritDoc
167160
*/
168161
public function deleteMultiple(iterable $keys): bool
169162
{
@@ -174,19 +167,12 @@ public function deleteMultiple(iterable $keys): bool
174167
}
175168

176169
/**
177-
* Determines whether an item is present in the cache.
178-
*
179-
* NOTE: It is recommended that has() is only to be used for cache warming type purposes
180-
* and not to be used within your live applications operations for get/set, as this method
181-
* is subject to a race condition where your has() will return true and immediately after,
182-
* another script can remove it making the state of your app out of date.
183-
*
184-
* @param string $key The cache item key.
185-
*
186-
* @return bool
170+
* @inheritDoc
187171
*/
188172
public function has(string $key): bool
189173
{
174+
$this->validateKey($key);
175+
190176
// remove file when is expired
191177
if (
192178
file_exists($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key)
@@ -201,26 +187,31 @@ public function has(string $key): bool
201187

202188
/**
203189
* Lock a key to prevent other processes from modifying it
204-
* @param string $key
205-
* @return bool
206190
*/
207191
public function lock(string $key): bool
208192
{
193+
$this->validateKey($key);
194+
209195
$this->handles[$key] = fopen($this->tempDir . $key, 'c+');
210196
return flock($this->handles[$key], LOCK_EX);
211197
}
212198

213199
/**
214200
* Unlock a key
215-
* @param string $key
216-
* @return void
217201
*/
218202
public function unlock(string $key): void
219203
{
204+
$this->validateKey($key);
205+
220206
if (array_key_exists($key, $this->handles)) {
221207
flock($this->handles[$key], LOCK_UN);
222208
fclose($this->handles[$key]);
223209
unset($this->handles[$key]);
224210
}
225211
}
212+
213+
public function __destruct()
214+
{
215+
$this->shutdown();
216+
}
226217
}

tests/helpers/FileCacheTest.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
namespace Bolt\tests\helpers;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Bolt\helpers\FileCache;
7+
8+
class FileCacheTest extends TestCase
9+
{
10+
private FileCache $cache;
11+
12+
protected function setUp(): void
13+
{
14+
$this->cache = new FileCache();
15+
}
16+
17+
public function testConstruct(): void
18+
{
19+
$this->assertDirectoryExists(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-filecache');
20+
$this->assertDirectoryExists(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-filecache' . DIRECTORY_SEPARATOR . '.ttl');
21+
}
22+
23+
public function testGetAndSet(): void
24+
{
25+
$key = uniqid('key_', true);
26+
$value = uniqid('value_', true);
27+
$this->assertTrue($this->cache->set($key, $value));
28+
$this->assertEquals($value, $this->cache->get($key));
29+
}
30+
31+
public function testDelete(): void
32+
{
33+
$key = uniqid('key_', true);
34+
$value = uniqid('value_', true);
35+
$this->assertTrue($this->cache->set($key, $value));
36+
$this->assertTrue($this->cache->delete($key));
37+
$this->assertNull($this->cache->get($key));
38+
}
39+
40+
public function testClear(): void
41+
{
42+
$this->assertTrue($this->cache->set(uniqid('key_', true), uniqid('value_', true)));
43+
$this->assertTrue($this->cache->set(uniqid('key_', true), uniqid('value_', true)));
44+
$this->assertTrue($this->cache->clear());
45+
$this->assertNull($this->cache->get(uniqid('key_', true)));
46+
$this->assertNull($this->cache->get(uniqid('key_', true)));
47+
}
48+
49+
public function testGetMultiple(): void
50+
{
51+
$key1 = uniqid('key1_', true);
52+
$key2 = uniqid('key2_', true);
53+
$value1 = uniqid('value1_', true);
54+
$value2 = uniqid('value2_', true);
55+
$this->assertTrue($this->cache->set($key1, $value1));
56+
$this->assertTrue($this->cache->set($key2, $value2));
57+
$result = iterator_to_array($this->cache->getMultiple([$key1, $key2]));
58+
$this->assertEquals([$value1, $value2], $result);
59+
}
60+
61+
public function testSetMultiple(): void
62+
{
63+
$values = [
64+
uniqid('key1_', true) => uniqid('value1_', true),
65+
uniqid('key2_', true) => uniqid('value2_', true)
66+
];
67+
$this->assertTrue($this->cache->setMultiple($values));
68+
foreach ($values as $key => $value) {
69+
$this->assertEquals($value, $this->cache->get($key));
70+
}
71+
}
72+
73+
public function testDeleteMultiple(): void
74+
{
75+
$key1 = uniqid('key1_', true);
76+
$key2 = uniqid('key2_', true);
77+
$value1 = uniqid('value1_', true);
78+
$value2 = uniqid('value2_', true);
79+
$this->assertTrue($this->cache->set($key1, $value1));
80+
$this->assertTrue($this->cache->set($key2, $value2));
81+
$this->assertTrue($this->cache->deleteMultiple([$key1, $key2]));
82+
$this->assertNull($this->cache->get($key1));
83+
$this->assertNull($this->cache->get($key2));
84+
}
85+
86+
public function testHas(): void
87+
{
88+
$key = uniqid('key_', true);
89+
$value = uniqid('value_', true);
90+
$this->assertTrue($this->cache->set($key, $value));
91+
$this->assertTrue($this->cache->has($key));
92+
$this->assertTrue($this->cache->delete($key));
93+
$this->assertFalse($this->cache->has($key));
94+
}
95+
96+
public function testLockAndUnlock(): void
97+
{
98+
$key = uniqid('key_', true);
99+
$value = uniqid('value_', true);
100+
$this->assertTrue($this->cache->set($key, $value));
101+
$this->assertTrue($this->cache->lock($key));
102+
$this->cache->unlock($key);
103+
104+
$reflection = new \ReflectionClass($this->cache);
105+
$property = $reflection->getProperty('handles');
106+
$property->setAccessible(true);
107+
$handles = $property->getValue($this->cache);
108+
$this->assertFalse(array_key_exists($key, $handles));
109+
}
110+
111+
public function testLockingMechanism(): void
112+
{
113+
$key = 'test_lock_key';
114+
$this->assertTrue($this->cache->delete($key));
115+
116+
$t = microtime(true);
117+
// run another script in background
118+
exec('php ' . __DIR__ . '/lock.php > /dev/null 2>&1 &');
119+
120+
if ($this->cache->lock($key)) {
121+
$this->assertGreaterThan(3.0, microtime(true) - $t);
122+
$this->assertEquals(123, $this->cache->get($key));
123+
$this->cache->unlock($key);
124+
}
125+
}
126+
127+
public function testShutdown(): void
128+
{
129+
$key = 'test_lock_key';
130+
$this->assertTrue($this->cache->delete($key));
131+
132+
$descriptorspec = array(
133+
0 => array("pipe", "r"),
134+
1 => array("pipe", "w"),
135+
);
136+
137+
$t = microtime(true);
138+
// Run another script in background
139+
$proc = proc_open('php ' . __DIR__ . DIRECTORY_SEPARATOR . 'lock.php', $descriptorspec, $pipes);
140+
$pid = proc_get_status($proc)['pid'];
141+
sleep(1);
142+
143+
// Terminate the process
144+
if (strncasecmp(PHP_OS, 'WIN', 3) === 0) {
145+
exec("taskkill /F /PID $pid /T");
146+
} else {
147+
exec("kill -9 $pid");
148+
}
149+
150+
proc_close($proc);
151+
152+
$this->assertLessThan(3.0, microtime(true) - $t);
153+
$this->assertTrue($this->cache->has($key));
154+
$this->assertNull($this->cache->get($key));
155+
$this->assertTrue($this->cache->lock($key));
156+
$this->cache->unlock($key);
157+
}
158+
159+
public function testInvalidKey(): void
160+
{
161+
$this->expectException(\Psr\SimpleCache\InvalidArgumentException::class);
162+
$this->expectExceptionMessage('Invalid cache key: invalid key!. Allowed characters are A-Za-z0-9_.');
163+
$this->cache->set('invalid key!', 'value');
164+
}
165+
}

0 commit comments

Comments
 (0)