1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | /* |
||||
6 | * This file is part of BiuradPHP opensource projects. |
||||
7 | * |
||||
8 | * PHP version 7.1 and above required |
||||
9 | * |
||||
10 | * @author Divine Niiquaye Ibok <[email protected]> |
||||
11 | * @copyright 2019 Biurad Group (https://biurad.com/) |
||||
12 | * @license https://opensource.org/licenses/BSD-3-Clause License |
||||
13 | * |
||||
14 | * For the full copyright and license information, please view the LICENSE |
||||
15 | * file that was distributed with this source code. |
||||
16 | */ |
||||
17 | |||||
18 | namespace BiuradPHP\Cache; |
||||
19 | |||||
20 | use BadMethodCallException; |
||||
21 | use Closure; |
||||
22 | use Exception; |
||||
23 | use Generator; |
||||
24 | use Psr\Cache\CacheItemInterface; |
||||
25 | use Psr\Cache\CacheItemPoolInterface; |
||||
26 | use Psr\SimpleCache\CacheInterface; |
||||
27 | use stdClass; |
||||
28 | |||||
29 | class CacheItemPool implements CacheItemPoolInterface |
||||
30 | { |
||||
31 | /** |
||||
32 | * @var CacheInterface |
||||
33 | */ |
||||
34 | protected $pool; |
||||
35 | |||||
36 | /** |
||||
37 | * @var null|int The maximum length to enforce for identifiers or null when no limit applies |
||||
38 | */ |
||||
39 | protected $maxIdLength; |
||||
40 | |||||
41 | /** |
||||
42 | * @var Closure needs to be set by class, signature is function(string <key>, mixed <value>, bool <isHit>) |
||||
43 | */ |
||||
44 | private $createCacheItem; |
||||
45 | |||||
46 | /** |
||||
47 | * @var Closure needs to be set by class, signature is function(array <deferred>, array <&expiredIds>) |
||||
48 | */ |
||||
49 | private $mergeByLifetime; |
||||
50 | |||||
51 | /** @var array<string,CacheItemInterface> */ |
||||
52 | private $deferred = []; |
||||
53 | |||||
54 | /** @var array<string,string> */ |
||||
55 | private $ids = []; |
||||
56 | |||||
57 | /** @var stdclass */ |
||||
58 | private $miss; |
||||
59 | |||||
60 | /** |
||||
61 | * Cache Constructor. |
||||
62 | * |
||||
63 | * @param CacheInterface $psr16 |
||||
64 | */ |
||||
65 | public function __construct(CacheInterface $psr16) |
||||
66 | { |
||||
67 | $this->pool = $psr16; |
||||
68 | $this->miss = new stdClass(); |
||||
69 | |||||
70 | $this->createCacheItem = Closure::bind( |
||||
71 | static function ($key, $value, $isHit) { |
||||
72 | $item = new CacheItem(); |
||||
73 | $item->key = $key; |
||||
74 | $item->value = $v = $value; |
||||
75 | $item->isHit = $isHit; |
||||
76 | $item->defaultLifetime = 0; |
||||
77 | // Detect wrapped values that encode for their expiry and creation duration |
||||
78 | // For compactness, these values are packed in the key of an array using |
||||
79 | // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F |
||||
80 | if ( |
||||
81 | \is_array($v) && |
||||
82 | 1 === \count($v) && |
||||
83 | 10 === \strlen($k = (string) \key($v)) && |
||||
84 | "\x9D" === $k[0] && |
||||
85 | "\0" === $k[5] && |
||||
86 | "\x5F" === $k[9] |
||||
87 | ) { |
||||
88 | $item->value = $v[$k]; |
||||
89 | } |
||||
90 | |||||
91 | return $item; |
||||
92 | }, |
||||
93 | null, |
||||
94 | CacheItem::class |
||||
95 | ); |
||||
96 | $getId = Closure::fromCallable([$this, 'getId']); |
||||
97 | $this->mergeByLifetime = Closure::bind( |
||||
98 | static function ($deferred, &$expiredIds) use ($getId) { |
||||
99 | $byLifetime = []; |
||||
100 | $now = \microtime(true); |
||||
101 | $expiredIds = []; |
||||
102 | |||||
103 | foreach ($deferred as $key => $item) { |
||||
104 | $key = (string) $key; |
||||
105 | |||||
106 | if (null === $item->expiry) { |
||||
107 | $ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0; |
||||
108 | } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) { |
||||
109 | $expiredIds[] = $getId($key); |
||||
110 | |||||
111 | continue; |
||||
112 | } |
||||
113 | |||||
114 | // For compactness, expiry and creation duration are packed in the key of an array, |
||||
115 | // using magic numbers as separators |
||||
116 | $byLifetime[$ttl][$getId($key)] = $item->value; |
||||
117 | } |
||||
118 | |||||
119 | return $byLifetime; |
||||
120 | }, |
||||
121 | null, |
||||
122 | CacheItem::class |
||||
123 | ); |
||||
124 | } |
||||
125 | |||||
126 | public function __destruct() |
||||
127 | { |
||||
128 | if ($this->deferred) { |
||||
0 ignored issues
–
show
|
|||||
129 | $this->commit(); |
||||
130 | } |
||||
131 | } |
||||
132 | |||||
133 | public function __sleep(): void |
||||
134 | { |
||||
135 | throw new BadMethodCallException('Cannot serialize ' . __CLASS__); |
||||
136 | } |
||||
137 | |||||
138 | public function __wakeup(): void |
||||
139 | { |
||||
140 | throw new BadMethodCallException('Cannot unserialize ' . __CLASS__); |
||||
141 | } |
||||
142 | |||||
143 | /** |
||||
144 | * {@inheritdoc} |
||||
145 | */ |
||||
146 | public function getItem($key) |
||||
147 | { |
||||
148 | if ($this->deferred) { |
||||
0 ignored issues
–
show
The expression
$this->deferred of type array<string,Psr\Cache\CacheItemInterface> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||||
149 | $this->commit(); |
||||
150 | } |
||||
151 | $id = $this->getId($key); |
||||
152 | |||||
153 | $f = $this->createCacheItem; |
||||
154 | $isHit = false; |
||||
155 | $value = null; |
||||
156 | |||||
157 | try { |
||||
158 | foreach ($this->doFetch([$id]) as $value) { |
||||
159 | $isHit = true; |
||||
160 | } |
||||
161 | |||||
162 | return $f($key, $value, $isHit); |
||||
163 | } catch (Exception $e) { |
||||
164 | return $f($key, null, false); |
||||
165 | } |
||||
166 | } |
||||
167 | |||||
168 | /** |
||||
169 | * {@inheritdoc} |
||||
170 | * |
||||
171 | * @return iterable<string,CacheItemInterface> |
||||
172 | */ |
||||
173 | public function getItems(array $keys = []) |
||||
174 | { |
||||
175 | if ($this->deferred) { |
||||
0 ignored issues
–
show
The expression
$this->deferred of type array<string,Psr\Cache\CacheItemInterface> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||||
176 | $this->commit(); |
||||
177 | } |
||||
178 | $kIds = []; |
||||
179 | |||||
180 | foreach ($keys as $key) { |
||||
181 | $kIds[] = $this->getId($key); |
||||
182 | } |
||||
183 | |||||
184 | $items = $this->doFetch($kIds); |
||||
185 | $kIds = \array_combine($kIds, $keys); |
||||
186 | |||||
187 | return $this->generateItems($items, $kIds); |
||||
0 ignored issues
–
show
It seems like
$kIds can also be of type false ; however, parameter $keys of BiuradPHP\Cache\CacheItemPool::generateItems() does only seem to accept 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
Loading history...
|
|||||
188 | } |
||||
189 | |||||
190 | /** |
||||
191 | * {@inheritdoc} |
||||
192 | */ |
||||
193 | public function hasItem($key) |
||||
194 | { |
||||
195 | $id = $this->getId($key); |
||||
196 | |||||
197 | if (isset($this->deferred[$key])) { |
||||
198 | $this->commit(); |
||||
199 | } |
||||
200 | |||||
201 | return $this->doHave($id); |
||||
202 | } |
||||
203 | |||||
204 | /** |
||||
205 | * {@inheritdoc} |
||||
206 | */ |
||||
207 | public function clear() |
||||
208 | { |
||||
209 | $this->deferred = []; |
||||
210 | |||||
211 | return $this->doClear(); |
||||
212 | } |
||||
213 | |||||
214 | /** |
||||
215 | * {@inheritdoc} |
||||
216 | */ |
||||
217 | public function deleteItem($key) |
||||
218 | { |
||||
219 | return $this->deleteItems([$key]); |
||||
220 | } |
||||
221 | |||||
222 | /** |
||||
223 | * {@inheritdoc} |
||||
224 | */ |
||||
225 | public function deleteItems(array $keys) |
||||
226 | { |
||||
227 | $kIds = []; |
||||
228 | |||||
229 | foreach ($keys as $key) { |
||||
230 | $kIds[$key] = $this->getId($key); |
||||
231 | unset($this->deferred[$key]); |
||||
232 | } |
||||
233 | |||||
234 | return $this->doDelete($kIds); |
||||
235 | } |
||||
236 | |||||
237 | /** |
||||
238 | * {@inheritdoc} |
||||
239 | */ |
||||
240 | public function save(CacheItemInterface $item) |
||||
241 | { |
||||
242 | $this->saveDeferred($item); |
||||
243 | |||||
244 | return $this->commit(); |
||||
245 | } |
||||
246 | |||||
247 | /** |
||||
248 | * {@inheritdoc} |
||||
249 | */ |
||||
250 | public function saveDeferred(CacheItemInterface $item) |
||||
251 | { |
||||
252 | $this->deferred[$item->getKey()] = $item; |
||||
253 | |||||
254 | return true; |
||||
255 | } |
||||
256 | |||||
257 | /** |
||||
258 | * {@inheritdoc} |
||||
259 | */ |
||||
260 | public function commit() |
||||
261 | { |
||||
262 | $ok = true; |
||||
263 | $byLifetime = $this->mergeByLifetime; |
||||
264 | $this->deferred = $expiredIds = []; |
||||
265 | $byLifetime = $byLifetime($this->deferred, $expiredIds); |
||||
266 | |||||
267 | if (!empty($expiredIds)) { |
||||
268 | $this->doDelete($expiredIds); |
||||
269 | } |
||||
270 | |||||
271 | foreach ($byLifetime as $lifetime => $values) { |
||||
272 | if ($this->doSave($values, $lifetime)) { |
||||
273 | continue; |
||||
274 | } |
||||
275 | |||||
276 | $ok = false; |
||||
277 | } |
||||
278 | |||||
279 | return $ok; |
||||
280 | } |
||||
281 | |||||
282 | /** |
||||
283 | * Fetches several cache items. |
||||
284 | * |
||||
285 | * @param array<mixed,string> $ids The cache identifiers to fetch |
||||
286 | * |
||||
287 | * @return iterable<string,CacheItemInterface> The corresponding values found in the cache |
||||
288 | */ |
||||
289 | protected function doFetch(array $ids) |
||||
290 | { |
||||
291 | $fetched = $this->pool->getMultiple($ids, $this->miss); |
||||
292 | |||||
293 | if ($fetched instanceof Generator) { |
||||
294 | $fetched = $fetched->getReturn(); |
||||
295 | } |
||||
296 | |||||
297 | foreach ($fetched as $key => $value) { |
||||
298 | if ($this->miss !== $value) { |
||||
299 | yield $key => $value; |
||||
300 | } |
||||
301 | } |
||||
302 | } |
||||
303 | |||||
304 | /** |
||||
305 | * Confirms if the cache contains specified cache item. |
||||
306 | * |
||||
307 | * @param string $id The identifier for which to check existence |
||||
308 | * |
||||
309 | * @return bool True if item exists in the cache, false otherwise |
||||
310 | */ |
||||
311 | protected function doHave(string $id) |
||||
312 | { |
||||
313 | return $this->pool->has($id); |
||||
314 | } |
||||
315 | |||||
316 | /** |
||||
317 | * Deletes all items in the pool. |
||||
318 | * |
||||
319 | * @return bool True if the pool was successfully cleared, false otherwise |
||||
320 | */ |
||||
321 | protected function doClear() |
||||
322 | { |
||||
323 | return $this->pool->clear(); |
||||
324 | } |
||||
325 | |||||
326 | /** |
||||
327 | * Removes multiple items from the pool. |
||||
328 | * |
||||
329 | * @param array<string,string> $ids An array of identifiers that should be removed from the pool |
||||
330 | * |
||||
331 | * @return bool True if the items were successfully removed, false otherwise |
||||
332 | */ |
||||
333 | protected function doDelete(array $ids) |
||||
334 | { |
||||
335 | return $this->pool->deleteMultiple($ids); |
||||
336 | } |
||||
337 | |||||
338 | /** |
||||
339 | * Persists several cache items immediately. |
||||
340 | * |
||||
341 | * @param array<string,mixed> $values The values to cache, indexed by their cache identifier |
||||
342 | * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning |
||||
343 | * |
||||
344 | * @return bool a boolean stating if caching succeeded or not |
||||
345 | */ |
||||
346 | protected function doSave(array $values, int $lifetime) |
||||
347 | { |
||||
348 | return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); |
||||
349 | } |
||||
350 | |||||
351 | /** |
||||
352 | * @param iterable<string,mixed> $items |
||||
353 | * @param array<string,string> $keys |
||||
354 | * |
||||
355 | * @return iterable<string,CacheItemInterface> |
||||
356 | */ |
||||
357 | private function generateItems(iterable $items, array &$keys): iterable |
||||
358 | { |
||||
359 | $f = $this->createCacheItem; |
||||
360 | |||||
361 | foreach ($items as $id => $value) { |
||||
362 | if (!isset($keys[$id])) { |
||||
363 | $id = \key($keys); |
||||
364 | } |
||||
365 | $key = $keys[$id]; |
||||
366 | unset($keys[$id]); |
||||
367 | |||||
368 | yield $key => $f($key, $value, true); |
||||
369 | } |
||||
370 | |||||
371 | foreach ($keys as $key) { |
||||
372 | yield $key => $f($key, null, false); |
||||
373 | } |
||||
374 | } |
||||
375 | |||||
376 | /** |
||||
377 | * @param mixed $key |
||||
378 | * |
||||
379 | * @return string |
||||
380 | */ |
||||
381 | private function getId($key) |
||||
382 | { |
||||
383 | if (\is_string($key) && isset($this->ids[$key])) { |
||||
384 | return $this->ids[$key]; |
||||
385 | } |
||||
386 | CacheItem::validateKey($key); |
||||
387 | $this->ids[$key] = $key; |
||||
388 | |||||
389 | return $key; |
||||
390 | } |
||||
391 | } |
||||
392 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.