Completed
Push — master ( 47d35e...4232be )
by Alexander
02:04
created

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