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