Passed
Push — master ( f0915f...7925ec )
by Alexander
01:20
created

Cache::getOrSet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 4
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 3
rs 10
c 0
b 0
f 0
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 96
    public function __construct(\Psr\SimpleCache\CacheInterface $handler = null)
62
    {
63 96
        $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 93
    private function buildKey($key): string
77
    {
78 93
        if (!$this->keyNormalization) {
79 4
            $normalizedKey = $key;
80 90
        } elseif (\is_string($key)) {
81 88
            $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 93
        return $this->keyPrefix . $normalizedKey;
87
    }
88
89
90 74
    public function get($key, $default = null)
91
    {
92 74
        $key = $this->buildKey($key);
93 74
        $value = $this->handler->get($key, $default);
94 74
        $value = $this->getValueOrDefaultIfDependencyChanged($value, $default);
95
96 74
        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 16
    public function getMultiple($keys, $default = null): iterable
119
    {
120 16
        $keyMap = $this->buildKeyMap($keys);
121 16
        $values = $this->handler->getMultiple(array_keys($keyMap), $default);
122 16
        $values = $this->restoreKeys($values, $keyMap);
123 16
        $values = $this->getValuesOrDefaultIfDependencyChanged($values, $default);
124
125 16
        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 82
    public function set($key, $value, $ttl = null, Dependency $dependency = null): bool
143
    {
144 82
        $key = $this->buildKey($key);
145 82
        $dependency = $this->evaluateDependency($dependency);
146 82
        $value = $this->addDependencyToValue($value, $dependency);
147 82
        $ttl = $this->normalizeTtl($ttl);
148
149 82
        return $this->handler->set($key, $value, $ttl);
150
    }
151
152
    /**
153
     * Stores multiple values in cache. Each value contains a value identified by a key.
154
     * If the cache already contains such a key, the existing value and
155
     * expiration time will be replaced with the new ones, respectively.
156
     *
157
     * @param array $values the values to be cached, as key-value pairs.
158
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
159
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
160
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
161
     * @return bool True on success and false on failure.
162
     * @throws InvalidArgumentException
163
     */
164 19
    public function setMultiple($values, $ttl = null, Dependency $dependency = null): bool
165
    {
166 19
        $values = $this->prepareDataForSetOrAddMultiple($values, $dependency);
167 19
        $ttl = $this->normalizeTtl($ttl);
168 19
        return $this->handler->setMultiple($values, $ttl);
169
    }
170
171 1
    public function deleteMultiple($keys): bool
172
    {
173 1
        $keyMap = $this->buildKeyMap($this->iterableToArray($keys));
174 1
        return $this->handler->deleteMultiple(array_keys($keyMap));
175
    }
176
177
    /**
178
     * Stores multiple values in cache. Each value contains a value identified by a key.
179
     * If the cache already contains such a key, the existing value and expiration time will be preserved.
180
     *
181
     * @param array $values the values to be cached, as key-value pairs.
182
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
183
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
184
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
185
     * @return bool
186
     * @throws InvalidArgumentException
187
     */
188 1
    public function addMultiple(array $values, $ttl = null, Dependency $dependency = null): bool
189
    {
190 1
        $values = $this->prepareDataForSetOrAddMultiple($values, $dependency);
191 1
        $values = $this->excludeExistingValues($values);
192 1
        $ttl = $this->normalizeTtl($ttl);
193
194 1
        return $this->handler->setMultiple($values, $ttl);
195
    }
196
197 20
    private function prepareDataForSetOrAddMultiple(iterable $values, ?Dependency $dependency): array
198
    {
199 20
        $data = [];
200 20
        $dependency = $this->evaluateDependency($dependency);
201 20
        foreach ($values as $key => $value) {
202 20
            $value = $this->addDependencyToValue($value, $dependency);
203 20
            $key = $this->buildKey($key);
204 20
            $data[$key] = $value;
205
        }
206
207 20
        return $data;
208
    }
209
210
    /**
211
     * Stores a value identified by a key into cache if the cache does not contain this key.
212
     * Nothing will be done if the cache already contains the key.
213
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
214
     * a complex data structure consisting of factors representing the key.
215
     * @param mixed $value the value to be cached
216
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
217
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
218
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
219
     * @return bool whether the value is successfully stored into cache
220
     * @throws InvalidArgumentException
221
     */
222 2
    public function add($key, $value, $ttl = null, Dependency $dependency = null): bool
223
    {
224 2
        $key = $this->buildKey($key);
225
226 2
        if ($this->handler->has($key)) {
227 1
            return false;
228
        }
229
230 2
        $dependency = $this->evaluateDependency($dependency);
231 2
        $value = $this->addDependencyToValue($value, $dependency);
232 2
        $ttl = $this->normalizeTtl($ttl);
233
234 2
        return $this->handler->set($key, $value, $ttl);
235
    }
236
237
    /**
238
     * Deletes a value with the specified key from cache.
239
     * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or
240
     * a complex data structure consisting of factors representing the key.
241
     * @return bool if no error happens during deletion
242
     * @throws InvalidArgumentException
243
     */
244 10
    public function delete($key): bool
245
    {
246 10
        $key = $this->buildKey($key);
247
248 10
        return $this->handler->delete($key);
249
    }
250
251
    /**
252
     * Deletes all values from cache.
253
     * Be careful of performing this operation if the cache is shared among multiple applications.
254
     * @return bool whether the flush operation was successful.
255
     */
256 86
    public function clear(): bool
257
    {
258 86
        return $this->handler->clear();
259
    }
260
261
    /**
262
     * Method combines both {@see CacheInterface::set()} and {@see CacheInterface::get()} methods to retrieve
263
     * value identified by a $key, or to store the result of $callable execution if there is no cache available
264
     * for the $key.
265
     *
266
     * Usage example:
267
     *
268
     * ```php
269
     * public function getTopProducts($count = 10) {
270
     *     $cache = $this->cache;
271
     *     return $cache->getOrSet(['top-n-products', 'n' => $count], function ($cache) use ($count) {
272
     *         return $this->getTopNProductsFromDatabase($count);
273
     *     }, 1000);
274
     * }
275
     * ```
276
     *
277
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
278
     * a complex data structure consisting of factors representing the key.
279
     * @param callable|\Closure $callable the callable or closure that will be used to generate a value to be cached.
280
     * In case $callable returns `false`, the value will not be cached.
281
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
282
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
283
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
284
     * @return mixed result of $callable execution
285
     * @throws SetCacheException
286
     * @throws InvalidArgumentException
287
     */
288 4
    public function getOrSet($key, callable $callable, $ttl = null, Dependency $dependency = null)
289
    {
290 4
        if (($value = $this->get($key)) !== null) {
291 1
            return $value;
292
        }
293
294 4
        $value = $callable($this);
295 4
        $ttl = $this->normalizeTtl($ttl);
296 4
        if (!$this->set($key, $value, $ttl, $dependency)) {
297 2
            throw new SetCacheException($key, $value, $this);
298
        }
299
300 2
        return $value;
301
    }
302
303 3
    public function enableKeyNormalization(): void
304
    {
305 3
        $this->keyNormalization = true;
306
    }
307
308 4
    public function disableKeyNormalization(): void
309
    {
310 4
        $this->keyNormalization = false;
311
    }
312
313
    /**
314
     * @param string $keyPrefix a string prefixed to every cache key so that it is unique globally in the whole cache storage.
315
     * It is recommended that you set a unique cache key prefix for each application if the same cache
316
     * storage is being used by different applications.
317
     */
318 7
    public function setKeyPrefix(string $keyPrefix): void
319
    {
320 7
        if ($keyPrefix !== '' && !ctype_alnum($keyPrefix)) {
321 1
            throw new \Yiisoft\Cache\Exception\InvalidArgumentException('Cache key prefix should be alphanumeric');
322
        }
323 6
        $this->keyPrefix = $keyPrefix;
324
    }
325
326
    /**
327
     * @return int|null
328
     */
329 2
    public function getDefaultTtl(): ?int
330
    {
331 2
        return $this->defaultTtl;
332
    }
333
334
    /**
335
     * @param int|DateInterval|null $defaultTtl
336
     */
337 2
    public function setDefaultTtl($defaultTtl): void
338
    {
339 2
        $this->defaultTtl = $this->normalizeTtl($defaultTtl);
340
    }
341
342
    /**
343
     * @noinspection PhpDocMissingThrowsInspection DateTime won't throw exception because constant string is passed as time
344
     *
345
     * Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
346
     * @param int|DateInterval|null $ttl raw TTL.
347
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
348
     */
349 94
    protected function normalizeTtl($ttl): ?int
350
    {
351 94
        if ($ttl === null) {
352 90
            return $this->defaultTtl;
353
        }
354
355 6
        if ($ttl instanceof DateInterval) {
356 2
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
357
        }
358
359 4
        return $ttl;
360
    }
361
362
    /**
363
     * Converts iterable to array
364
     * @param iterable $iterable
365
     * @return array
366
     */
367 1
    private function iterableToArray(iterable $iterable): array
368
    {
369 1
        return $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
370
    }
371
372
    /**
373
     * Evaluates dependency if it is not null
374
     * @param Dependency|null $dependency
375
     * @return Dependency|null
376
     */
377 92
    private function evaluateDependency(?Dependency $dependency)
378
    {
379 92
        if ($dependency !== null) {
380 7
            $dependency->evaluateDependency($this);
381
        }
382
383 92
        return $dependency;
384
    }
385
386
    /**
387
     * Returns array of value and dependency or just value if dependency is null
388
     * @param mixed $value
389
     * @param Dependency|null $dependency
390
     * @return mixed
391
     */
392 92
    private function addDependencyToValue($value, ?Dependency $dependency)
393
    {
394 92
        if ($dependency === null) {
395 90
            return $value;
396
        }
397
398 7
        return [$value, $dependency];
399
    }
400
401
    /**
402
     * Checks for the existing values and returns only values that are not in the cache yet.
403
     * @param array $values
404
     * @return array
405
     */
406 1
    private function excludeExistingValues(array $values): array
407
    {
408 1
        $existingValues = $this->handler->getMultiple(array_keys($values));
409 1
        foreach ($existingValues as $key => $value) {
410 1
            if ($value !== null) {
411 1
                unset($values[$key]);
412
            }
413
        }
414
415 1
        return $values;
416
    }
417
418
    /**
419
     * Returns value if there is no dependency or it has not been changed and default value otherwise.
420
     * @param mixed $value
421
     * @param mixed $default
422
     * @return mixed
423
     */
424 82
    private function getValueOrDefaultIfDependencyChanged($value, $default)
425
    {
426 82
        if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
427
            /** @var Dependency $dependency */
428 6
            [$value, $dependency] = $value;
429 6
            if ($dependency->isChanged($this)) {
430 4
                return $default;
431
            }
432
        }
433
434 82
        return $value;
435
    }
436
437
    /**
438
     * Returns values without dependencies or if dependency has not been changed and default values otherwise.
439
     * @param iterable $values
440
     * @param mixed $default
441
     * @return array
442
     */
443 16
    private function getValuesOrDefaultIfDependencyChanged(iterable $values, $default): array
444
    {
445 16
        $results = [];
446 16
        foreach ($values as $key => $value) {
447 16
            $results[$key] = $this->getValueOrDefaultIfDependencyChanged($value, $default);
448
        }
449
450 16
        return $results;
451
    }
452
453
    /**
454
     * Builds key map `[built_key => key]`
455
     * @param array $keys
456
     * @return array
457
     */
458 16
    private function buildKeyMap(array $keys): array
459
    {
460 16
        $keyMap = [];
461 16
        foreach ($keys as $key) {
462 16
            $keyMap[$this->buildKey($key)] = $key;
463
        }
464
465 16
        return $keyMap;
466
    }
467
468
    /**
469
     * Restores original keys
470
     * @param iterable $values
471
     * @param array $keyMap
472
     * @return array
473
     */
474 16
    private function restoreKeys(iterable $values, array $keyMap): array
475
    {
476 16
        $results = [];
477 16
        foreach ($values as $key => $value) {
478 16
            $restoredKey = $key;
479 16
            if (array_key_exists($key, $keyMap)) {
480 16
                $restoredKey = $keyMap[$key];
481
            }
482 16
            $results[$restoredKey] = $value;
483
        }
484
485 16
        return $results;
486
    }
487
}
488