Completed
Push — 7.5 ( 9e0292...164ebc )
by
unknown
18:00
created

TransactionalInMemoryCacheAdapter::save()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 10
rs 9.9332
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
        $this->clearInMemoryPools();
145
146
        if ($this->transactionDepth > 0) {
147
            $this->deferredTagsInvalidation += \array_fill_keys($tags, true);
148
149
            return true;
150
        }
151
152
        return $this->sharedPool->invalidateTags($tags);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function clear()
159
    {
160
        $this->clearInMemoryPools();
161
162
        // @todo Should we trow RunTime error or add support deferring full cache clearing?
163
        $this->transactionDepth = 0;
164
        $this->deferredItemsDeletion = [];
165
        $this->deferredTagsInvalidation = [];
166
167
        return $this->sharedPool->clear();
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function save(CacheItemInterface $item)
174
    {
175
        if ($this->transactionDepth > 0) {
176
            $this->deferredItemsDeletion[$item->getKey()] = true;
177
178
            return true;
179
        }
180
181
        return $this->sharedPool->save($item);
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function beginTransaction(): void
188
    {
189
        ++$this->transactionDepth;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function commitTransaction(): void
196
    {
197
        if ($this->transactionDepth === 0) {
198
            // ignore, might have been a previous rollback
199
            return;
200
        }
201
202
        --$this->transactionDepth;
203
204
        // Once we reach 0 transaction count, sent out deferred deletes/invalidations to shared pool
205
        if ($this->transactionDepth === 0) {
206
            if (!empty($this->deferredItemsDeletion)) {
207
                $this->sharedPool->deleteItems(\array_keys($this->deferredItemsDeletion));
208
                $this->deferredItemsDeletion = [];
209
            }
210
211
            if (!empty($this->deferredTagsInvalidation)) {
212
                $this->sharedPool->invalidateTags(\array_keys($this->deferredTagsInvalidation));
213
                $this->deferredTagsInvalidation = [];
214
            }
215
        }
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function rollbackTransaction(): void
222
    {
223
        $this->transactionDepth = 0;
224
        $this->deferredItemsDeletion = [];
225
        $this->deferredTagsInvalidation = [];
226
227
        $this->clearInMemoryPools();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     *
233
     * Symfony cache feature for deferring saves, not used by eZ & not related to transaction handling here.
234
     */
235
    public function saveDeferred(CacheItemInterface $item)
236
    {
237
        return $this->sharedPool->saveDeferred($item);
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     *
243
     * Symfony cache feature for committing deferred saves, not used by eZ & not related to transaction handling here.
244
     */
245
    public function commit()
246
    {
247
        return $this->sharedPool->commit();
248
    }
249
250
    /**
251
     * For use by getItem(s) to mark items as a miss if it's going to be cleared on transaction commit.
252
     *
253
     * @param \Symfony\Component\Cache\CacheItem[] $items
254
     *
255
     * @return \Symfony\Component\Cache\CacheItem[]
256
     */
257
    private function markItemsAsDeferredMissIfNeeded(iterable $items)
258
    {
259
        if ($this->transactionDepth === 0) {
260
            return $items;
261
        }
262
263
        // In case of $items being generator we map items over to new array as it can't be iterated several times
264
        $iteratedItems = [];
265
        $fnSetCacheItemAsMiss = $this->setCacheItemAsMiss;
266
        foreach ($items as $key => $item) {
267
            $iteratedItems[$key] = $item;
268
269
            if (!$item->isHit()) {
270
                continue;
271
            }
272
273
            if ($this->itemIsDeferredMiss($item)) {
274
                $fnSetCacheItemAsMiss($item);
275
            }
276
        }
277
278
        return $iteratedItems;
279
    }
280
281
    /**
282
     * @param \Symfony\Component\Cache\CacheItem $item
283
     *
284
     * @return bool
285
     */
286
    private function itemIsDeferredMiss(CacheItem $item): bool
287
    {
288
        if (isset($this->deferredItemsDeletion[$item->getKey()])) {
289
            return true;
290
        }
291
292
        foreach ($item->getPreviousTags() as $tag) {
293
            if (isset($this->deferredTagsInvalidation[$tag])) {
294
                return true;
295
            }
296
        }
297
298
        return false;
299
    }
300
301
    private function clearInMemoryPools(): void
302
    {
303
        foreach ($this->inMemoryPools as $inMemory) {
304
            $inMemory->clear();
305
        }
306
    }
307
}
308