Passed
Push — master ( c82647...c877fa )
by Georges
11:01
created

CacheItemPoolTrait   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 146
dl 0
loc 455
rs 6
c 3
b 0
f 0
wmc 55

How to fix   Complexity   

Complex Class

Complex classes like CacheItemPoolTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CacheItemPoolTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 *
5
 * This file is part of Phpfastcache.
6
 *
7
 * @license MIT License (MIT)
8
 *
9
 * For full copyright and license information, please see the docs/CREDITS.txt and LICENCE files.
10
 *
11
 * @author Georges.L (Geolim4)  <[email protected]>
12
 * @author Contributors  https://github.com/PHPSocialNetwork/phpfastcache/graphs/contributors
13
 */
14
15
declare(strict_types=1);
16
17
namespace Phpfastcache\Core\Pool;
18
19
use DateTime;
20
use Phpfastcache\Config\ConfigurationOptionInterface;
21
use Phpfastcache\Config\IOConfigurationOptionInterface;
22
use Phpfastcache\Core\Item\ExtendedCacheItemInterface;
23
use Phpfastcache\Entities\DriverIO;
24
use Phpfastcache\Entities\ItemBatch;
25
use Phpfastcache\Event\Event;
26
use Phpfastcache\Event\EventManagerInterface;
27
use Phpfastcache\Event\EventReferenceParameter;
28
use Phpfastcache\Exceptions\PhpfastcacheCoreException;
29
use Phpfastcache\Exceptions\PhpfastcacheDriverException;
30
use Phpfastcache\Exceptions\PhpfastcacheInvalidArgumentException;
31
use Phpfastcache\Exceptions\PhpfastcacheInvalidTypeException;
32
use Phpfastcache\Exceptions\PhpfastcacheIOException;
33
use Phpfastcache\Exceptions\PhpfastcacheLogicException;
34
use Phpfastcache\Exceptions\PhpfastcacheUnsupportedMethodException;
35
use Psr\Cache\CacheItemInterface;
36
use RuntimeException;
37
38
/**
39
 * @method string[] driverUnwrapTags(array $wrapper)
40
 * @method void cleanItemTags(ExtendedCacheItemInterface $item)
41
 */
