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()) { |
|
|
|
|
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()) { |
|
|
|
|
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)) { |
|
|
|
|
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
|
|
|
|
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.