Completed
Pull Request — master (#32)
by Alexander
01:45
created

Cache::getMultiple()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
nc 1
nop 2
dl 0
loc 8
c 0
b 0
f 0
cc 1
rs 10
ccs 6
cts 6
cp 1
crap 1
1
<?php declare(strict_types=1);
2
3
namespace Yiisoft\Cache;
4
5
use DateInterval;
6
use DateTime;
7
use Psr\SimpleCache\InvalidArgumentException;
8
use Yiisoft\Cache\Dependency\Dependency;
9
use Yiisoft\Cache\Exception\SetCacheException;
10
11
/**
12
 * Cache provides support for the data caching, including cache key composition and dependencies.
13
 * The actual data caching is performed via {@see Cache::$handler}, which should be configured
14
 * to be {@see \Psr\SimpleCache\CacheInterface} instance.
15
 *
16
 * A value can be stored in the cache by calling {@see CacheInterface::set()} and be retrieved back
17
 * later (in the same or different request) by {@see CacheInterface::get()}. In both operations,
18
 * a key identifying the value is required. An expiration time and/or a {@see Dependency}
19
 * can also be specified when calling {@see CacheInterface::set()}. If the value expires or the dependency
20
 * changes at the time of calling {@see CacheInterface::get()}, the cache will return no data.
21
 *
22
 * A typical usage pattern of cache is like the following:
23
 *
24
 * ```php
25
 * $key = 'demo';
26
 * $data = $cache->get($key);
27
 * if ($data === null) {
28
 *     // ...generate $data here...
29
 *     $cache->set($key, $data, $ttl, $dependency);
30
 * }
31
 * ```
32
 *
33
 * For more details and usage information on Cache, see
34
 * [PSR-16 specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md).
35
 */
36
final class Cache implements CacheInterface
37
{
38
    /**
39
     * @var \Psr\SimpleCache\CacheInterface actual cache handler.
40
     */
41
    private $handler;
42
43
    /**
44
     * @var string a string prefixed to every cache key so that it is unique globally in the whole cache storage.
45
     * It is recommended that you set a unique cache key prefix for each application if the same cache
46
     * storage is being used by different applications.
47
     */
48
    private $keyPrefix = '';
49
50
    private $keyNormalization = true;
51
52
    /**
53
     * @var int|null default TTL for a cache entry. null meaning infinity, negative or zero results in cache key deletion.
54
     * This value is used by {@see set()} and {@see setMultiple()}, if the duration is not explicitly given.
55
     */
56
    private $defaultTtl;
57
58
    /**
59
     * @param \Psr\SimpleCache\CacheInterface cache handler.
60
     */
61 93
    public function __construct(\Psr\SimpleCache\CacheInterface $handler = null)
62
    {
63 93
        $this->handler = $handler;
64
    }
65
66
    /**
67
     * Builds a normalized cache key from a given key.
68
     *
69
     * If the given key is a string containing alphanumeric characters only and no more than 32 characters,
70
     * then the key will be returned back as it is. Otherwise, a normalized key is generated by serializing
71
     * the given key and applying MD5 hashing.
72
     *
73
     * @param mixed $key the key to be normalized
74
     * @return string the generated cache key
75
     */
76 90
    private function buildKey($key): string
77
    {
78 90
        if (!$this->keyNormalization) {
79 4
            $normalizedKey = $key;
80 87
        } elseif (\is_string($key)) {
81 85
            $normalizedKey = ctype_alnum($key) && mb_strlen($key, '8bit') <= 32 ? $key : md5($key);
82
        } else {
83 2
            $normalizedKey = $this->keyPrefix . md5(json_encode($key));
84
        }
85
86 90
        return $this->keyPrefix . $normalizedKey;
87
    }
88
89
90 72
    public function get($key, $default = null)
91
    {
92 72
        $key = $this->buildKey($key);
93 72
        $value = $this->handler->get($key, $default);
94 72
        $value = $this->verifyDependency($value, $default);
95
96 72
        return $value;
97
    }
98
99
100 19
    public function has($key): bool
101
    {
102 19
        $key = $this->buildKey($key);
103 19
        return $this->handler->has($key);
104
    }
105
106
    /**
107
     * Retrieves multiple values from cache with the specified keys.
108
     * Some caches, such as memcached or apcu, allow retrieving multiple cached values at the same time,
109
     * which may improve the performance. In case a cache does not support this feature natively,
110
     * this method will try to simulate it.
111
     * @param string[] $keys list of string keys identifying the cached values
112
     * @param mixed $default Default value to return for keys that do not exist.
113
     * @return iterable list of cached values corresponding to the specified keys. The array
114
     * is returned in terms of (key, value) pairs.
115
     * If a value is not cached or expired, the corresponding array value will be false.
116
     * @throws InvalidArgumentException
117
     */
118 15
    public function getMultiple($keys, $default = null): iterable
119
    {
120 15
        $keyMap = $this->buildKeyMap($keys);
121 15
        $values = $this->handler->getMultiple(array_keys($keyMap), $default);
122 15
        $values = $this->restoreKeys($values, $keyMap);
123 15
        $values = $this->verifyDependencyMultiple($values, $default);
124
125 15
        return $values;
126
    }
127
128
    /**
129
     * Stores a value identified by a key into cache.
130
     * If the cache already contains such a key, the existing value and
131
     * expiration time will be replaced with the new ones, respectively.
132
     *
133
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
134
     * a complex data structure consisting of factors representing the key.
135
     * @param mixed $value the value to be cached
136
     * @param null|int|\DateInterval $ttl the TTL of this value. If not set, default value is used.
137
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
138
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
139
     * @return bool whether the value is successfully stored into cache
140
     * @throws InvalidArgumentException
141
     */
142 79
    public function set($key, $value, $ttl = null, Dependency $dependency = null): bool
143
    {
144 79
        $key = $this->buildKey($key);
145 79
        $value = $this->addDependency($value, $dependency);
146 79
        $ttl = $this->normalizeTtl($ttl);
147
148 79
        return $this->handler->set($key, $value, $ttl);
149
    }
150
151
    /**
152
     * Stores multiple values in cache. Each value contains a value identified by a key.
153
     * If the cache already contains such a key, the existing value and
154
     * expiration time will be replaced with the new ones, respectively.
155
     *
156
     * @param array $values the values to be cached, as key-value pairs.
157
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
158
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
159
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
160
     * @return bool True on success and false on failure.
161
     * @throws InvalidArgumentException
162
     */
163 18
    public function setMultiple($values, $ttl = null, Dependency $dependency = null): bool
164
    {
165 18
        $values = $this->prepareDataForSetOrAddMultiple($values, $dependency);
166 18
        $ttl = $this->normalizeTtl($ttl);
167 18
        return $this->handler->setMultiple($values, $ttl);
168
    }
169
170 1
    public function deleteMultiple($keys): bool
171
    {
172 1
        $keyMap = $this->buildKeyMap($this->iterableToArray($keys));
173 1
        return $this->handler->deleteMultiple(array_keys($keyMap));
174
    }
175
176
    /**
177
     * Stores multiple values in cache. Each value contains a value identified by a key.
178
     * If the cache already contains such a key, the existing value and expiration time will be preserved.
179
     *
180
     * @param array $values the values to be cached, as key-value pairs.
181
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
182
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
183
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
184
     * @return bool
185
     * @throws InvalidArgumentException
186
     */
187 1
    public function addMultiple(array $values, $ttl = null, Dependency $dependency = null): bool
188
    {
189 1
        $values = $this->prepareDataForSetOrAddMultiple($values, $dependency);
190 1
        $values = $this->excludeExistingValues($values);
191 1
        $ttl = $this->normalizeTtl($ttl);
192
193 1
        return $this->handler->setMultiple($values, $ttl);
194
    }
195
196 19
    private function prepareDataForSetOrAddMultiple(iterable $values, ?Dependency $dependency): array
197
    {
198 19
        $data = [];
199 19
        foreach ($values as $key => $value) {
200 19
            $value = $this->addDependency($value, $dependency);
201 19
            $key = $this->buildKey($key);
202 19
            $data[$key] = $value;
203
        }
204
205 19
        return $data;
206
    }
207
208
    /**
209
     * Stores a value identified by a key into cache if the cache does not contain this key.
210
     * Nothing will be done if the cache already contains the key.
211
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
212
     * a complex data structure consisting of factors representing the key.
213
     * @param mixed $value the value to be cached
214
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
215
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
216
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
217
     * @return bool whether the value is successfully stored into cache
218
     * @throws InvalidArgumentException
219
     */
220 2
    public function add($key, $value, $ttl = null, Dependency $dependency = null): bool
221
    {
222 2
        $key = $this->buildKey($key);
223
224 2
        if ($this->handler->has($key)) {
225 1
            return false;
226
        }
227
228 2
        $value = $this->addDependency($value, $dependency);
229 2
        $ttl = $this->normalizeTtl($ttl);
230
231 2
        return $this->handler->set($key, $value, $ttl);
232
    }
233
234
    /**
235
     * Deletes a value with the specified key from cache.
236
     * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or
237
     * a complex data structure consisting of factors representing the key.
238
     * @return bool if no error happens during deletion
239
     * @throws InvalidArgumentException
240
     */
241 10
    public function delete($key): bool
242
    {
243 10
        $key = $this->buildKey($key);
244
245 10
        return $this->handler->delete($key);
246
    }
247
248
    /**
249
     * Deletes all values from cache.
250
     * Be careful of performing this operation if the cache is shared among multiple applications.
251
     * @return bool whether the flush operation was successful.
252
     */
253 86
    public function clear(): bool
254
    {
255 86
        return $this->handler->clear();
256
    }
257
258
    /**
259
     * Method combines both {@see CacheInterface::set()} and {@see CacheInterface::get()} methods to retrieve
260
     * value identified by a $key, or to store the result of $callable execution if there is no cache available
261
     * for the $key.
262
     *
263
     * Usage example:
264
     *
265
     * ```php
266
     * public function getTopProducts($count = 10) {
267
     *     $cache = $this->cache;
268
     *     return $cache->getOrSet(['top-n-products', 'n' => $count], function ($cache) use ($count) {
269
     *         return $this->getTopNProductsFromDatabase($count);
270
     *     }, 1000);
271
     * }
272
     * ```
273
     *
274
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
275
     * a complex data structure consisting of factors representing the key.
276
     * @param callable|\Closure $callable the callable or closure that will be used to generate a value to be cached.
277
     * In case $callable returns `false`, the value will not be cached.
278
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
279
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
280
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
281
     * @return mixed result of $callable execution
282
     * @throws SetCacheException
283
     * @throws InvalidArgumentException
284
     */
285 4
    public function getOrSet($key, callable $callable, $ttl = null, Dependency $dependency = null)
286
    {
287 4
        if (($value = $this->get($key)) !== null) {
288 1
            return $value;
289
        }
290
291 4
        $value = $callable($this);
292 4
        $ttl = $this->normalizeTtl($ttl);
293 4
        if (!$this->set($key, $value, $ttl, $dependency)) {
294 2
            throw new SetCacheException($key, $value, $this);
295
        }
296
297 2
        return $value;
298
    }
299
300 3
    public function enableKeyNormalization(): void
301
    {
302 3
        $this->keyNormalization = true;
303
    }
304
305 4
    public function disableKeyNormalization(): void
306
    {
307 4
        $this->keyNormalization = false;
308
    }
309
310
    /**
311
     * @param string $keyPrefix a string prefixed to every cache key so that it is unique globally in the whole cache storage.
312
     * It is recommended that you set a unique cache key prefix for each application if the same cache
313
     * storage is being used by different applications.
314
     */
315 7
    public function setKeyPrefix(string $keyPrefix): void
316
    {
317 7
        if ($keyPrefix !== '' && !ctype_alnum($keyPrefix)) {
318 1
            throw new \Yiisoft\Cache\Exception\InvalidArgumentException('Cache key prefix should be alphanumeric');
319
        }
320 6
        $this->keyPrefix = $keyPrefix;
321
    }
322
323
    /**
324
     * @return int|null
325
     */
326 2
    public function getDefaultTtl(): ?int
327
    {
328 2
        return $this->defaultTtl;
329
    }
330
331
    /**
332
     * @param int|DateInterval|null $defaultTtl
333
     */
334 2
    public function setDefaultTtl($defaultTtl): void
335
    {
336 2
        $this->defaultTtl = $this->normalizeTtl($defaultTtl);
337
    }
338
339
    /**
340
     * @noinspection PhpDocMissingThrowsInspection DateTime won't throw exception because constant string is passed as time
341
     *
342
     * Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
343
     * @param int|DateInterval|null $ttl raw TTL.
344
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
345
     */
346 91
    protected function normalizeTtl($ttl): ?int
347
    {
348 91
        if ($ttl === null) {
349 87
            return $this->defaultTtl;
350
        }
351
352 6
        if ($ttl instanceof DateInterval) {
353 2
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
354
        }
355
356 4
        return $ttl;
357
    }
358
359
    /**
360
     * Converts iterable to array
361
     * @param iterable $iterable
362
     * @return array
363
     */
364 1
    private function iterableToArray(iterable $iterable): array
365
    {
366 1
        return $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
367
    }
368
369
370
    /**
371
     * Evaluates dependency if it is not null and adds it to the value
372
     * @param mixed $value
373
     * @param Dependency|null $dependency
374
     * @return mixed
375
     */
376 89
    private function addDependency($value, ?Dependency $dependency)
377
    {
378 89
        if ($dependency === null) {
379 89
            return $value;
380
        }
381
382 4
        if (!$dependency->isEvaluated()) {
383 4
            $dependency->evaluateDependency($this);
384
        }
385
386 4
        return [$value, $dependency];
387
    }
388
389
    /**
390
     * Checks for the existing values and returns only values that are not in the cache yet.
391
     * @param array $values
392
     * @return array
393
     */
394 1
    private function excludeExistingValues(array $values): array
395
    {
396 1
        $existingValues = $this->handler->getMultiple(array_keys($values));
397 1
        foreach ($existingValues as $key => $value) {
398 1
            if ($value !== null) {
399 1
                unset($values[$key]);
400
            }
401
        }
402
403 1
        return $values;
404
    }
405
406
    /**
407
     * Checks whether value has a dependency and verifies whether it has changed
408
     * @param mixed $value
409
     * @param mixed $default
410
     * @return mixed
411
     */
412 79
    private function verifyDependency($value, $default)
413
    {
414 79
        if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
415
            /** @var Dependency $dependency */
416 4
            [$value, $dependency] = $value;
417 4
            if ($dependency->isChanged($this)) {
418 4
                return $default;
419
            }
420 4
            return $value;
421
        }
422
423 79
        return $value;
424
    }
425
426
    /**
427
     * Builds key map `[built_key => key]`
428
     * @param array $keys
429
     * @return array
430
     */
431 15
    private function buildKeyMap(array $keys): array
432
    {
433 15
        $keyMap = [];
434 15
        foreach ($keys as $key) {
435 15
            $keyMap[$this->buildKey($key)] = $key;
436
        }
437
438 15
        return $keyMap;
439
    }
440
441
    /**
442
     * Restores original keys
443
     * @param iterable $values
444
     * @param array $keyMap
445
     * @return array
446
     */
447 15
    private function restoreKeys(iterable $values, array $keyMap): array
448
    {
449 15
        $results = [];
450 15
        foreach ($values as $key => $value) {
451 15
            $restoredKey = $key;
452 15
            if (array_key_exists($key, $keyMap)) {
453 15
                $restoredKey = $keyMap[$key];
454
            }
455 15
            $results[$restoredKey] = $value;
456
        }
457
458 15
        return $results;
459
    }
460
461
    /**
462
     * Verifies dependency for each value
463
     * @param iterable $values
464
     * @param mixed $default
465
     * @return array
466
     */
467 15
    private function verifyDependencyMultiple(iterable $values, $default): array
468
    {
469 15
        $results = [];
470 15
        foreach ($values as $key => $value) {
471 15
            $results[$key] = $this->verifyDependency($value, $default);
472
        }
473
474 15
        return $results;
475
    }
476
}
477