Passed
Branch master (0b4094)
by Alexander
01:32
created

Cache::get()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 7
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 5
rs 9.6111
1
<?php
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 348
    public function __construct(\Psr\SimpleCache\CacheInterface $handler = null)
62
    {
63 348
        $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 340
    private function buildKey($key): string
77
    {
78 340
        if (!$this->keyNormalization) {
79 16
            $normalizedKey = $key;
80 328
        } elseif (\is_string($key)) {
81 320
            $normalizedKey = ctype_alnum($key) && mb_strlen($key, '8bit') <= 32 ? $key : md5($key);
82
        } else {
83 8
            $normalizedKey = $this->keyPrefix . md5(json_encode($key));
84
        }
85
86 340
        return $this->keyPrefix . $normalizedKey;
87
    }
88
89
90 272
    public function get($key, $default = null)
91
    {
92 272
        $key = $this->buildKey($key);
93 272
        $value = $this->handler->get($key, $default);
94
95 272
        if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
96 5
            if ($value[1]->isChanged($this)) {
97 5
                return $default;
98
            }
99 5
            $value = $value[0];
100
        }
101
102 272
        return $value;
103
    }
104
105
106 70
    public function has($key): bool
107
    {
108 70
        $key = $this->buildKey($key);
109 70
        return $this->handler->has($key);
110
    }
111
112
    /**
113
     * Retrieves multiple values from cache with the specified keys.
114
     * Some caches, such as memcached or apcu, allow retrieving multiple cached values at the same time,
115
     * which may improve the performance. In case a cache does not support this feature natively,
116
     * this method will try to simulate it.
117
     * @param string[] $keys list of string keys identifying the cached values
118
     * @param mixed $default Default value to return for keys that do not exist.
119
     * @return iterable list of cached values corresponding to the specified keys. The array
120
     * is returned in terms of (key, value) pairs.
121
     * If a value is not cached or expired, the corresponding array value will be false.
122
     * @throws InvalidArgumentException
123
     */
124 49
    public function getMultiple($keys, $default = null): iterable
125
    {
126 49
        $keyMap = [];
127 49
        foreach ($keys as $key) {
128 49
            $keyMap[$key] = $this->buildKey($key);
129
        }
130 49
        $values = $this->handler->getMultiple(array_values($keyMap), $default);
131 49
        $results = [];
132 49
        foreach ($keyMap as $key => $newKey) {
133 49
            $results[$key] = $default;
134 49
            if (array_key_exists($newKey, $this->iterableToArray($values))) {
135 49
                $value = $values[$newKey];
136 49
                if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
137
                    if ($value[1]->isChanged($this)) {
138
                        continue;
139
                    }
140
141
                    $value = $value[0];
142
                }
143 49
                $results[$key] = $value;
144
            }
145
        }
146
147 49
        return $results;
148
    }
149
150
    /**
151
     * Stores a value identified by a key into cache.
152
     * If the cache already contains such a key, the existing value and
153
     * expiration time will be replaced with the new ones, respectively.
154
     *
155
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
156
     * a complex data structure consisting of factors representing the key.
157
     * @param mixed $value the value to be cached
158
     * @param null|int|\DateInterval $ttl the TTL of this value. If not set, default value is used.
159
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
160
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
161
     * @return bool whether the value is successfully stored into cache
162
     * @throws InvalidArgumentException
163
     */
164 304
    public function set($key, $value, $ttl = null, Dependency $dependency = null): bool
