Completed
Push — 7.5 ( 13f815...467086 )
by André
19:24
created

TransactionalInMemoryCacheAdapter::saveDeferred()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
8
9
namespace eZ\Publish\Core\Persistence\Cache\Adapter;
10
11
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
12
use Psr\Cache\CacheItemInterface;
13
use Symfony\Component\Cache\CacheItem;
14
15
/**
16
 * Internal proxy adapter invalidating our isolated in-memory cache, and defer shared pool changes during transactions.
17
 *
18
 * @internal For type hinting inside eZ\Publish\Core\Persistence\Cache\*. For external, type hint on TagAwareAdapterInterface.
19
 */
20
class TransactionalInMemoryCacheAdapter implements TransactionAwareAdapterInterface
21
{
22
    /** @var \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface */
23
    protected $sharedPool;
24
25
    /** @var \eZ\Publish\Core\Persistence\Cache\inMemory\InMemoryCache[] */
26
    private $inMemoryPools;
27
28
    /** @var int */
29
    protected $transactionDepth;
30
31
    /** @var array To be unique and simplify lookup hash key is cache tag, value is only true value */
32
    protected $deferredTagsInvalidation;
33
34
    /** @var array To be unique and simplify lookup hash key is cache key, value is only true value */
35
    protected $deferredItemsDeletion;
36
37
    /** @var \Closure Callback for use by {@see markItemsAsDeferredMissIfNeeded()} when items are misses by deferred action */
38
    protected $setCacheItemAsMiss;
39
40
    /**
41
     * @param \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface $sharedPool
42
     * @param \eZ\Publish\Core\Persistence\Cache\inMemory\InMemoryCache[] $inMemoryPools
43
     * @param int $transactionDepth
44
     * @param array $deferredTagsInvalidation
45
     * @param array $deferredItemsDeletion
46
     */
47
    public function __construct(
48
        TagAwareAdapterInterface $sharedPool,
49
        iterable $inMemoryPools,
50
        int $transactionDepth = 0,
51
        array $deferredTagsInvalidation = [],
52
        array $deferredItemsDeletion = []
53
    ) {
54
        $this->sharedPool = $sharedPool;
55
        $this->inMemoryPools = $inMemoryPools;
56
        $this->transactionDepth = $transactionDepth;
57
        $this->deferredTagsInvalidation = empty($deferredTagsInvalidation) ? [] : \array_fill_keys($deferredTagsInvalidation, true);
58
        $this->deferredItemsDeletion = empty($deferredItemsDeletion) ? [] : \array_fill_keys($deferredItemsDeletion, true);
59
        // To modify protected $isHit when items are a "miss" based on deferred delete/invalidation during transactions
60
        $this->setCacheItemAsMiss = \Closure::bind(
61
            static function (CacheItem $item) {
62
                // ... Might not work for anything but new items
63
                $item->isHit = false;
0 ignored issues
show
Bug introduced by
The property isHit cannot be accessed from this context as it is declared protected in class Symfony\Component\Cache\CacheItem.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
64
            },
65
            null,
66
            CacheItem::class
67
        );
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function getItem($key)
74
    {
75
        return $this->markItemsAsDeferredMissIfNeeded(
76
            [$this->sharedPool->getItem($key)]
77
        )[0];
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function getItems(array $keys = [])
84
    {
85
        return $this->markItemsAsDeferredMissIfNeeded(
86
            $this->sharedPool->getItems($keys)
0 ignored issues
show
Bug introduced by
It seems like $this->sharedPool->getItems($keys) targeting Symfony\Component\Cache\...erInterface::getItems() can also be of type object<Traversable>; however, eZ\Publish\Core\Persiste...sDeferredMissIfNeeded() does only seem to accept array<integer,object<Sym...onent\Cache\CacheItem>>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
87
        );
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function hasItem($key)
94
    {
95
        if (isset($this->deferredItemsDeletion[$key])) {
96
            return false;
97
        }
98
99
        return $this->sharedPool->hasItem($key);
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 View Code Duplication
    public function deleteItem($key)
106
    {
107
        foreach ($this->inMemoryPools as $inMemory) {
108
            $inMemory->deleteMulti([$key]);
109
        }
110
111
        if ($this->transactionDepth > 0) {
112
            $this->deferredItemsDeletion[$key] = true;
113
114
            return true;
115
        }
116
117
        return $this->sharedPool->deleteItem($key);
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 View Code Duplication
    public function deleteItems(array $keys)
124
    {
125
        foreach ($this->inMemoryPools as $inMemory) {
126
            $inMemory->deleteMulti($keys);
127
        }
128
129
        if ($this->transactionDepth > 0) {
130
            $this->deferredItemsDeletion += \array_fill_keys($keys, true);
131
132
            return true;
133
        }
134
135
        return $this->sharedPool->deleteItems($keys);
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function invalidateTags(array $tags)
142
    {
143
        // No tracking of tags in in-memory, as it's anyway meant to only optimize for reads (GETs) and not writes.
144
        foreach ($this->inMemoryPools as $inMemory) {
145
            $inMemory->clear();
146
        }
147
148
        if ($this->transactionDepth > 0) {
149
            $this->deferredTagsInvalidation += \array_fill_keys($tags, true);
150
151
            return true;
152
        }
153
154
        return $this->sharedPool->invalidateTags($tags);
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function clear()
161
    {
162
        foreach ($this->inMemoryPools as $inMemory) {
163
            $inMemory->clear();
164
        }
165
166
        // @todo Should we trow RunTime error or add support deferring full cache clearing?
167
        $this->transactionDepth = 0;
168
        $this->deferredItemsDeletion = [];
169
        $this->deferredTagsInvalidation = [];
170
171
        return $this->sharedPool->clear();
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function save(CacheItemInterface $item)
178
    {
179
        if ($this->transactionDepth > 0) {
180
            $this->deferredItemsDeletion[$item->getKey()] = true;
181
182
            return true;
183
        }
184
185
        return $this->sharedPool->save($item);
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function beginTransaction(): void
192
    {
193
        ++$this->transactionDepth;
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function commitTransaction(): void
200
    {
201
        if ($this->transactionDepth === 0) {
202
            // ignore, might have been a previous rollback
203
            return;
204
        }
205
206
        --$this->transactionDepth;
207
208
        // Once we reach 0 transaction count, sent out deferred deletes/invalidations to shared pool
209
        if ($this->transactionDepth === 0) {
210
            if (!empty($this->deferredItemsDeletion)) {
211
                $this->sharedPool->deleteItems(\array_keys($this->deferredItemsDeletion));
212
                $this->deferredItemsDeletion = [];
213
            }
214
215
            if (!empty($this->deferredTagsInvalidation)) {
216
                $this->sharedPool->invalidateTags(\array_keys($this->deferredTagsInvalidation));
217
                $this->deferredTagsInvalidation = [];
218
            }
219
        }
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function rollbackTransaction(): void
226
    {
227
        $this->transactionDepth = 0;
228
        $this->deferredItemsDeletion = [];
229
        $this->deferredTagsInvalidation = [];
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     *
235
     * Symfony cache feature for deferring saves, not used by eZ & not related to transaction handling here.
236
     */
237
    public function saveDeferred(CacheItemInterface $item)
238
    {
239
        return $this->sharedPool->saveDeferred($item);
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     *
245
     * Symfony cache feature for committing deferred saves, not used by eZ & not related to transaction handling here.
246
     */
247
    public function commit()
248
    {
249
        return $this->sharedPool->commit();
250
    }
251
252
    /**
253
     * For use by getItem(s) to mark items as a miss if it's going to be cleared on transaction commit.
254
     *
255
     * @param \Symfony\Component\Cache\CacheItem[] $items
256
     *
257
     * @return \Symfony\Component\Cache\CacheItem[]
258
     */
259
    private function markItemsAsDeferredMissIfNeeded(iterable $items)
260
    {
261
        if ($this->transactionDepth === 0) {
262
            return $items;
263
        }
264
265
        // In case of $items being generator we map items over to new array as it can't be iterated several times
266
        $iteratedItems = [];
267
        $fnSetCacheItemAsMiss = $this->setCacheItemAsMiss;
268
        foreach ($items as $key => $item) {
269
            $iteratedItems[$key] = $item;
270
271
            if (!$item->isHit()) {
272
                continue;
273
            }
274
275
            if ($this->itemIsDeferredMiss($item)) {
276
                $fnSetCacheItemAsMiss($item);
277
            }
278
        }
279
280
        return $iteratedItems;
281
    }
282
283
    /**
284
     * @param \Symfony\Component\Cache\CacheItem $item
285
     *
286
     * @return bool
287
     */
288
    private function itemIsDeferredMiss(CacheItem $item): bool
289
    {
290
        if (isset($this->deferredItemsDeletion[$item->getKey()])) {
291
            return true;
292
        }
293
294
        foreach ($item->getPreviousTags() as $tag) {
295
            if (isset($this->deferredTagsInvalidation[$tag])) {
296
                return true;
297
            }
298
        }
299
300
        return false;
301
    }
302
}
303