1 | <?php |
||||
2 | |||||
3 | /* |
||||
4 | * Created by Fernando Robledo <[email protected]>. |
||||
5 | */ |
||||
6 | |||||
7 | namespace Overdesign\PsrCache; |
||||
8 | |||||
9 | use Psr\Cache\CacheItemInterface; |
||||
10 | use Psr\Cache\CacheItemPoolInterface; |
||||
11 | |||||
12 | class FileCacheDriver implements CacheItemPoolInterface |
||||
13 | { |
||||
14 | /** @var string */ |
||||
15 | protected $path; |
||||
16 | /** @var CacheItem[] */ |
||||
17 | protected $deferred = array(); |
||||
18 | |||||
19 | /** |
||||
20 | * FileCacheDriver constructor. |
||||
21 | * |
||||
22 | * @param string $path |
||||
23 | */ |
||||
24 | public function __construct($path = __DIR__) |
||||
25 | { |
||||
26 | $this->path = substr($path, strlen($path) - 1, 1) === DIRECTORY_SEPARATOR ? $path : $path . DIRECTORY_SEPARATOR; |
||||
27 | } |
||||
28 | |||||
29 | /** |
||||
30 | * Returns current cache folder path |
||||
31 | * |
||||
32 | * @return string |
||||
33 | */ |
||||
34 | public function getPath() |
||||
35 | { |
||||
36 | return $this->path; |
||||
37 | } |
||||
38 | |||||
39 | /** |
||||
40 | * @param string $key |
||||
41 | * |
||||
42 | * @return string hashed key |
||||
43 | * @throws InvalidArgumentException |
||||
44 | */ |
||||
45 | private function checkKey($key) |
||||
46 | { |
||||
47 | if (!is_string($key)) { |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
48 | throw new InvalidArgumentException('The given key must be a string.'); |
||||
49 | } elseif (!preg_match('/^[a-zA-Z\d\.\_]+$/', $key)) { |
||||
50 | throw new InvalidArgumentException(sprintf('The given key %s contains invalid characters.', $key)); |
||||
51 | } |
||||
52 | |||||
53 | return $key; |
||||
54 | } |
||||
55 | |||||
56 | /** |
||||
57 | * @param string $key |
||||
58 | * @param bool $hash |
||||
59 | * |
||||
60 | * @return string |
||||
61 | */ |
||||
62 | private function getFilename($key, $hash = true) |
||||
63 | { |
||||
64 | $key = $hash ? sha1($key) : $key; |
||||
65 | return $this->path . "cachepool-{$key}.php"; |
||||
66 | } |
||||
67 | |||||
68 | /** |
||||
69 | * Returns a Cache Item representing the specified key. |
||||
70 | * |
||||
71 | * This method must always return a CacheItemInterface object, even in case of |
||||
72 | * a cache miss. It MUST NOT return null. |
||||
73 | * |
||||
74 | * @param string $key |
||||
75 | * The key for which to return the corresponding Cache Item. |
||||
76 | * |
||||
77 | * @throws InvalidArgumentException |
||||
78 | * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException |
||||
79 | * MUST be thrown. |
||||
80 | * |
||||
81 | * @return CacheItemInterface |
||||
82 | * The corresponding Cache Item. |
||||
83 | */ |
||||
84 | public function getItem($key) |
||||
85 | { |
||||
86 | $key = $this->checkKey($key); |
||||
87 | |||||
88 | if (array_key_exists($key, $this->deferred)) { |
||||
89 | return clone $this->deferred[$key]; |
||||
90 | } |
||||
91 | |||||
92 | $file = $this->getFilename($key); |
||||
93 | |||||
94 | return $this->readCacheFile($file) ?: new CacheItem($key); |
||||
95 | } |
||||
96 | |||||
97 | /** |
||||
98 | * @param string $file filename |
||||
99 | * |
||||
100 | * @return false|CacheItem |
||||
101 | * @throws InvalidArgumentException |
||||
102 | */ |
||||
103 | private function readCacheFile($file) |
||||
104 | { |
||||
105 | if (!is_file($file) || !is_readable($file)) { |
||||
106 | return false; |
||||
107 | } |
||||
108 | |||||
109 | $item = file_get_contents($file); |
||||
110 | $item = $item === false ? false : unserialize($item); |
||||
111 | |||||
112 | if ($item === false || !$item instanceof CacheItem) { |
||||
113 | return false; |
||||
114 | } |
||||
115 | |||||
116 | if ($item->isHit() === false) { |
||||
117 | $this->deleteItem($item->getKey()); // clear expired |
||||
118 | } |
||||
119 | |||||
120 | return $item; |
||||
121 | } |
||||
122 | |||||
123 | /** |
||||
124 | * Gets a cache file by key hash |
||||
125 | * |
||||
126 | * @param string $hash sha1 key hash |
||||
127 | * |
||||
128 | * @return CacheItem |
||||
129 | * |
||||
130 | * @throws InvalidArgumentException |
||||
131 | */ |
||||
132 | private function getItemByHash($hash) |
||||
133 | { |
||||
134 | $file = $this->getFilename($hash, false); |
||||
135 | |||||
136 | return $this->readCacheFile($file) ?: new CacheItem($key); |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
137 | } |
||||
138 | |||||
139 | /** |
||||
140 | * Returns a traversable set of cache items. |
||||
141 | * |
||||
142 | * @param string[] $keys |
||||
143 | * An indexed array of keys of items to retrieve. |
||||
144 | * |
||||
145 | * @throws InvalidArgumentException |
||||
146 | * If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException |
||||
147 | * MUST be thrown. |
||||
148 | * |
||||
149 | * @return array|\Traversable |
||||
150 | * A traversable collection of Cache Items keyed by the cache keys of |
||||
151 | * each item. A Cache item will be returned for each key, even if that |
||||
152 | * key is not found. However, if no keys are specified then an empty |
||||
153 | * traversable MUST be returned instead. |
||||
154 | */ |
||||
155 | public function getItems(array $keys = array()) |
||||
156 | { |
||||
157 | $collection = array(); |
||||
158 | |||||
159 | foreach ($keys as $key) { |
||||
160 | $collection[$key] = $this->getItem($key); |
||||
161 | } |
||||
162 | |||||
163 | return $collection; |
||||
164 | } |
||||
165 | |||||
166 | /** |
||||
167 | * Confirms if the cache contains specified cache item. |
||||
168 | * |
||||
169 | * Note: This method MAY avoid retrieving the cached value for performance reasons. |
||||
170 | * This could result in a race condition with CacheItemInterface::get(). To avoid |
||||
171 | * such situation use CacheItemInterface::isHit() instead. |
||||
172 | * |
||||
173 | * @param string $key |
||||
174 | * The key for which to check existence. |
||||
175 | * |
||||
176 | * @throws InvalidArgumentException |
||||
177 | * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException |
||||
178 | * MUST be thrown. |
||||
179 | * |
||||
180 | * @return bool |
||||
181 | * True if item exists in the cache, false otherwise. |
||||
182 | */ |
||||
183 | public function hasItem($key) |
||||
184 | { |
||||
185 | return $this->getItem($key)->isHit(); |
||||
186 | } |
||||
187 | |||||
188 | /** |
||||
189 | * Deletes all items in the pool. |
||||
190 | * |
||||
191 | * @return bool |
||||
192 | * True if the pool was successfully cleared. False if there was an error. |
||||
193 | */ |
||||
194 | public function clear() |
||||
195 | { |
||||
196 | $this->deferred = array(); |
||||
197 | |||||
198 | $files = glob($this->path . 'cachepool-*.php', GLOB_NOSORT); |
||||
199 | $result = true; |
||||
200 | |||||
201 | foreach ($files as $file) { |
||||
202 | if (is_file($file)) { |
||||
203 | $result = @unlink($file) && $result; |
||||
204 | } |
||||
205 | } |
||||
206 | |||||
207 | return $result; |
||||
208 | } |
||||
209 | |||||
210 | /** |
||||
211 | * Removes the item from the pool. |
||||
212 | * |
||||
213 | * @param string $key |
||||
214 | * The key to delete. |
||||
215 | * |
||||
216 | * @throws InvalidArgumentException |
||||
217 | * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException |
||||
218 | * MUST be thrown. |
||||
219 | * |
||||
220 | * @return bool |
||||
221 | * True if the item was successfully removed. False if there was an error. |
||||
222 | */ |
||||
223 | public function deleteItem($key) |
||||
224 | { |
||||
225 | $key = $this->checkKey($key); |
||||
226 | |||||
227 | if (array_key_exists($key, $this->deferred)) { |
||||
228 | unset($this->deferred[$key]); |
||||
229 | } |
||||
230 | |||||
231 | $file = $this->getFilename($key); |
||||
232 | |||||
233 | return file_exists($file) ? @unlink($file) : true; |
||||
234 | } |
||||
235 | |||||
236 | /** |
||||
237 | * Removes multiple items from the pool. |
||||
238 | * |
||||
239 | * @param string[] $keys |
||||
240 | * An array of keys that should be removed from the pool. |
||||
241 | * |
||||
242 | * @throws InvalidArgumentException |
||||
243 | * If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException |
||||
244 | * MUST be thrown. |
||||
245 | * |
||||
246 | * @return bool |
||||
247 | * True if the items were successfully removed. False if there was an error. |
||||
248 | */ |
||||
249 | public function deleteItems(array $keys) |
||||
250 | { |
||||
251 | $result = true; |
||||
252 | |||||
253 | foreach ($keys as $key) { |
||||
254 | $result = $this->deleteItem($key) && $result; |
||||
255 | } |
||||
256 | |||||
257 | return $result; |
||||
258 | } |
||||
259 | |||||
260 | /** |
||||
261 | * Persists a cache item immediately. |
||||
262 | * |
||||
263 | * @param CacheItemInterface $item |
||||
264 | * The cache item to save. |
||||
265 | * |
||||
266 | * @return bool True if the item was successfully persisted. False if there was an error. |
||||
267 | * True if the item was successfully persisted. False if there was an error. |
||||
268 | * |
||||
269 | * @throws CacheException |
||||
270 | */ |
||||
271 | public function save(CacheItemInterface $item) |
||||
272 | { |
||||
273 | $file = $this->getFilename($item->getKey()); |
||||
274 | |||||
275 | if (false === file_put_contents($file, serialize($item))) { |
||||
276 | throw new CacheException(sprintf('Cant write to cache file %s', $file), CacheException::ERROR_CANT_WRITE); |
||||
277 | } |
||||
278 | |||||
279 | return true; |
||||
280 | } |
||||
281 | |||||
282 | /** |
||||
283 | * Sets a cache item to be persisted later. |
||||
284 | * |
||||
285 | * @param CacheItemInterface $item |
||||
286 | * The cache item to save. |
||||
287 | * |
||||
288 | * @return bool |
||||
289 | * False if the item could not be queued or if a commit was attempted and failed. True otherwise. |
||||
290 | */ |
||||
291 | public function saveDeferred(CacheItemInterface $item) |
||||
292 | { |
||||
293 | $this->deferred[$item->getKey()] = $item; |
||||
294 | |||||
295 | return true; |
||||
296 | } |
||||
297 | |||||
298 | /** |
||||
299 | * Persists any deferred cache items. |
||||
300 | * |
||||
301 | * @return bool True if all not-yet-saved items were successfully saved or there were none. False otherwise. |
||||
302 | * True if all not-yet-saved items were successfully saved or there were none. False otherwise. |
||||
303 | * |
||||
304 | * @throws CacheException |
||||
305 | */ |
||||
306 | public function commit() |
||||
307 | { |
||||
308 | $allSaved = true; |
||||
309 | |||||
310 | foreach ($this->deferred as $item) { |
||||
311 | $allSaved = $this->save($item) && $allSaved; |
||||
312 | unset($this->deferred[$item->getKey()]); |
||||
313 | } |
||||
314 | |||||
315 | return $allSaved; |
||||
316 | } |
||||
317 | |||||
318 | /** |
||||
319 | * Deletes all the expired items in the pool. |
||||
320 | * |
||||
321 | * @return bool |
||||
322 | * True if the pool was successfully cleared. False if there was an error. |
||||
323 | */ |
||||
324 | public function clearExpired() |
||||
325 | { |
||||
326 | $regex = '/cachepool-(?P<hash>[0-9a-f]+)\.php$/'; |
||||
327 | $files = glob($this->path . 'cachepool-*.php', GLOB_NOSORT); |
||||
328 | $result = true; |
||||
329 | |||||
330 | foreach ($files as $file) { |
||||
331 | if (preg_match($regex, $file, $hash) === false) { |
||||
332 | continue; |
||||
333 | } |
||||
334 | |||||
335 | try { |
||||
336 | $this->getItemByHash($hash['hash']); // Get item auto clears expired items |
||||
337 | } catch (InvalidArgumentException $e) { |
||||
338 | $result = false; |
||||
339 | } |
||||
340 | } |
||||
341 | |||||
342 | return $result; |
||||
343 | } |
||||
344 | |||||
345 | /** |
||||
346 | * Cache pool garbage collector, deletes all cache files and optional empty directories in the current cache path |
||||
347 | * It can do a recursive search over the main directory, the maximum deep for the recursive can be specified |
||||
348 | * |
||||
349 | * @param bool $checkExpired if true only deletes the items that are expired else deletes all cached files |
||||
350 | * @param bool $recursive true for doing a recursive gc over the main path |
||||
351 | * @param bool $deleteEmpty true for deleting empty directories inside the path |
||||
352 | * @param int $depth maximum search depth |
||||
353 | * |
||||
354 | * @return bool True if all the items where deleted |
||||
355 | * |
||||
356 | * @throws InvalidArgumentException |
||||
357 | */ |
||||
358 | public function gc($checkExpired = true, $recursive = false, $deleteEmpty = true, $depth = 1) |
||||
359 | { |
||||
360 | $recursive = $recursive && $depth > 0; |
||||
361 | $result = $checkExpired ? $this->clearExpired() : $this->clear(); |
||||
362 | |||||
363 | if (!$recursive) { |
||||
364 | return $result; |
||||
365 | } |
||||
366 | |||||
367 | $dirs = glob($this->path . '*', GLOB_ONLYDIR | GLOB_NOSORT); |
||||
368 | |||||
369 | foreach ($dirs as $dir) { |
||||
370 | $pool = new self($dir); |
||||
371 | $result = $result && $pool->gc($recursive, $deleteEmpty, --$depth); |
||||
0 ignored issues
–
show
--$depth of type integer is incompatible with the type boolean expected by parameter $deleteEmpty of Overdesign\PsrCache\FileCacheDriver::gc() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
372 | |||||
373 | if ($deleteEmpty && count(glob($dir . DIRECTORY_SEPARATOR . '*', GLOB_NOSORT)) === 0) { |
||||
0 ignored issues
–
show
It seems like
glob($dir . Overdesign\P...n\PsrCache\GLOB_NOSORT) can also be of type false ; however, parameter $var of count() does only seem to accept Countable|array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
374 | rmdir($dir); |
||||
375 | } |
||||
376 | } |
||||
377 | |||||
378 | return $result; |
||||
379 | } |
||||
380 | |||||
381 | /** |
||||
382 | * Save deferred items before destruct |
||||
383 | */ |
||||
384 | public function __destruct() |
||||
385 | { |
||||
386 | $this->commit(); |
||||
387 | } |
||||
388 | } |
||||
389 |