42
trait CacheItemPoolTrait
43
{
44
    use DriverBaseTrait {
45
        DriverBaseTrait::__construct as __driverBaseConstruct;
46
    }
47
48
    /**
49
     * @var string
50
     */
51
    protected static string $unsupportedKeyChars = '{}()/\@:';
52
53
    /**
54
     * @var ExtendedCacheItemInterface[]|CacheItemInterface[]
55
     */
56
    protected array $deferredList = [];
57
58
    /**
59
     * @var ExtendedCacheItemInterface[]|CacheItemInterface[]
60
     */
61
    protected array $itemInstances = [];
62
63
    protected DriverIO $IO;
64
65
    public function __construct(#[\SensitiveParameter] ConfigurationOptionInterface $config, string $instanceId, EventManagerInterface $em)
66
    {
67
        $this->IO = new DriverIO();
68
        $this->__driverBaseConstruct($config, $instanceId, $em);
69
    }
70
71
    /**
72
     * @throws PhpfastcacheLogicException
73
     * @throws PhpfastcacheInvalidArgumentException
74
     */
75
    public function setItem(CacheItemInterface $item): static
76
    {
77
        if (self::getItemClass() === $item::class) {
78
            if (!$this->getConfig()->isUseStaticItemCaching()) {
79
                throw new PhpfastcacheLogicException(
80
                    'The static item caching option (useStaticItemCaching) is disabled so you cannot attach an item.'
81
                );
82
            }
83
84
            $this->itemInstances[$item->getKey()] = $item;
85
86
            return $this;
87
        }
88
        throw new PhpfastcacheInvalidArgumentException(
89
            \sprintf(
90
                'Invalid cache item class "%s" for driver "%s".',
91
                get_class($item),
92
                get_class($this)
93
            )
94
        );
95
    }
96
97
    /**
98
     * @inheritDoc
99
     * @throws PhpfastcacheCoreException
100
     * @throws PhpfastcacheDriverException
101
     * @throws PhpfastcacheInvalidArgumentException
102
     * @throws PhpfastcacheLogicException
103
     */
104
    public function getItems(array $keys = []): iterable
105
    {
106
        $items = [];
107
108
        /**
109
         * Usually, drivers that are able to enable cache slams
110
         * does not benefit of driverReadMultiple() call.
111
         */
112
        if (!$this->getConfig()->isPreventCacheSlams()) {
113
            $this->validateCacheKeys(...$keys);
114
115
            /**
116
             * Check for local item instances first.
117
             */
118
            foreach ($keys as $index => $key) {
119
                if (isset($this->itemInstances[$key]) && $this->getConfig()->isUseStaticItemCaching()) {
120
                    $items[$key] = $this->itemInstances[$key];
121
                    // Key already exists in local item instances, no need to fetch it again.
122
                    unset($keys[$index]);
123
                }
124
            }
125
            $keys = array_values($keys);
126
127
            /**
128
             * If there's still keys to fetch, let's choose the right method (if supported).
129
             */
130
            if (count($keys) > 1) {
131
                $items = array_merge(
132
                    array_combine($keys, array_map(fn($key) => new (self::getItemClass())($this, $key, $this->eventManager), $keys)),
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '(' on line 132 at column 67
Loading history...
133
                    $items
134
                );
135
136
                try {
137
                    $driverArrays = $this->driverReadMultiple(...$items);
138
                } catch (PhpfastcacheUnsupportedMethodException) {
139
                    /**
140
                     * Fallback for drivers that does not yet implement driverReadMultiple() method.
141
                     */
142
                    $driverArrays = array_combine(
143
                        array_map(fn($item) => $item->getKey(), $items),
144
                        array_map(fn($item) => $this->driverRead($item), $items)
145
                    );
146
                } finally {
147
                    foreach ($items as $item) {
148
                        $driverArray = $driverArrays[$item->getKey()] ?? null;
149
                        if ($driverArray !== null) {
150
                            $item->set($this->driverUnwrapData($driverArray));
151
                            $item->expiresAt($this->driverUnwrapEdate($driverArray));
152
                            if ($this->getConfig()->isItemDetailedDate()) {
153
                                /**
154
                                 * If the itemDetailedDate has been
155
                                 * set after caching, we MUST inject
156
                                 * a new DateTime object on the fly
157
                                 */
158
                                $item->setCreationDate($this->driverUnwrapCdate($driverArray) ?: new DateTime());
159
                                $item->setModificationDate($this->driverUnwrapMdate($driverArray) ?: new DateTime());
160
                            }
161
                            $item->setTags($this->driverUnwrapTags($driverArray));
162
                            $this->handleExpiredCacheItem($item);
163
                        } else {
164
                            $item->expiresAfter((int) abs($this->getConfig()->getDefaultTtl()));
165
                        }
166
                        $item->isHit() ? $this->getIO()->incReadHit() : $this->getIO()->incReadMiss();
167
                    }
168
                }
169
            } else {
170
                $index = array_key_first($keys);
171
                if ($index !== null) {
172
                    $items[$keys[$index]] = $this->getItem($keys[$index]);
173
                }
174
            }
175
        } else {
176
            $collection = [];
177
178
            foreach ($keys as $key) {
179
                $collection[$key] = $this->getItem($key);
180
            }
181
182
            return $collection;
183
        }
184
185
        $this->eventManager->dispatch(Event::CACHE_GET_ITEMS, $this, $items);
186
187
        return $items;
188
    }
189
190
    /**
191
     * @param string $key
192
     * @return ExtendedCacheItemInterface
193
     * @throws PhpfastcacheCoreException
194
     * @throws PhpfastcacheInvalidArgumentException
195
     * @throws PhpfastcacheLogicException
196
     * @throws PhpfastcacheDriverException
197
     *
198
     * @SuppressWarnings(PHPMD.NPathComplexity)
199
     * @SuppressWarnings(PHPMD.GotoStatement)
200
     */
201
    public function getItem(string $key): ExtendedCacheItemInterface
202
    {
203
        /**
204
         * Replace array_key_exists by isset
205
         * due to performance issue on huge
206
         * loop dispatching operations
207
         */
208
        if (!isset($this->itemInstances[$key]) || !$this->getConfig()->isUseStaticItemCaching()) {
209
            $this->validateCacheKeys($key);
210
211
            /** @var $item ExtendedCacheItemInterface */
212
            $item = new (self::getItemClass())($this, $key, $this->eventManager);
213
214
            $getItemDriverRead = function (float $cacheSlamsSpendSeconds = 0) use (&$getItemDriverRead, $item): void {
215
                $config = $this->getConfig();
216
217
                $driverArray = $this->driverRead($item);
218
219
                if ($driverArray) {
220
                    $driverData = $this->driverUnwrapData($driverArray);
221
222
                    if ($config instanceof IOConfigurationOptionInterface && $config->isPreventCacheSlams()) {
223
                        while ($driverData instanceof ItemBatch) {
224
                            if ($driverData->getItemDate()->getTimestamp() + $config->getCacheSlamsTimeout() < \time()) {
225
                                /**
226
                                 * The timeout has been reached
227
                                 * Consider that the batch has
228
                                 * failed and serve an empty item
229
                                 * to avoid get stuck with a
230
                                 * batch item stored in driver
231
                                 */
232
                                $this->handleExpiredCacheItem($item);
233
                                return;
234
                            }
235
236
                            $this->eventManager->dispatch(Event::CACHE_GET_ITEM_IN_SLAM_BATCH, $this, $driverData, $cacheSlamsSpendSeconds);
237
238
                            /**
239
                             * Wait for a second before
240
                             * attempting to get exit
241
                             * the current batch process
242
                             */
243
                            \usleep(100000);
244
245
                            $getItemDriverRead($cacheSlamsSpendSeconds + 0.1);
246
                        }
247
                    }
248
249
                    $item->set($driverData);
250
                    $item->expiresAt($this->driverUnwrapEdate($driverArray));
251
252
                    if ($this->getConfig()->isItemDetailedDate()) {
253
                        /**
254
                         * If the itemDetailedDate has been
255
                         * set after caching, we MUST inject
256
                         * a new DateTime object on the fly
257
                         */
258
                        $item->setCreationDate($this->driverUnwrapCdate($driverArray) ?: new DateTime());
259
                        $item->setModificationDate($this->driverUnwrapMdate($driverArray) ?: new DateTime());
260
                    }
261
262
                    $item->setTags($this->driverUnwrapTags($driverArray));
263
                    $this->handleExpiredCacheItem($item);
264
                } else {
265
                    $item->expiresAfter((int) abs($this->getConfig()->getDefaultTtl()));
266
                }
267
            };
268
            $getItemDriverRead();
269
        } else {
270
            $item = $this->itemInstances[$key];
271
        }
272
273
        $this->eventManager->dispatch(Event::CACHE_GET_ITEM, $this, $item);
274
275
        $item->isHit() ? $this->getIO()->incReadHit() : $this->getIO()->incReadMiss();
276
277
        return $item;
278
    }
279
280
    /**
281
     * @param string $key
282
     * @return bool
283
     * @throws PhpfastcacheCoreException
284
     * @throws PhpfastcacheDriverException
285
     * @throws PhpfastcacheInvalidArgumentException
286
     * @throws PhpfastcacheLogicException
287
     */
288
    public function hasItem(string $key): bool
289
    {
290
        return $this->getItem($key)->isHit();
291
    }
292
293
    /**
294
     * @return bool
295
     * @throws PhpfastcacheCoreException
296
     * @throws PhpfastcacheDriverException
297
     * @throws PhpfastcacheLogicException
298
     * @throws PhpfastcacheIOException
299
     */
300
    public function clear(): bool
301
    {
302
        $this->eventManager->dispatch(Event::CACHE_CLEAR_ITEM, $this, $this->itemInstances);
303
304
        $this->getIO()->incWriteHit();
305
        // Faster than detachAllItems()
306
        $this->itemInstances = [];
307
308
        return $this->driverClear();
309
    }
310
311
    /**
312
     * @inheritDoc
313
     * @throws PhpfastcacheCoreException
314
     * @throws PhpfastcacheDriverException
315
     * @throws PhpfastcacheInvalidArgumentException
316
     * @throws PhpfastcacheLogicException
317
     */
318
    public function deleteItems(array $keys): bool
319
    {
320
        if (count($keys) > 1) {
321
            $return = true;
322
            try {
323
                $items = $this->getItems($keys);
324
                $return = $this->driverDeleteMultiple($keys);
325
                foreach ($items as $item) {
326
                    $item->setHit(false);
327
328
                    if (!\str_starts_with($item->getKey(), TaggableCacheItemPoolInterface::DRIVER_TAGS_KEY_PREFIX)) {
329
                        $this->cleanItemTags($item);
330
                    }
331
                }
332
                $this->getIO()->incWriteHit();
333
                $this->eventManager->dispatch(Event::CACHE_DELETE_ITEMS, $this, $items);
334
                $this->deregisterItems($keys);
335
            } catch (PhpfastcacheUnsupportedMethodException) {
336
                foreach ($keys as $key) {
337
                    $result = $this->deleteItem($key);
338
                    if ($result !== true) {
339
                        $return = false;
340
                    }
341
                }
342
            }
343
344
            return $return;
345
        }
346
347
        $index = array_key_first($keys);
348
        if ($index !== null) {
349
            return $this->deleteItem($keys[$index]);
350
        }
351
352
        return false;
353
    }
354
355
    /**
356
     * @param string $key
357
     * @return bool
358
     * @throws PhpfastcacheCoreException
359
     * @throws PhpfastcacheDriverException
360
     * @throws PhpfastcacheInvalidArgumentException
361
     * @throws PhpfastcacheLogicException
362
     */
363
    public function deleteItem(string $key): bool
364
    {
365
        $item = $this->getItem($key);
366
        if ($item->isHit()) {
367
            $result = $this->driverDelete($item->getKey(), $item->getEncodedKey());
368
            $item->setHit(false);
369
            $this->getIO()->incWriteHit();
370
371
            $this->eventManager->dispatch(Event::CACHE_DELETE_ITEM, $this, $item);
372
373
            /**
374
             * De-register the item instance
375
             * then collect gc cycles
376
             */
377
            $this->deregisterItem($key);
378
379
            /**
380
             * Perform a tag cleanup to avoid memory leaks
381
             */
382
            if (!\str_starts_with($key, TaggableCacheItemPoolInterface::DRIVER_TAGS_KEY_PREFIX)) {
383
                $this->cleanItemTags($item);
384
            }
385
386
            return $result;
387
        }
388
389
        return false;
390
    }
391
392
    /**
393
     * @param CacheItemInterface $item
394
     * @return bool
395
     */
396
    public function saveDeferred(CacheItemInterface $item): bool
397
    {
398
        $this->assertCacheItemType($item, self::getItemClass());
399
        if (!\array_key_exists($item->getKey(), $this->itemInstances)) {
400
            $this->itemInstances[$item->getKey()] = $item;
401
        } elseif (\spl_object_hash($item) !== \spl_object_hash($this->itemInstances[$item->getKey()])) {
402
            throw new RuntimeException('Spl object hash mismatches ! You probably tried to save a detached item which has been already retrieved from cache.');
403
        }
404
405
        $this->eventManager->dispatch(Event::CACHE_SAVE_DEFERRED_ITEM, $this, $item);
406
        $this->deferredList[$item->getKey()] = $item;
407
408
        return true;
409
    }
410
411
    /**
412
     * @return bool
413
     * @throws PhpfastcacheCoreException
414
     * @throws PhpfastcacheDriverException
415
     * @throws PhpfastcacheInvalidArgumentException
416
     * @throws PhpfastcacheLogicException
417
     */
418
    public function commit(): bool
419
    {
420
        $this->eventManager->dispatch(Event::CACHE_COMMIT_ITEM, $this, new EventReferenceParameter($this->deferredList));
421
422
        if (\count($this->deferredList)) {
423
            $return = true;
424
            foreach ($this->deferredList as $key => $item) {
425
                $result = $this->save($item);
426
                unset($this->deferredList[$key]);
427
                if ($result !== true) {
428
                    $return = $result;
429
                }
430
            }
431
432
            return $return;
433
        }
434
        return false;
435
    }
436
437
    /**
438
     * @param CacheItemInterface $item
439
     * @return bool
440
     * @throws PhpfastcacheCoreException
441
     * @throws PhpfastcacheDriverException
442
     * @throws PhpfastcacheIOException
443
     * @throws PhpfastcacheInvalidArgumentException
444
     * @throws PhpfastcacheLogicException
445
     */
446
    public function save(CacheItemInterface $item): bool
447
    {
448
        $this->assertCacheItemType($item, self::getItemClass());
449
        /**
450
         * @var ExtendedCacheItemInterface $item
451
         *
452
         * Replace array_key_exists by isset
453
         * due to performance issue on huge
454
         * loop dispatching operations
455
         */
456
        if (!isset($this->itemInstances[$item->getKey()])) {
457
            if ($this->getConfig()->isUseStaticItemCaching()) {
458
                $this->itemInstances[$item->getKey()] = $item;
459
            }
460
        } elseif (\spl_object_hash($item) !== \spl_object_hash($this->itemInstances[$item->getKey()])) {
461
            throw new RuntimeException('Spl object hash mismatches ! You probably tried to save a detached item which has been already retrieved from cache.');
462
        }
463
464
        $this->assertCacheItemType($item, self::getItemClass());
465
        $this->eventManager->dispatch(Event::CACHE_SAVE_ITEM, $this, $item);
466
467
        if ($this->getConfig() instanceof IOConfigurationOptionInterface && $this->getConfig()->isPreventCacheSlams()) {
468
            /**
469
             * @var $itemBatch ExtendedCacheItemInterface
470
             */
471
            $itemClassName = self::getItemClass();
472
            $itemBatch = new $itemClassName($this, $item->getKey(), $this->eventManager);
473
            $itemBatch->set(new ItemBatch($item->getKey(), new DateTime()))
474
                ->expiresAfter($this->getConfig()->getCacheSlamsTimeout());
475
476
            /**
477
             * To avoid SPL mismatches
478
             * we have to re-attach the
479
             * original item to the pool
480
             */
481
            $this->driverWrite($itemBatch);
482
            $this->detachItem($itemBatch);
483
            $this->attachItem($item);
484
        }
485
486
487
        if ($this->driverWrite($item) && $this->driverWriteTags($item)) {
488
            $item->setHit(true)
489
                ->clearRemovedTags();
490
491
            if ($this->getConfig()->isItemDetailedDate()) {
492
                $item->setModificationDate(new \DateTime());
493
            }
494
495
            $this->getIO()->incWriteHit();
496
497
            return true;
498
        }
499
500
        return false;
501
    }
502
503
    /**
504
     * @return DriverIO
505
     */
506
    public function getIO(): DriverIO
507
    {
508
        return $this->IO;
509
    }
510
511
    /**
512
     * @internal This method de-register an item from $this->itemInstances
513
     */
514
    protected function deregisterItem(string $itemKey): static
515
    {
516
        unset($this->itemInstances[$itemKey]);
517
518
        if (\gc_enabled()) {
519
            \gc_collect_cycles();
520
        }
521
522
        return $this;
523
    }
524
525
    /**
526
     * @param string[] $itemKeys
527
     * @internal This method de-register multiple items from $this->itemInstances
528
     */
529
    protected function deregisterItems(array $itemKeys): static
530
    {
531
        $this->itemInstances = array_diff_key($this->itemInstances, array_flip($itemKeys));
532
533
        if (\gc_enabled()) {
534
            \gc_collect_cycles();
535
        }
536
537
        return $this;
538
    }
539
540
    /**
541
     * @throws PhpfastcacheLogicException
542
     */
543
    public function attachItem(CacheItemInterface $item): static
544
    {
545
        if (isset($this->itemInstances[$item->getKey()]) && \spl_object_hash($item) !== \spl_object_hash($this->itemInstances[$item->getKey()])) {
546
            throw new PhpfastcacheLogicException(
547
                'The item already exists and cannot be overwritten because the Spl object hash mismatches ! 
548
                You probably tried to re-attach a detached item which has been already retrieved from cache.'
549
            );
550
        }
551
552
        if (!$this->getConfig()->isUseStaticItemCaching()) {
553
            throw new PhpfastcacheLogicException(
554
                'The static item caching option (useStaticItemCaching) is disabled so you cannot attach an item.'
555
            );
556
        }
557
558
        $this->itemInstances[$item->getKey()] = $item;
559
560
        return $this;
561
    }
562
563
    public function isAttached(CacheItemInterface $item): bool
564
    {
565
        if (isset($this->itemInstances[$item->getKey()])) {
566
            return \spl_object_hash($item) === \spl_object_hash($this->itemInstances[$item->getKey()]);
567
        }
568
        return false;
569
    }
570
571
    protected function validateCacheKeys(string ...$keys): void
572
    {
573
        foreach ($keys as $key) {
574
            if (\preg_match('~([' . \preg_quote(self::$unsupportedKeyChars, '~') . ']+)~', $key, $matches)) {
575
                throw new PhpfastcacheInvalidArgumentException(
576
                    'Unsupported key character detected: "' . $matches[1] . '". 
577
                    Please check: https://github.com/PHPSocialNetwork/phpfastcache/wiki/%5BV6%5D-Unsupported-characters-in-key-identifiers'
578
                );
579
            }
580
        }
581
    }
582
583
    protected function handleExpiredCacheItem(ExtendedCacheItemInterface $item): void
584
    {
585
        if ($item->isExpired()) {
586
            /**
587
             * Using driverDelete() instead of delete()
588
             * to avoid infinite loop caused by
589
             * getItem() call in delete() method
590
             * As we MUST return an item in any
591
             * way, we do not de-register here
592
             */
593
            $this->driverDelete($item->getKey(), $item->getEncodedKey());
594
595
            /**
596
             * Reset the Item
597
             */
598
            $item->set(null)
599
                ->expiresAfter((int) abs($this->getConfig()->getDefaultTtl()))
600
                ->setHit(false)
601
                ->setTags([]);
602
603
            if ($this->getConfig()->isItemDetailedDate()) {
604
                /**
605
                 * If the itemDetailedDate has been
606
                 * set after caching, we MUST inject
607
                 * a new DateTime object on the fly
608
                 */
609
                $item->setCreationDate(new DateTime());
610
                $item->setModificationDate(new DateTime());
611
            }
612
        } else {
613
            $item->setHit(true);
614
        }
615
    }
616
617
    /**
618
     * @param ExtendedCacheItemInterface[] $items
619
     * @param bool $encoded
620
     * @return string[]
621
     */
622
    protected function getKeys(array $items, bool $encoded = false): array
623
    {
624
        return array_map(
625
            static fn(ExtendedCacheItemInterface $item) => $encoded ? $item->getEncodedKey() : $item->getKey(),
626
            $items
627
        );
628
    }
629
}
630