Completed
Push — master ( 7bc8ae...9f2037 )
by Łukasz
21:21
created

AbstractInMemoryHandler::getCacheValue()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 38

Duplication

Lines 6
Ratio 15.79 %

Importance

Changes 0
Metric Value
cc 6
nc 3
nop 7
dl 6
loc 38
rs 8.6897
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
namespace eZ\Publish\Core\Persistence\Cache;
8
9
use eZ\Publish\Core\Persistence\Cache\Adapter\InMemoryClearingProxyAdapter;
10
use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache;
11
12
/**
13
 * Abstract handler for use in other SPI Handlers.
14
 *
15
 * @internal Can be used in external handlers, but be aware this should be regarded as experimental feature.
16
 *           As in, method signatures and behaviour might change in the future.
17
 */
18
abstract class AbstractInMemoryHandler
19
{
20
    /**
21
     * NOTE: Instance of this must be InMemoryClearingProxyAdapter in order for cache clearing to affect in-memory cache.
22
     *
23
     * @var \eZ\Publish\Core\Persistence\Cache\Adapter\InMemoryClearingProxyAdapter
24
     */
25
    protected $cache;
26
27
    /**
28
     * @var \eZ\Publish\Core\Persistence\Cache\PersistenceLogger
29
     */
30
    protected $logger;
31
32
    /**
33
     * NOTE: On purpose private as it's only supposed to be interacted with in tandem with symfony cache here,
34
     *       hence the cache decorator and the reusable methods here.
35
     *
36
     * @var \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache
37
     */
38
    private $inMemory;
39
40
    /**
41
     * Setups current handler with everything needed.
42
     *
43
     * @param \eZ\Publish\Core\Persistence\Cache\Adapter\InMemoryClearingProxyAdapter $cache
44
     * @param \eZ\Publish\Core\Persistence\Cache\PersistenceLogger $logger
45
     * @param \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache $inMemory
46
     */
47
    public function __construct(
48
        InMemoryClearingProxyAdapter $cache,
49
        PersistenceLogger $logger,
50
        InMemoryCache $inMemory
51
    ) {
52
        $this->cache = $cache;
53
        $this->logger = $logger;
54
        $this->inMemory = $inMemory;
55
    }
56
57
    /**
58
     * Load one cache item from cache and loggs the hits / misses.
59
     *
60
     * Load items from in-memory cache, symfony cache pool or backend in that order.
61
     * If not cached the returned objects will be placed in cache.
62
     *
63
     * @param int|string $id
64
     * @param string $keyPrefix E.g "ez-content-"
65
     * @param callable $backendLoader Function for loading missing objects, gets array with missing id's as argument,
66
     *                                expects return value to be array with id as key. Missing items should be missing.
67
     * @param callable $cacheTagger Gets cache object as argument, return array of cache tags.
68
     * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys.
69
     * @param string $keySuffix Optional, e.g "-by-identifier"
70
     *
71
     * @return object
72
     */
73
    final protected function getCacheValue(
74
        $id,
75
        string $keyPrefix,
76
        callable $backendLoader,
77
        callable $cacheTagger,
78
        callable $cacheIndexes,
79
        string $keySuffix = '',
80
        array $arguments = []
81
    ) {
82
        $key = $keyPrefix . $id . $keySuffix;
83
        // In-memory
84
        if ($object = $this->inMemory->get($key)) {
85
            $this->logger->logCacheHit($arguments ?: [$id], 3, true);
86
87
            return $object;
88
        }
89
90
        // Cache pool
91
        $cacheItem = $this->cache->getItem($key);
92 View Code Duplication
        if ($cacheItem->isHit()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
93
            $this->logger->logCacheHit($arguments ?: [$id], 3);
94
            $this->inMemory->setMulti([$object = $cacheItem->get()], $cacheIndexes);
95
96
            return $object;
97
        }
98
99
        // Backend
100
        $object = $backendLoader($id);
101
        $this->inMemory->setMulti([$object], $cacheIndexes);
102
        $this->logger->logCacheMiss($arguments ?: [$id], 3);
103
        $this->cache->save(
104
            $cacheItem
105
                ->set($object)
106
                ->tag($cacheTagger($object))
107
        );
108
109
        return $object;
110
    }
111
112
    /**
113
     * Load list of objects of some type and loggs the hits / misses.
114
     *
115
     * Load items from in-memory cache, symfony cache pool or backend in that order.
116
     * If not cached the returned objects will be placed in cache.
117
     *
118
     * @param string $key
119
     * @param callable $backendLoader Function for loading ALL objects, value is cached as-is.
120
     * @param callable $cacheTagger Gets cache object as argument, return array of cache tags.
121
     * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys.
122
     * @param callable $listTags Optional, global tags for the list cache.
123
     * @param array $arguments Optional, arguments when parnt method takes arguments that key varies on.
124
     *
125
     * @return array
126
     */
127
    final protected function getListCacheValue(
128
        string $key,
129
        callable $backendLoader,
130
        callable $cacheTagger,
131
        callable $cacheIndexes,
132
        callable $listTags = null,
133
        array $arguments = []
134
    ) {
135
        // In-memory
136
        if ($objects = $this->inMemory->get($key)) {
137
            $this->logger->logCacheHit($arguments, 3, true);
138
139
            return $objects;
140
        }
141
142
        // Cache pool
143
        $cacheItem = $this->cache->getItem($key);
144 View Code Duplication
        if ($cacheItem->isHit()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
145
            $this->logger->logCacheHit($arguments, 3);
146
            $this->inMemory->setMulti($objects = $cacheItem->get(), $cacheIndexes, $key);
147
148
            return $objects;
149
        }
150
151
        // Backend
152
        $objects = $backendLoader();
153
        $this->inMemory->setMulti($objects, $cacheIndexes, $key);
154
        $this->logger->logCacheMiss($arguments, 3);
155
156
        if ($listTags !== null) {
157
            $tagSet = [$listTags()];
158
        } else {
159
            $tagSet = [[]];
160
        }
161
162
        foreach ($objects as $object) {
163
            $tagSet[] = $cacheTagger($object);
164
        }
165
166
        $this->cache->save(
167
            $cacheItem
168
                ->set($objects)
169
                ->tag(array_unique(array_merge(...$tagSet)))
170
        );
171
172
        return $objects;
173
    }
174
175
    /**
176
     * Load several cache items from cache and loggs the hits / misses.
177
     *
178
     * Load items from in-memory cache, symfony cache pool or backend in that order.
179
     * If not cached the returned objects will be placed in cache.
180
     *
181
     * Cache items must be stored with a key in the following format "${keyPrefix}${id}", like "ez-content-info-${id}",
182
     * in order for this method to be able to prefix key on id's and also extract key prefix afterwards.
183
     *
184
     * @param array $ids
185
     * @param string $keyPrefix E.g "ez-content-"
186
     * @param callable $backendLoader Function for loading missing objects, gets array with missing id's as argument,
187
     *                                expects return value to be array with id as key. Missing items should be missing.
188
     * @param callable $cacheTagger Gets cache object as argument, return array of cache tags.
189
     * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys.
190
     * @param string $keySuffix Optional, e.g "-by-identifier"
191
     *
192
     * @return array
193
     */
194
    final protected function getMultipleCacheValues(
195
        array $ids,
196
        string $keyPrefix,
197
        callable $backendLoader,
198
        callable $cacheTagger,
199
        callable $cacheIndexes,
200
        string $keySuffix = '',
201
        array $arguments = []
202
    ): array {
203
        if (empty($ids)) {
204
            return [];
205
        }
206
207
        // Generate unique cache keys and check if in-memory
208
        $list = [];
209
        $cacheKeys = [];
210
        $cacheKeysToIdMap = [];
211
        foreach (array_unique($ids) as $id) {
212
            $key = $keyPrefix . $id . $keySuffix;
213
            if ($object = $this->inMemory->get($key)) {
214
                $list[$id] = $object;
215
            } else {
216
                $cacheKeys[] = $key;
217
                $cacheKeysToIdMap[$key] = $id;
218
            }
219
        }
220
221
        // No in-memory misses
222
        if (empty($cacheKeys)) {
223
            $this->logger->logCacheHit($arguments ?: $ids, 3, true);
224
225
            return $list;
226
        }
227
228
        // Load cache items by cache keys (will contain hits and misses)
229
        $loaded = [];
230
        $cacheMisses = [];
231
        foreach ($this->cache->getItems($cacheKeys) as $key => $cacheItem) {
232
            $id = $cacheKeysToIdMap[$key];
233
            if ($cacheItem->isHit()) {
234
                $list[$id] = $cacheItem->get();
235
                $loaded[$id] = $list[$id];
236
            } else {
237
                $cacheMisses[] = $id;
238
                $list[$id] = $cacheItem;
239
            }
240
        }
241
242
        // No cache pool misses, cache loaded items in-memory and return
243 View Code Duplication
        if (empty($cacheMisses)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
244
            $this->logger->logCacheHit($arguments ?: $ids, 3);
245
            $this->inMemory->setMulti($loaded, $cacheIndexes);
246
247
            return $list;
248
        }
249
250
        // Load missing items, save to cache & apply to list if found
251
        $backendLoadedList = $backendLoader($cacheMisses);
252
        foreach ($cacheMisses as $id) {
253
            if (isset($backendLoadedList[$id])) {
254
                $this->cache->save(
255
                    $list[$id]
256
                        ->set($backendLoadedList[$id])
257
                        ->tag($cacheTagger($backendLoadedList[$id]))
258
                );
259
                $loaded[$id] = $backendLoadedList[$id];
260
                $list[$id] = $backendLoadedList[$id];
261
            } else {
262
                // not found
263
                unset($list[$id]);
264
            }
265
        }
266
267
        $this->inMemory->setMulti($loaded, $cacheIndexes);
268
        unset($loaded, $backendLoadedList);
269
        $this->logger->logCacheMiss($arguments ?: $cacheMisses, 3);
270
271
        return $list;
272
    }
273
274
    /**
275
     * Escape an argument for use in cache keys when needed.
276
     *
277
     * WARNING: Only use the result of this in cache keys, it won't work to use loading the item from backend on miss.
278
     *
279
     * @param string $identifier
280
     *
281
     * @return string
282
     */
283 View Code Duplication
    final protected function escapeForCacheKey(string $identifier)
284
    {
285
        return \str_replace(
286
            ['_', '/', ':', '(', ')', '@', '\\', '{', '}'],
287
            ['__', '_S', '_C', '_BO', '_BC', '_A', '_BS', '_CBO', '_CBC'],
288
            $identifier
289
        );
290
    }
291
}
292