1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Psr\Cache\InvalidArgumentException;
16use Symfony\Component\Cache\CacheItem;
17use Symfony\Component\Cache\PruneableInterface;
18use Symfony\Component\Cache\ResettableInterface;
19use Symfony\Component\Cache\Traits\ContractsTrait;
20use Symfony\Component\Cache\Traits\ProxyTrait;
21use Symfony\Contracts\Cache\TagAwareCacheInterface;
22
23/**
24 * @author Nicolas Grekas <p@tchwork.com>
25 */
26class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface
27{
28    const TAGS_PREFIX = "\0tags\0";
29
30    use ProxyTrait;
31    use ContractsTrait;
32
33    private $deferred = [];
34    private $createCacheItem;
35    private $setCacheItemTags;
36    private $getTagsByKey;
37    private $invalidateTags;
38    private $tags;
39    private $knownTagVersions = [];
40    private $knownTagVersionsTtl;
41
42    public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15)
43    {
44        $this->pool = $itemsPool;
45        $this->tags = $tagsPool ?: $itemsPool;
46        $this->knownTagVersionsTtl = $knownTagVersionsTtl;
47        $this->createCacheItem = \Closure::bind(
48            static function ($key, $value, CacheItem $protoItem) {
49                $item = new CacheItem();
50                $item->key = $key;
51                $item->value = $value;
52                $item->defaultLifetime = $protoItem->defaultLifetime;
53                $item->expiry = $protoItem->expiry;
54                $item->poolHash = $protoItem->poolHash;
55
56                return $item;
57            },
58            null,
59            CacheItem::class
60        );
61        $this->setCacheItemTags = \Closure::bind(
62            static function (CacheItem $item, $key, array &$itemTags) {
63                $item->isTaggable = true;
64                if (!$item->isHit) {
65                    return $item;
66                }
67                if (isset($itemTags[$key])) {
68                    foreach ($itemTags[$key] as $tag => $version) {
69                        $item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag;
70                    }
71                    unset($itemTags[$key]);
72                } else {
73                    $item->value = null;
74                    $item->isHit = false;
75                }
76
77                return $item;
78            },
79            null,
80            CacheItem::class
81        );
82        $this->getTagsByKey = \Closure::bind(
83            static function ($deferred) {
84                $tagsByKey = [];
85                foreach ($deferred as $key => $item) {
86                    $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
87                }
88
89                return $tagsByKey;
90            },
91            null,
92            CacheItem::class
93        );
94        $this->invalidateTags = \Closure::bind(
95            static function (AdapterInterface $tagsAdapter, array $tags) {
96                foreach ($tags as $v) {
97                    $v->defaultLifetime = 0;
98                    $v->expiry = null;
99                    $tagsAdapter->saveDeferred($v);
100                }
101
102                return $tagsAdapter->commit();
103            },
104            null,
105            CacheItem::class
106        );
107    }
108
109    /**
110     * {@inheritdoc}
111     */
112    public function invalidateTags(array $tags)
113    {
114        $ok = true;
115        $tagsByKey = [];
116        $invalidatedTags = [];
117        foreach ($tags as $tag) {
118            CacheItem::validateKey($tag);
119            $invalidatedTags[$tag] = 0;
120        }
121
122        if ($this->deferred) {
123            $items = $this->deferred;
124            foreach ($items as $key => $item) {
125                if (!$this->pool->saveDeferred($item)) {
126                    unset($this->deferred[$key]);
127                    $ok = false;
128                }
129            }
130
131            $f = $this->getTagsByKey;
132            $tagsByKey = $f($items);
133            $this->deferred = [];
134        }
135
136        $tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags);
137        $f = $this->createCacheItem;
138
139        foreach ($tagsByKey as $key => $tags) {
140            $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
141        }
142        $ok = $this->pool->commit() && $ok;
143
144        if ($invalidatedTags) {
145            $f = $this->invalidateTags;
146            $ok = $f($this->tags, $invalidatedTags) && $ok;
147        }
148
149        return $ok;
150    }
151
152    /**
153     * {@inheritdoc}
154     *
155     * @return bool
156     */
157    public function hasItem($key)
158    {
159        if ($this->deferred) {
160            $this->commit();
161        }
162        if (!$this->pool->hasItem($key)) {
163            return false;
164        }
165
166        $itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key);
167
168        if (!$itemTags->isHit()) {
169            return false;
170        }
171
172        if (!$itemTags = $itemTags->get()) {
173            return true;
174        }
175
176        foreach ($this->getTagVersions([$itemTags]) as $tag => $version) {
177            if ($itemTags[$tag] !== $version && 1 !== $itemTags[$tag] - $version) {
178                return false;
179            }
180        }
181
182        return true;
183    }
184
185    /**
186     * {@inheritdoc}
187     */
188    public function getItem($key)
189    {
190        foreach ($this->getItems([$key]) as $item) {
191            return $item;
192        }
193
194        return null;
195    }
196
197    /**
198     * {@inheritdoc}
199     */
200    public function getItems(array $keys = [])
201    {
202        if ($this->deferred) {
203            $this->commit();
204        }
205        $tagKeys = [];
206
207        foreach ($keys as $key) {
208            if ('' !== $key && \is_string($key)) {
209                $key = static::TAGS_PREFIX.$key;
210                $tagKeys[$key] = $key;
211            }
212        }
213
214        try {
215            $items = $this->pool->getItems($tagKeys + $keys);
216        } catch (InvalidArgumentException $e) {
217            $this->pool->getItems($keys); // Should throw an exception
218
219            throw $e;
220        }
221
222        return $this->generateItems($items, $tagKeys);
223    }
224
225    /**
226     * {@inheritdoc}
227     *
228     * @return bool
229     */
230    public function clear(string $prefix = '')
231    {
232        if ('' !== $prefix) {
233            foreach ($this->deferred as $key => $item) {
234                if (0 === strpos($key, $prefix)) {
235                    unset($this->deferred[$key]);
236                }
237            }
238        } else {
239            $this->deferred = [];
240        }
241
242        if ($this->pool instanceof AdapterInterface) {
243            return $this->pool->clear($prefix);
244        }
245
246        return $this->pool->clear();
247    }
248
249    /**
250     * {@inheritdoc}
251     *
252     * @return bool
253     */
254    public function deleteItem($key)
255    {
256        return $this->deleteItems([$key]);
257    }
258
259    /**
260     * {@inheritdoc}
261     *
262     * @return bool
263     */
264    public function deleteItems(array $keys)
265    {
266        foreach ($keys as $key) {
267            if ('' !== $key && \is_string($key)) {
268                $keys[] = static::TAGS_PREFIX.$key;
269            }
270        }
271
272        return $this->pool->deleteItems($keys);
273    }
274
275    /**
276     * {@inheritdoc}
277     *
278     * @return bool
279     */
280    public function save(CacheItemInterface $item)
281    {
282        if (!$item instanceof CacheItem) {
283            return false;
284        }
285        $this->deferred[$item->getKey()] = $item;
286
287        return $this->commit();
288    }
289
290    /**
291     * {@inheritdoc}
292     *
293     * @return bool
294     */
295    public function saveDeferred(CacheItemInterface $item)
296    {
297        if (!$item instanceof CacheItem) {
298            return false;
299        }
300        $this->deferred[$item->getKey()] = $item;
301
302        return true;
303    }
304
305    /**
306     * {@inheritdoc}
307     *
308     * @return bool
309     */
310    public function commit()
311    {
312        return $this->invalidateTags([]);
313    }
314
315    public function __sleep()
316    {
317        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
318    }
319
320    public function __wakeup()
321    {
322        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
323    }
324
325    public function __destruct()
326    {
327        $this->commit();
328    }
329
330    private function generateItems(iterable $items, array $tagKeys)
331    {
332        $bufferedItems = $itemTags = [];
333        $f = $this->setCacheItemTags;
334
335        foreach ($items as $key => $item) {
336            if (!$tagKeys) {
337                yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
338                continue;
339            }
340            if (!isset($tagKeys[$key])) {
341                $bufferedItems[$key] = $item;
342                continue;
343            }
344
345            unset($tagKeys[$key]);
346
347            if ($item->isHit()) {
348                $itemTags[$key] = $item->get() ?: [];
349            }
350
351            if (!$tagKeys) {
352                $tagVersions = $this->getTagVersions($itemTags);
353
354                foreach ($itemTags as $key => $tags) {
355                    foreach ($tags as $tag => $version) {
356                        if ($tagVersions[$tag] !== $version && 1 !== $version - $tagVersions[$tag]) {
357                            unset($itemTags[$key]);
358                            continue 2;
359                        }
360                    }
361                }
362                $tagVersions = $tagKeys = null;
363
364                foreach ($bufferedItems as $key => $item) {
365                    yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
366                }
367                $bufferedItems = null;
368            }
369        }
370    }
371
372    private function getTagVersions(array $tagsByKey, array &$invalidatedTags = [])
373    {
374        $tagVersions = $invalidatedTags;
375
376        foreach ($tagsByKey as $tags) {
377            $tagVersions += $tags;
378        }
379
380        if (!$tagVersions) {
381            return [];
382        }
383
384        if (!$fetchTagVersions = 1 !== \func_num_args()) {
385            foreach ($tagsByKey as $tags) {
386                foreach ($tags as $tag => $version) {
387                    if ($tagVersions[$tag] > $version) {
388                        $tagVersions[$tag] = $version;
389                    }
390                }
391            }
392        }
393
394        $now = microtime(true);
395        $tags = [];
396        foreach ($tagVersions as $tag => $version) {
397            $tags[$tag.static::TAGS_PREFIX] = $tag;
398            if ($fetchTagVersions || !isset($this->knownTagVersions[$tag])) {
399                $fetchTagVersions = true;
400                continue;
401            }
402            $version -= $this->knownTagVersions[$tag][1];
403            if ((0 !== $version && 1 !== $version) || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) {
404                // reuse previously fetched tag versions up to the ttl, unless we are storing items or a potential miss arises
405                $fetchTagVersions = true;
406            } else {
407                $this->knownTagVersions[$tag][1] += $version;
408            }
409        }
410
411        if (!$fetchTagVersions) {
412            return $tagVersions;
413        }
414
415        foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
416            $tagVersions[$tag = $tags[$tag]] = $version->get() ?: 0;
417            if (isset($invalidatedTags[$tag])) {
418                $invalidatedTags[$tag] = $version->set(++$tagVersions[$tag]);
419            }
420            $this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]];
421        }
422
423        return $tagVersions;
424    }
425}
426