165
    {
166 304
        $ttl = $this->normalizeTtl($ttl);
167
168 304
        if ($dependency !== null) {
169 5
            $dependency->evaluateDependency($this);
170 5
            $value = [$value, $dependency];
171
        }
172 304
        $key = $this->buildKey($key);
173
174 304
        return $this->handler->set($key, $value, $ttl);
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
180
     * expiration time will be replaced with the new ones, respectively.
181
     *
182
     * @param array $values the values to be cached, as key-value pairs.
183
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
184
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
185
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
186
     * @return bool True on success and false on failure.
187
     * @throws InvalidArgumentException
188
     */
189 61
    public function setMultiple($values, $ttl = null, Dependency $dependency = null): bool
190
    {
191 61
        $data = $this->prepareDataForSetOrAddMultiple($values, $dependency);
192 61
        $ttl = $this->normalizeTtl($ttl);
193 61
        return $this->handler->setMultiple($data, $ttl);
194
    }
195
196 4
    public function deleteMultiple($keys): bool
197
    {
198 4
        $actualKeys = [];
199 4
        foreach ($keys as $key) {
200 4
            $actualKeys[] = $this->buildKey($key);
201
        }
202 4
        return $this->handler->deleteMultiple($actualKeys);
203
    }
204
205
    /**
206
     * Stores multiple values in cache. Each value contains a value identified by a key.
207
     * If the cache already contains such a key, the existing value and expiration time will be preserved.
208
     *
209
     * @param array $values the values to be cached, as key-value pairs.
210
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
211
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
212
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
213
     * @return bool
214
     * @throws InvalidArgumentException
215
     */
216 4
    public function addMultiple(array $values, $ttl = null, Dependency $dependency = null): bool
217
    {
218 4
        $data = $this->prepareDataForSetOrAddMultiple($values, $dependency);
219 4
        $ttl = $this->normalizeTtl($ttl);
220 4
        $existingValues = $this->handler->getMultiple(array_keys($data));
221 4
        foreach ($existingValues as $key => $value) {
222 4
            if ($value !== null) {
223 4
                unset($data[$key]);
224
            }
225
        }
226 4
        return $this->handler->setMultiple($data, $ttl);
227
    }
228
229 65
    private function prepareDataForSetOrAddMultiple(iterable $values, ?Dependency $dependency): array
230
    {
231 65
        if ($dependency !== null) {
232
            $dependency->evaluateDependency($this);
233
        }
234
235 65
        $data = [];
236 65
        foreach ($values as $key => $value) {
237 65
            if ($dependency !== null) {
238
                $value = [$value, $dependency];
239
            }
240
241 65
            $key = $this->buildKey($key);
242 65
            $data[$key] = $value;
243
        }
244
245 65
        return $data;
246
    }
247
248
    /**
249
     * Stores a value identified by a key into cache if the cache does not contain this key.
250
     * Nothing will be done if the cache already contains the key.
251
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
252
     * a complex data structure consisting of factors representing the key.
253
     * @param mixed $value the value to be cached
254
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
255
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
256
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
257
     * @return bool whether the value is successfully stored into cache
258
     * @throws InvalidArgumentException
259
     */
260 4
    public function add($key, $value, $ttl = null, Dependency $dependency = null): bool
261
    {
262 4
        if ($dependency !== null) {
263
            $dependency->evaluateDependency($this);
264
            $value = [$value, $dependency];
265
        }
266
267 4
        $key = $this->buildKey($key);
268
269 4
        if ($this->handler->has($key)) {
270 4
            return false;
271
        }
272
273 4
        $ttl = $this->normalizeTtl($ttl);
274
275 4
        return $this->handler->set($key, $value, $ttl);
276
    }
277
278
    /**
279
     * Deletes a value with the specified key from cache.
280
     * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or
281
     * a complex data structure consisting of factors representing the key.
282
     * @return bool if no error happens during deletion
283
     * @throws InvalidArgumentException
284
     */
285 40
    public function delete($key): bool
286
    {
287 40
        $key = $this->buildKey($key);
288
289 40
        return $this->handler->delete($key);
290
    }
291
292
    /**
293
     * Deletes all values from cache.
294
     * Be careful of performing this operation if the cache is shared among multiple applications.
295
     * @return bool whether the flush operation was successful.
296
     */
297 343
    public function clear(): bool
298
    {
299 343
        return $this->handler->clear();
300
    }
301
302
    /**
303
     * Method combines both {@see CacheInterface::set()} and {@see CacheInterface::get()} methods to retrieve
304
     * value identified by a $key, or to store the result of $callable execution if there is no cache available
305
     * for the $key.
306
     *
307
     * Usage example:
308
     *
309
     * ```php
310
     * public function getTopProducts($count = 10) {
311
     *     $cache = $this->cache;
312
     *     return $cache->getOrSet(['top-n-products', 'n' => $count], function ($cache) use ($count) {
313
     *         return $this->getTopNProductsFromDatabase($count);
314
     *     }, 1000);
315
     * }
316
     * ```
317
     *
318
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
319
     * a complex data structure consisting of factors representing the key.
320
     * @param callable|\Closure $callable the callable or closure that will be used to generate a value to be cached.
321
     * In case $callable returns `false`, the value will not be cached.
322
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
323
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
324
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
325
     * @return mixed result of $callable execution
326
     * @throws SetCacheException
327
     * @throws InvalidArgumentException
328
     */
329 8
    public function getOrSet($key, callable $callable, $ttl = null, Dependency $dependency = null)
330
    {
331 8
        if (($value = $this->get($key)) !== null) {
332 4
            return $value;
333
        }
334
335 8
        $value = $callable($this);
336 8
        $ttl = $this->normalizeTtl($ttl);
337 8
        if (!$this->set($key, $value, $ttl, $dependency)) {
338
            throw new SetCacheException($key, $value, $this);
339
        }
340
341 8
        return $value;
342
    }
343
344 12
    public function enableKeyNormalization(): void
345
    {
346 12
        $this->keyNormalization = true;
347
    }
348
349 16
    public function disableKeyNormalization(): void
350
    {
351 16
        $this->keyNormalization = false;
352
    }
353
354
    /**
355
     * @param string $keyPrefix a string prefixed to every cache key so that it is unique globally in the whole cache storage.
356
     * It is recommended that you set a unique cache key prefix for each application if the same cache
357
     * storage is being used by different applications.
358
     */
359 24
    public function setKeyPrefix(string $keyPrefix): void
360
    {
361 24
        if ($keyPrefix !== '' && !ctype_alnum($keyPrefix)) {
362
            throw new \Yiisoft\Cache\Exception\InvalidArgumentException('Cache key prefix should be alphanumeric');
363
        }
364 24
        $this->keyPrefix = $keyPrefix;
365
    }
366
367
    /**
368
     * @return int|null
369
     */
370 8
    public function getDefaultTtl(): ?int
371
    {
372 8
        return $this->defaultTtl;
373
    }
374
375
    /**
376
     * @param int|DateInterval|null $defaultTtl
377
     */
378 8
    public function setDefaultTtl($defaultTtl): void
379
    {
380 8
        $this->defaultTtl = $this->normalizeTtl($defaultTtl);
381
    }
382
383
    /**
384
     * @noinspection PhpDocMissingThrowsInspection DateTime won't throw exception because constant string is passed as time
385
     *
386
     * Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
387
     * @param int|DateInterval|null $ttl raw TTL.
388
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
389
     */
390 344
    protected function normalizeTtl($ttl): ?int
391
    {
392 344
        if ($ttl === null) {
393 329
            return $this->defaultTtl;
394
        }
395
396 23
        if ($ttl instanceof DateInterval) {
397 8
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
398
        }
399
400 15
        return $ttl;
401
    }
402
403
    /**
404
     * Converts iterable to array
405
     * @param iterable $iterable
406
     * @return array
407
     */
408 49
    private function iterableToArray(iterable $iterable): array
409
    {
410 49
        return $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
411
    }
412
}
413