Passed
Pull Request — master (#53)
by Rustam
01:45
created

Cache::setMultiple()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 5
ccs 1
cts 1
cp 1
crap 1
rs 10
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
    public function __construct(\Psr\SimpleCache\CacheInterface $handler)
59
    {
60 110
        $this->handler = $handler;
61
    }
62 110
63 110
    /**
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
    private function buildKey($key): string
70
    {
71
        if (!is_scalar($key)) {
72
            throw new \Yiisoft\Cache\Exception\InvalidArgumentException('Invalid key.');
73
        }
74 106
        return $this->keyPrefix . $key;
75
    }
76 106
77 4
78 103
    public function get($key, $default = null)
79 100
    {
80 100
        $key = $this->buildKey($key);
81
        $value = $this->handler->get($key, $default);
82 3
        $value = $this->getValueOrDefaultIfDependencyChanged($value, $default);
83 3
84 1
        return $value;
85
    }
86 2
87
88
    public function has($key): bool
89 105
    {
90
        $key = $this->buildKey($key);
91
        return $this->handler->has($key);
92
    }
93 84
94
    /**
95 84
     * Retrieves multiple values from cache with the specified keys.
96 84
     * Some caches, such as memcached or apcu, allow retrieving multiple cached values at the same time,
97 84
     * which may improve the performance. In case a cache does not support this feature natively,
98
     * this method will try to simulate it.
99 84
     * @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 21
     * If a value is not cached or expired, the corresponding array value will be false.
104
     * @throws InvalidArgumentException
105 21
     * @suppress PhanTypeInvalidThrowsIsInterface
106 21
     */
107
    public function getMultiple($keys, $default = null): iterable
108
    {
109
        $keyMap = $this->buildKeyMap($this->iterableToArray($keys));
110
        $values = $this->handler->getMultiple($this->getKeys($keyMap), $default);
111
        $values = $this->restoreKeys($values, $keyMap);
112
        $values = $this->getValuesOrDefaultIfDependencyChanged($values, $default);
113
114
        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 16
     * @param string $key a key identifying the value to be cached.
123
     * @param mixed $value the value to be cached
124 16
     * @param null|int|\DateInterval $ttl the TTL of this value. If not set, default value is used.
125 16
     * @param Dependency|null $dependency dependency of the cached value. If the dependency changes,
126 16
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
127 16
     * @return bool whether the value is successfully stored into cache
128
     * @throws InvalidArgumentException
129 16
     * @suppress PhanTypeInvalidThrowsIsInterface
130
     */
131
    public function set($key, $value, $ttl = null, Dependency $dependency = null): bool
132
    {
133
        $key = $this->buildKey($key);
134
        $dependency = $this->evaluateDependency($dependency);
135
        $value = $this->addDependencyToValue($value, $dependency);
136
        $ttl = $this->normalizeTtl($ttl);
137
138
        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 95
     * @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 95
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
150 94
     * @return bool True on success and false on failure.
151 94
     * @throws InvalidArgumentException
152 94
     * @suppress PhanTypeInvalidThrowsIsInterface
153
     */
154 94
    public function setMultiple($values, $ttl = null, Dependency $dependency = null): bool
155
    {
156
        $values = $this->prepareDataForSetOrAddMultiple($values, $dependency);
157
        $ttl = $this->normalizeTtl($ttl);
158
        return $this->handler->setMultiple($values, $ttl);
159
    }
160
161
    public function deleteMultiple($keys): bool
162
    {
163
        $keyMap = $this->buildKeyMap($this->iterableToArray($keys));
164
        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 19
     *
171
     * @param array $values the values to be cached, as key-value pairs.
172 19
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
173 19
     * @param Dependency|null $dependency dependency of the cached values. If the dependency changes,
174 19
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
175
     * @return bool
176
     * @throws InvalidArgumentException
177 1
     * @suppress PhanTypeInvalidThrowsIsInterface
178
     */
179 1
    public function addMultiple(array $values, $ttl = null, Dependency $dependency = null): bool
180 1
    {
181
        $values = $this->prepareDataForSetOrAddMultiple($values, $dependency);
182
        $values = $this->excludeExistingValues($values);
183
        $ttl = $this->normalizeTtl($ttl);
184
185
        return $this->handler->setMultiple($values, $ttl);
186
    }
187
188
    private function prepareDataForSetOrAddMultiple(iterable $values, ?Dependency $dependency): array
189
    {
190
        $data = [];
191
        $dependency = $this->evaluateDependency($dependency);
192
        foreach ($values as $key => $value) {
193
            $value = $this->addDependencyToValue($value, $dependency);
194
            $key = $this->buildKey($key);
195 1
            $data[$key] = $value;
196
        }
197 1
198 1
        return $data;
199 1
    }
200
201 1
    /**
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 20
     * @param string $key a key identifying the value to be cached.
205
     * @param mixed $value the value to be cached
206 20
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
207 20
     * @param Dependency|null $dependency dependency of the cached value. If the dependency changes,
208 20
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
209 20
     * @return bool whether the value is successfully stored into cache
210 20
     * @throws InvalidArgumentException
211 20
     * @suppress PhanTypeInvalidThrowsIsInterface
212
     */
213
    public function add(string $key, $value, $ttl = null, Dependency $dependency = null): bool
214 20
    {
215
        $key = $this->buildKey($key);
216
217
        if ($this->handler->has($key)) {
218
            return false;
219
        }
220
221
        $dependency = $this->evaluateDependency($dependency);
222
        $value = $this->addDependencyToValue($value, $dependency);
223
        $ttl = $this->normalizeTtl($ttl);
224
225
        return $this->handler->set($key, $value, $ttl);
226
    }
227
228
    /**
229
     * Deletes a value with the specified key from cache.
230 2
     * @param mixed $key a key identifying the value to be deleted from cache.
231
     * @return bool if no error happens during deletion
232 2
     * @throws InvalidArgumentException
233
     * @suppress PhanTypeInvalidThrowsIsInterface
234 2
     */
235 1
    public function delete($key): bool
236
    {
237
        $key = $this->buildKey($key);
238 2
239 2
        return $this->handler->delete($key);
240 2
    }
241
242 2
    /**
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
    public function clear(): bool
248
    {
249
        return $this->handler->clear();
250
    }
251
252
    /**
253 12
     * 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 12
     * for the $key.
256
     *
257 12
     * 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 99
     * }
266
     * ```
267 99
     *
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
    public function getOrSet(string $key, callable $callable, $ttl = null, Dependency $dependency = null)
280
    {
281
        if (($value = $this->get($key)) !== null) {
282
            return $value;
283
        }
284
285
        $value = $callable($this);
286
        $ttl = $this->normalizeTtl($ttl);
287
        if (!$this->set($key, $value, $ttl, $dependency)) {
288
            throw new SetCacheException($key, $value, $this);
289
        }
290
291
        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 4
     */
299
    public function setKeyPrefix(string $keyPrefix): void
300 4
    {
301 1
        if ($keyPrefix !== '' && !ctype_alnum($keyPrefix)) {
302
            throw new \Yiisoft\Cache\Exception\InvalidArgumentException('Cache key prefix should be alphanumeric');
303
        }
304 4
        $this->keyPrefix = $keyPrefix;
305 4
    }
306 4
307 2
    /**
308
     * @return int|null
309
     */
310 2
    public function getDefaultTtl(): ?int
311
    {
312
        return $this->defaultTtl;
313 3
    }
314
315 3
    /**
316 3
     * @param int|DateInterval|null $defaultTtl
317
     */
318 4
    public function setDefaultTtl($defaultTtl): void
319
    {
320 4
        $this->defaultTtl = $this->normalizeTtl($defaultTtl);
321 4
    }
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 7
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
329
     * @suppress PhanPossiblyFalseTypeReturn
330 7
     */
331 1
    protected function normalizeTtl($ttl): ?int
332
    {
333 6
        if ($ttl === null) {
334 6
            return $this->defaultTtl;
335
        }
336
337
        if ($ttl instanceof DateInterval) {
338
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
339 2
        }
340
341 2
        return $ttl;
342
    }
343
344
    /**
345
     * Converts iterable to array
346
     * @param iterable $iterable
347 2
     * @return array
348
     */
349 2
    private function iterableToArray(iterable $iterable): array
350 2
    {
351
        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
    private function evaluateDependency(?Dependency $dependency): ?Dependency
360 106
    {
361
        if ($dependency !== null) {
362 106
            $dependency->evaluateDependency($this);
363 102
        }
364
365
        return $dependency;
366 6
    }
367 2
368
    /**
369
     * Returns array of value and dependency or just value if dependency is null
370 4
     * @param mixed $value
371
     * @param Dependency|null $dependency
372
     * @return mixed
373
     */
374
    private function addDependencyToValue($value, ?Dependency $dependency)
375
    {
376
        if ($dependency === null) {
377
            return $value;
378 16
        }
379
380 16
        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 104
    private function excludeExistingValues(array $values): array
389
    {
390 104
        $existingValues = $this->handler->getMultiple($this->getKeys($values));
391 7
        foreach ($existingValues as $key => $value) {
392
            if ($value !== null) {
393
                unset($values[$key]);
394 104
            }
395
        }
396
397
        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 104
     * @param mixed $default
404
     * @return mixed
405 104
     */
406 102
    private function getValueOrDefaultIfDependencyChanged($value, $default)
407
    {
408
        if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
409 7
            /** @var Dependency $dependency */
410
            [$value, $dependency] = $value;
411
            if ($dependency->isChanged($this)) {
412
                return $default;
413
            }
414
        }
415
416
        return $value;
417 1
    }
418
419 1
    /**
420 1
     * Returns values without dependencies or if dependency has not been changed and default values otherwise.
421 1
     * @param iterable $values
422 1
     * @param mixed $default
423
     * @return array
424
     */
425
    private function getValuesOrDefaultIfDependencyChanged(iterable $values, $default): array
426 1
    {
427
        $results = [];
428
        foreach ($values as $key => $value) {
429
            $results[$key] = $this->getValueOrDefaultIfDependencyChanged($value, $default);
430
        }
431
432
        return $results;
433
    }
434
435 92
    /**
436
     * Builds key map `[built_key => key]`
437 92
     * @param array $keys
438
     * @return array
439 6
     */
440 6
    private function buildKeyMap(array $keys): array
441 4
    {
442
        $keyMap = [];
443
        foreach ($keys as $key) {
444
            $keyMap[$this->buildKey($key)] = $key;
445 92
        }
446
447
        return $keyMap;
448
    }
449
450
    /**
451
     * Restores original keys
452
     * @param iterable $values
453
     * @param array $keyMap
454 16
     * @return array
455
     */
456 16
    private function restoreKeys(iterable $values, array $keyMap): array
457 16
    {
458 16
        $results = [];
459
        foreach ($values as $key => $value) {
460
            $restoredKey = $key;
461 16
            if (array_key_exists($key, $keyMap)) {
462
                $restoredKey = $keyMap[$key];
463
            }
464
            $results[$restoredKey] = $value;
465
        }
466
467
        return $results;
468
    }
469 16
470
    /**
471 16
     * @param array $data
472 16
     * @return array
473 16
     */
474
    private function getKeys(array $data): array
475
    {
476 16
        return array_map('strval', array_keys($data));
477
    }
478
}
479