Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
154 / 154
100.00% covered (success)
100.00%
40 / 40
CRAP
100.00% covered (success)
100.00%
1 / 1
Cache
100.00% covered (success)
100.00%
154 / 154
100.00% covered (success)
100.00%
40 / 40
80
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableAdapters
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 isAvailable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 adapter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTtl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemTtl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 saveItems
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 tagIndexKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tagsForKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addToTagIndex
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 removeFromTagIndex
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 saveTaggedItem
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 rememberMetaKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 remember
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
9
 hasItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 incrementItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 decrementItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMultiple
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 set
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 setMultiple
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 has
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deleteItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 deleteMultiple
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 deleteItems
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 invalidateTag
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 invalidateTags
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 clear
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 destroy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Cache;
16
17use Pop\Cache\Adapter\AdapterInterface;
18use Pop\Cache\Clock;
19
20/**
21 * Cache class
22 *
23 * @category   Pop
24 * @package    Pop\Cache
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    5.0.0
29 */
30class Cache implements \ArrayAccess, \Psr\SimpleCache\CacheInterface
31{
32
33    /**
34     * Traits
35     */
36    use ValidatesKey;
37
38    /**
39     * Cache adapter
40     * @var ?Adapter\AdapterInterface
41     */
42    protected ?Adapter\AdapterInterface $adapter = null;
43
44    /**
45     * Clock used for stampede-protection bookkeeping (remember()'s $beta parameter)
46     * @var Clock\ClockInterface
47     */
48    protected Clock\ClockInterface $clock;
49
50    /**
51     * Constructor
52     *
53     * Instantiate the cache object
54     *
55     * @param  Adapter\AdapterInterface $adapter
56     * @param  Clock\ClockInterface     $clock
57     */
58    public function __construct(
59        Adapter\AdapterInterface $adapter, Clock\ClockInterface $clock = new Clock\SystemClock()
60    )
61    {
62        $this->adapter = $adapter;
63        $this->clock   = $clock;
64    }
65
66    /**
67     * Determine available adapters
68     *
69     * @return array
70     */
71    public static function getAvailableAdapters(): array
72    {
73        $pdoDrivers = (class_exists('Pdo', false)) ? \PDO::getAvailableDrivers() : [];
74
75        return [
76            'apc'       => (function_exists('apcu_cache_info')),
77            'file'      => true,
78            'memcached' => (class_exists('Memcached', false)),
79            'memory'    => true,
80            'null'      => true,
81            'redis'     => (class_exists('Redis', false)),
82            'session'   => (function_exists('session_start')),
83            'sqlite'    => (class_exists('Sqlite3') || in_array('sqlite', $pdoDrivers))
84        ];
85    }
86
87    /**
88     * Determine if an adapter is available
89     *
90     * @param  string $adapter
91     * @return bool
92     */
93    public static function isAvailable(string $adapter): bool
94    {
95        $adapter  = strtolower($adapter);
96        $adapters = self::getAvailableAdapters();
97        return (isset($adapters[$adapter]) && ($adapters[$adapter]));
98    }
99
100    /**
101     * Get the adapter
102     *
103     * @return ?Adapter\AdapterInterface
104     */
105    public function adapter(): ?Adapter\AdapterInterface
106    {
107        return $this->adapter;
108    }
109
110    /**
111     * Get global cache TTL
112     *
113     * @return int
114     */
115    public function getTtl(): int
116    {
117        return $this->adapter->getTtl();
118    }
119
120    /**
121     * Get item cache TTL
122     *
123     * @param  string $id
124     * @param  int    $default
125     * @return int
126     */
127    public function getItemTtl(string $id, int $default = 0): int
128    {
129        return $this->adapter->getItemTtl($id, $default);
130    }
131
132    /**
133     * Save an item to cache
134     *
135     * @param  string $id
136     * @param  mixed  $value
137     * @param  ?int   $ttl
138     * @throws InvalidArgumentException
139     * @return void
140     */
141    public function saveItem(string $id, mixed $value, ?int $ttl = null): void
142    {
143        $this->validateKey($id);
144        $this->adapter->saveItem($id, $value, $ttl);
145    }
146
147    /**
148     * Save items to cache
149     *
150     * @param  array $items
151     * @throws InvalidArgumentException
152     * @return void
153     */
154    public function saveItems(array $items): void
155    {
156        foreach (array_keys($items) as $id) {
157            $this->validateKey((string)$id);
158        }
159
160        foreach ($items as $id => $value) {
161            $this->adapter->saveItem((string)$id, $value);
162        }
163    }
164
165    /**
166     * Get the internal bookkeeping key for a tag's member index
167     *
168     * @param  string $tag
169     * @return string
170     */
171    protected function tagIndexKey(string $tag): string
172    {
173        return '@tag:' . sha1($tag);
174    }
175
176    /**
177     * Get the internal bookkeeping key for an id's own current tag list
178     *
179     * @param  string $id
180     * @return string
181     */
182    protected function tagsForKey(string $id): string
183    {
184        return '@tags-for:' . sha1($id);
185    }
186
187    /**
188     * Add $id to $tag's member index, creating the index if it doesn't exist yet
189     *
190     * Reads/writes the index directly via the adapter (not $this->getItem()/saveItem()), since the
191     * internally-generated key uses '@', a character validateKey() forbids in user-supplied keys. The index
192     * is always saved with ttl=0 (never expires), regardless of any individual tagged item's TTL, since
193     * multiple items with different TTLs can share one tag.
194     *
195     * @param  string $tag
196     * @param  string $id
197     * @return void
198     */
199    protected function addToTagIndex(string $tag, string $id): void
200    {
201        $key     = $this->tagIndexKey($tag);
202        $members = $this->adapter->getItem($key, []);
203
204        if (!in_array($id, $members, true)) {
205            $members[] = $id;
206            $this->adapter->saveItem($key, $members, 0);
207        }
208    }
209
210    /**
211     * Remove $id from $tag's member index, deleting the index entirely if it becomes empty
212     *
213     * @param  string $tag
214     * @param  string $id
215     * @return void
216     */
217    protected function removeFromTagIndex(string $tag, string $id): void
218    {
219        $key     = $this->tagIndexKey($tag);
220        $members = array_values(array_diff($this->adapter->getItem($key, []), [$id]));
221
222        if (empty($members)) {
223            $this->adapter->deleteItem($key);
224        } else {
225            $this->adapter->saveItem($key, $members, 0);
226        }
227    }
228
229    /**
230     * Save an item to cache under one or more tags for later bulk invalidation via invalidateTag()
231     *
232     * Non-atomic: every step here is a plain read-modify-write via the adapter's own getItem()/saveItem()/
233     * deleteItem(), with no locking. Mixing plain saveItem()/deleteItem() calls with saveTaggedItem() on the
234     * same id leaves tag bookkeeping stale â€” see docs/POP-CACHE.md for the full mechanism and known
235     * limitations. Both the tag index and this id's own tags-for bookkeeping are saved with ttl=0 (never
236     * expire) regardless of $ttl, so re-tagging reconciliation always has an accurate view of an id's prior
237     * tags even after the item itself has naturally expired â€” see docs/POP-CACHE.md for why an asymmetric
238     * policy here is a correctness bug, not just a minor inconsistency.
239     *
240     * @param  string $id
241     * @param  mixed  $value
242     * @param  array  $tags
243     * @param  ?int   $ttl
244     * @throws InvalidArgumentException
245     * @return void
246     */
247    public function saveTaggedItem(string $id, mixed $value, array $tags, ?int $ttl = null): void
248    {
249        $this->validateKey($id);
250        foreach ($tags as $tag) {
251            if (!is_string($tag)) {
252                throw new InvalidArgumentException('Error: Every tag must be a string.');
253            }
254            $this->validateKey($tag);
255        }
256
257        $tagsForKey = $this->tagsForKey($id);
258        $oldTags    = $this->adapter->getItem($tagsForKey, []);
259
260        foreach (array_diff($oldTags, $tags) as $oldTag) {
261            $this->removeFromTagIndex($oldTag, $id);
262        }
263
264        foreach ($tags as $tag) {
265            $this->addToTagIndex($tag, $id);
266        }
267
268        $this->adapter->saveItem($id, $value, $ttl);
269
270        if (empty($tags)) {
271            $this->adapter->deleteItem($tagsForKey);
272        } else {
273            $this->adapter->saveItem($tagsForKey, $tags, 0);
274        }
275    }
276
277    /**
278     * Get an item from cache
279     *
280     * @param  string $id
281     * @param  mixed  $default
282     * @throws InvalidArgumentException
283     * @return mixed
284     */
285    public function getItem(string $id, mixed $default = false): mixed
286    {
287        $this->validateKey($id);
288        return $this->adapter->getItem($id, $default);
289    }
290
291    /**
292     * Get the internal bookkeeping key for an id's stampede-protection metadata
293     *
294     * @param  string $id
295     * @return string
296     */
297    protected function rememberMetaKey(string $id): string
298    {
299        return '@remember:' . sha1($id);
300    }
301
302    /**
303     * Get an item from cache, or compute and cache it via the callback if it isn't already cached
304     *
305     * Any exception thrown by $callback propagates unchanged, and nothing is written to cache in that case.
306     *
307     * $beta enables probabilistic early recomputation (the XFetch algorithm) to reduce stampedes on
308     * steady-state re-computation: as a cached item approaches its TTL, reads have an increasing chance of
309     * triggering recomputation before hard expiry, so one caller refreshes the value while concurrent readers
310     * keep getting served the still-valid cached copy. $beta = 0.0 (the default) disables this entirely â€”
311     * mathematically equivalent to remember()'s behavior before this parameter existed, with zero extra
312     * adapter calls. This does NOT protect a key's true first-ever call (no cached value, no bookkeeping yet)
313     * â€” every concurrent caller on a genuine cold miss still recomputes; that requires locking, which this
314     * method deliberately does not implement. Never-expiring items (a resolved TTL of 0, whether explicit or
315     * from the adapter's global TTL) are not eligible for early recomputation â€” there's no expiry to recompute
316     * ahead of, so $beta has no effect on them. A callback that completes in under one second still gets a
317     * minimum effective delta of 1 second for the XFetch calculation, so $beta needs to be scaled accordingly
318     * for very fast callbacks to see meaningful early-recompute lead time (roughly delta Ã— beta seconds before
319     * real expiry). See docs/POP-CACHE.md for the full algorithm and why the bookkeeping key's TTL policy
320     * differs from saveTaggedItem()'s.
321     *
322     * @param  string   $id
323     * @param  callable $callback
324     * @param  ?int     $ttl
325     * @param  float    $beta
326     * @throws InvalidArgumentException
327     * @return mixed
328     */
329    public function remember(string $id, callable $callback, ?int $ttl = null, float $beta = 0.0): mixed
330    {
331        $miss  = new \stdClass();
332        $value = $this->getItem($id, $miss);
333
334        $shouldRecompute = ($value === $miss);
335
336        if (!$shouldRecompute && ($beta > 0.0)) {
337            $meta = $this->adapter->getItem($this->rememberMetaKey($id), null);
338
339            if (is_array($meta) && ($meta['ttl'] > 0)) {
340                $rand   = mt_rand(1, mt_getrandmax()) / mt_getrandmax();
341                $expiry = $meta['start'] + $meta['ttl'];
342
343                if (($this->clock->now() - ($meta['delta'] * $beta * log($rand))) >= $expiry) {
344                    $shouldRecompute = true;
345                }
346            }
347        }
348
349        if ($shouldRecompute) {
350            $start = $this->clock->now();
351            $value = $callback();
352            $delta = max(1, $this->clock->now() - $start);
353
354            $resolvedTtl = $ttl ?? $this->adapter->getTtl();
355
356            $this->saveItem($id, $value, $ttl);
357
358            if (($beta > 0.0) && ($resolvedTtl > 0)) {
359                $this->adapter->saveItem(
360                    $this->rememberMetaKey($id),
361                    ['start' => $start, 'ttl' => $resolvedTtl, 'delta' => $delta],
362                    $resolvedTtl
363                );
364            }
365        }
366
367        return $value;
368    }
369
370    /**
371     * Determine if the item is in cache
372     *
373     * @param  string $id
374     * @throws InvalidArgumentException
375     * @return bool
376     */
377    public function hasItem(string $id): bool
378    {
379        $this->validateKey($id);
380        return $this->adapter->hasItem($id);
381    }
382
383    /**
384     * Atomically increment a counter in cache, creating it at $initial if it doesn't exist
385     *
386     * @param  string $id
387     * @param  int    $amount
388     * @param  int    $initial
389     * @param  ?int   $ttl
390     * @throws InvalidArgumentException
391     * @return int
392     */
393    public function incrementItem(string $id, int $amount = 1, int $initial = 0, ?int $ttl = null): int
394    {
395        $this->validateKey($id);
396        return $this->adapter->incrementItem($id, $amount, $initial, $ttl);
397    }
398
399    /**
400     * Atomically decrement a counter in cache, creating it at $initial if it doesn't exist
401     *
402     * @param  string $id
403     * @param  int    $amount
404     * @param  int    $initial
405     * @param  ?int   $ttl
406     * @throws InvalidArgumentException
407     * @return int
408     */
409    public function decrementItem(string $id, int $amount = 1, int $initial = 0, ?int $ttl = null): int
410    {
411        $this->validateKey($id);
412        return $this->adapter->decrementItem($id, $amount, $initial, $ttl);
413    }
414
415    /**
416     * Fetch a value from the cache (PSR-16)
417     *
418     * @param  string $key
419     * @param  mixed  $default
420     * @throws InvalidArgumentException
421     * @return mixed
422     */
423    public function get(string $key, mixed $default = null): mixed
424    {
425        $this->validateKey($key);
426        return $this->adapter->getItem($key, $default);
427    }
428
429    /**
430     * Obtain multiple cache items by their unique keys (PSR-16)
431     *
432     * @param  iterable $keys
433     * @param  mixed    $default
434     * @throws InvalidArgumentException
435     * @return iterable
436     */
437    public function getMultiple(iterable $keys, mixed $default = null): iterable
438    {
439        $keys = is_array($keys) ? $keys : iterator_to_array($keys, false);
440
441        foreach ($keys as $key) {
442            $this->validateKey((string)$key);
443        }
444
445        $values = [];
446        foreach ($keys as $key) {
447            $values[(string)$key] = $this->adapter->getItem((string)$key, $default);
448        }
449
450        return $values;
451    }
452
453    /**
454     * Persist a value in the cache, with an optional TTL (PSR-16)
455     *
456     * A $ttl of null uses the adapter's configured global TTL. A $ttl (or a \DateInterval resolved to
457     * seconds) that is zero or negative deletes the item instead of caching it, per the PSR-16 spec â€”
458     * note this differs from saveItem()'s own "0 = never expires" convention, which is unaffected by this
459     * method and remains available via saveItem() directly.
460     *
461     * @param  string                 $key
462     * @param  mixed                  $value
463     * @param  null|int|\DateInterval $ttl
464     * @throws InvalidArgumentException
465     * @return bool
466     */
467    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
468    {
469        $this->validateKey($key);
470
471        $seconds = $ttl;
472        if ($ttl instanceof \DateInterval) {
473            $now     = new \DateTimeImmutable();
474            $seconds = $now->add($ttl)->getTimestamp() - $now->getTimestamp();
475        }
476
477        if (($seconds !== null) && ($seconds <= 0)) {
478            $this->adapter->deleteItem($key);
479        } else {
480            $this->adapter->saveItem($key, $value, $seconds);
481        }
482
483        return true;
484    }
485
486    /**
487     * Persist a set of key => value pairs in the cache, with an optional TTL (PSR-16)
488     *
489     * @param  iterable               $values
490     * @param  null|int|\DateInterval $ttl
491     * @throws InvalidArgumentException
492     * @return bool
493     */
494    public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool
495    {
496        $values = is_array($values) ? $values : iterator_to_array($values);
497
498        foreach (array_keys($values) as $key) {
499            $this->validateKey((string)$key);
500        }
501
502        foreach ($values as $key => $value) {
503            $this->set((string)$key, $value, $ttl);
504        }
505
506        return true;
507    }
508
509    /**
510     * Determine whether an item is present in the cache (PSR-16)
511     *
512     * @param  string $key
513     * @throws InvalidArgumentException
514     * @return bool
515     */
516    public function has(string $key): bool
517    {
518        $this->validateKey($key);
519        return $this->adapter->hasItem($key);
520    }
521
522    /**
523     * Delete an item in cache
524     *
525     * @param  string $id
526     * @throws InvalidArgumentException
527     * @return void
528     */
529    public function deleteItem(string $id): void
530    {
531        $this->validateKey($id);
532        $this->adapter->deleteItem($id);
533    }
534
535    /**
536     * Delete an item from the cache (PSR-16)
537     *
538     * @param  string $key
539     * @throws InvalidArgumentException
540     * @return bool
541     */
542    public function delete(string $key): bool
543    {
544        $this->validateKey($key);
545        $this->adapter->deleteItem($key);
546        return true;
547    }
548
549    /**
550     * Delete multiple cache items in a single operation (PSR-16)
551     *
552     * @param  iterable $keys
553     * @throws InvalidArgumentException
554     * @return bool
555     */
556    public function deleteMultiple(iterable $keys): bool
557    {
558        $keys = is_array($keys) ? $keys : iterator_to_array($keys, false);
559
560        foreach ($keys as $key) {
561            $this->validateKey($key);
562        }
563
564        foreach ($keys as $key) {
565            $this->adapter->deleteItem($key);
566        }
567
568        return true;
569    }
570
571    /**
572     * Delete items in cache
573     *
574     * @param  array $ids
575     * @throws InvalidArgumentException
576     * @return void
577     */
578    public function deleteItems(array $ids): void
579    {
580        foreach ($ids as $id) {
581            $this->validateKey($id);
582        }
583
584        foreach ($ids as $id) {
585            $this->adapter->deleteItem($id);
586        }
587    }
588
589    /**
590     * Delete every item currently tagged with $tag, and the tag's own bookkeeping
591     *
592     * Non-atomic: see saveTaggedItem() and docs/POP-CACHE.md for the full mechanism and known limitations.
593     *
594     * @param  string $tag
595     * @throws InvalidArgumentException
596     * @return void
597     */
598    public function invalidateTag(string $tag): void
599    {
600        $this->validateKey($tag);
601
602        $tagKey  = $this->tagIndexKey($tag);
603        $members = $this->adapter->getItem($tagKey, []);
604
605        foreach ($members as $id) {
606            $tagsForKey = $this->tagsForKey($id);
607            $idTags     = $this->adapter->getItem($tagsForKey, []);
608
609            foreach ($idTags as $otherTag) {
610                if ($otherTag !== $tag) {
611                    $this->removeFromTagIndex($otherTag, $id);
612                }
613            }
614
615            $this->adapter->deleteItem($id);
616            $this->adapter->deleteItem($tagsForKey);
617        }
618
619        $this->adapter->deleteItem($tagKey);
620    }
621
622    /**
623     * Invalidate multiple tags in a single operation
624     *
625     * Validates every tag before invalidating any of them (all-or-nothing), matching this codebase's
626     * existing getMultiple()/setMultiple()/deleteMultiple() precedent.
627     *
628     * @param  array $tags
629     * @throws InvalidArgumentException
630     * @return void
631     */
632    public function invalidateTags(array $tags): void
633    {
634        foreach ($tags as $tag) {
635            if (!is_string($tag)) {
636                throw new InvalidArgumentException('Error: Every tag must be a string.');
637            }
638            $this->validateKey($tag);
639        }
640
641        foreach ($tags as $tag) {
642            $this->invalidateTag($tag);
643        }
644    }
645
646    /**
647     * Clear all stored values from cache
648     *
649     * @return bool
650     */
651    public function clear(): bool
652    {
653        $this->adapter->clear();
654        return true;
655    }
656
657    /**
658     * Destroy cache resource
659     *
660     * @return void
661     */
662    public function destroy(): void
663    {
664        $this->adapter->destroy();
665    }
666
667    /**
668     * Magic get method to return an item from cache
669     *
670     * @param  string $name
671     * @return mixed
672     */
673    public function __get(string $name): mixed
674    {
675        return $this->getItem($name);
676    }
677
678    /**
679     * Magic set method to save an item in the cache
680     *
681     * @param  string $name
682     * @param  mixed $value
683     * @return void
684     */
685    public function __set(string $name, mixed $value): void
686    {
687        $this->saveItem($name, $value);
688    }
689
690    /**
691     * Determine if the item is in cache
692     *
693     * @param  string $name
694     * @return bool
695     */
696    public function __isset(string $name): bool
697    {
698        return $this->hasItem($name);
699    }
700
701    /**
702     * Delete value from cache
703     *
704     * @param  string $name
705     * @return void
706     */
707    public function __unset(string $name): void
708    {
709        $this->deleteItem($name);
710    }
711
712    /**
713     * ArrayAccess offsetExists
714     *
715     * @param  mixed $offset
716     * @return bool
717     */
718    public function offsetExists(mixed $offset): bool
719    {
720        return $this->__isset($offset);
721    }
722
723    /**
724     * ArrayAccess offsetGet
725     *
726     * @param  mixed $offset
727     * @return mixed
728     */
729    public function offsetGet(mixed $offset): mixed
730    {
731        return $this->__get($offset);
732    }
733
734    /**
735     * ArrayAccess offsetSet
736     *
737     * @param  mixed $offset
738     * @param  mixed $value
739     * @return void
740     */
741    public function offsetSet(mixed $offset, mixed  $value): void
742    {
743        $this->__set($offset, $value);
744    }
745
746    /**
747     * ArrayAccess offsetUnset
748     *
749     * @param  mixed $offset
750     * @return void
751     */
752    public function offsetUnset(mixed $offset): void
753    {
754        $this->__unset($offset);
755    }
756
757}