Completed
Push — EZP-30823_7.5_transaction_cach... ( 82ceaf...7f15f5 )
by André
20:04
created

TransactionalInMemoryCacheAdapter::getItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
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
 * @todo Naming? Is this something like IsolatedCacheDecorator
21
 */
22
class TransactionalInMemoryCacheAdapter implements TransactionAwareAdapterInterface
23
{
24
    /** @var \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface */
25
    protected $sharedPool;
26
27
    /** @var \eZ\Publish\Core\Persistence\Cache\inMemory\InMemoryCache[] */
28
    private $inMemoryPools;
29
30
    /** @var int */
31
    protected $transactionDepth;
32
33
    /** @var array To be unique and simplify lookup hash key is cache tag, value is only true value */
34
    protected $deferredTagsInvalidation;
35
36
    /** @var array To be unique and simplify lookup hash key is cache key, value is only true value */
37
    protected $deferredItemsDeletion;
38
39
    /**
40
     * @param \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface $sharedPool
41
     * @param \eZ\Publish\Core\Persistence\Cache\inMemory\InMemoryCache[] $inMemoryPools
42
     * @param int $transactionDepth
43
     * @param array $deferredTagsInvalidation
44
     * @param array $deferredItemsDeletion
45
     */
46
    public function __construct(
47
        TagAwareAdapterInterface $sharedPool,
48
        iterable $inMemoryPools,
49
        int $transactionDepth = 0,
50
        array $deferredTagsInvalidation = [],
51
        array $deferredItemsDeletion = []
52
    ) {
53
        $this->sharedPool = $sharedPool;
54
        $this->inMemoryPools = $inMemoryPools;
55
        $this->transactionDepth = $transactionDepth;
56
        $this->deferredTagsInvalidation = empty($deferredTagsInvalidation) ? [] : \array_fill_keys($deferredTagsInvalidation, true);
57
        $this->deferredItemsDeletion = empty($deferredItemsDeletion) ? [] : \array_fill_keys($deferredItemsDeletion, true);
58
        // To modify protected $isHit when items are a "miss" based on deferred delete/invalidation during transactions
59
        $this->setCacheItemAsMiss = \Closure::bind(
0 ignored issues
show
Bug introduced by
The property setCacheItemAsMiss does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

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