Passed
Push — master ( b0ec1a...8763d4 )
by Divine Niiquaye
01:43
created

src/CacheItemPool.php (4 issues)

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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

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 ignore-type  annotation

187
        return $this->generateItems($items, /** @scrutinizer ignore-type */ $kIds);
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