Passed
Push — master ( 90e408...d7adb4 )
by Alexander
07:18
created

Cache::addDependencyToValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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