Passed
Pull Request — master (#33)
by Alexander
01:42
created

Cache::getValuesOrDefaultIfDependencyChanged()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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