Completed
Pull Request — master (#30)
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 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 7
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 13
ccs 0
cts 0
cp 0
crap 30
rs 9.6111
1
<?php
2
3
namespace Yiisoft\Cache;
4
5
use DateInterval;
6
use DateTime;
7
use Exception;
8
use Psr\SimpleCache\InvalidArgumentException;
9
use Yiisoft\Cache\Dependency\Dependency;
10
use Yiisoft\Cache\Exception\SetCacheException;
11
12
/**
13
 * Cache provides support for the data caching, including cache key composition and dependencies.
14
 * The actual data caching is performed via {@see Cache::$handler}, which should be configured
15
 * to be {@see \Psr\SimpleCache\CacheInterface} instance.
16
 *
17
 * A value can be stored in the cache by calling {@see CacheInterface::set()} and be retrieved back
18
 * later (in the same or different request) by {@see CacheInterface::get()}. In both operations,
19
 * a key identifying the value is required. An expiration time and/or a {@see Dependency}
20
 * can also be specified when calling {@see CacheInterface::set()}. If the value expires or the dependency
21
 * changes at the time of calling {@see CacheInterface::get()}, the cache will return no data.
22
 *
23
 * A typical usage pattern of cache is like the following:
24
 *
25
 * ```php
26
 * $key = 'demo';
27
 * $data = $cache->get($key);
28
 * if ($data === null) {
29
 *     // ...generate $data here...
30
 *     $cache->set($key, $data, $ttl, $dependency);
31
 * }
32
 * ```
33
 *
34
 * For more details and usage information on Cache, see
35
 * [PSR-16 specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md).
36
 */
37
final class Cache implements CacheInterface
38
{
39
    /**
40
     * @var \Psr\SimpleCache\CacheInterface actual cache handler.
41
     */
42
    private $handler;
43 57
44
    /**
45 57
     * @var string a string prefixed to every cache key so that it is unique globally in the whole cache storage.
46
     * It is recommended that you set a unique cache key prefix for each application if the same cache
47
     * storage is being used by different applications.
48
     */
49
    private $keyPrefix = '';
50
51
    private $keyNormalization = true;
52
53
    /**
54
     * @var int|null default TTL for a cache entry. null meaning infinity, negative or zero results in cache key deletion.
55
     * This value is used by {@see set()} and {@see setMultiple()}, if the duration is not explicitly given.
56
     */
57
    private $defaultTtl;
58 57
59
    /**
60 57
     * @param \Psr\SimpleCache\CacheInterface cache handler.
61 51
     */
62
    public function __construct(\Psr\SimpleCache\CacheInterface $handler = null)
63 6
    {
64
        $this->handler = $handler;
65
    }
66
67 51
    /**
68
     * Builds a normalized cache key from a given key.
69 51
     *
70 51
     * If the given key is a string containing alphanumeric characters only and no more than 32 characters,
71
     * then the key will be returned back as it is. Otherwise, a normalized key is generated by serializing
72 51
     * the given key and applying MD5 hashing.
73 34
     *
74
     * @param mixed $key the key to be normalized
75
     * @return string the generated cache key
76 45
     */
77 4
    private function normalizeKey($key): string
78 4
    {
79
        if (!$this->keyNormalization) {
80 4
            $normalizedKey = $key;
81
        } elseif (\is_string($key)) {
82
            $normalizedKey = ctype_alnum($key) && mb_strlen($key, '8bit') <= 32 ? $key : md5($key);
83 41
        } else {
84
            $normalizedKey = $this->keyPrefix . md5(json_encode($key));
85
        }
86
87 9
        return $this->keyPrefix . $normalizedKey;
88
    }
89 9
90 9
91
    public function get($key, $default = null)
92
    {
93
        $key = $this->normalizeKey($key);
94
        $value = $this->handler->get($key, $default);
95
96
        if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
97
            if ($value[1]->isChanged($this)) {
98
                return $default;
99
            }
100
            $value = $value[0];
101
        }
102
103
        return $value;
104
    }
105 7
106
107 7
    public function has($key): bool
108 7
    {
109 7
        $key = $this->normalizeKey($key);
110
        return $this->handler->has($key);
111 7
    }
112 7
113 7
    /**
114 7
     * Retrieves multiple values from cache with the specified keys.
115 7
     * Some caches, such as memcached or apcu, allow retrieving multiple cached values at the same time,
116 7
     * which may improve the performance. In case a cache does not support this feature natively,
117 7
     * this method will try to simulate it.
118
     * @param string[] $keys list of string keys identifying the cached values
119
     * @param mixed $default Default value to return for keys that do not exist.
120
     * @return iterable list of cached values corresponding to the specified keys. The array
121
     * is returned in terms of (key, value) pairs.
122
     * If a value is not cached or expired, the corresponding array value will be false.
123
     * @throws InvalidArgumentException
124 7
     */
125
    public function getMultiple($keys, $default = null): iterable
126
    {
127
        $keyMap = [];
128 7
        foreach ($keys as $key) {
129
            $keyMap[$key] = $this->normalizeKey($key);
130
        }
131
        $values = $this->handler->getMultiple(array_values($keyMap), $default);
132
        $results = [];
133
        foreach ($keyMap as $key => $newKey) {
134
            $results[$key] = $default;
135
            if (array_key_exists($newKey, (array)$values)) {
136
                $value = $values[$newKey];
137
                if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
138
                    if ($value[1]->isChanged($this)) {
139
                        continue;
140
                    }
141
142
                    $value = $value[0];
143
                }
144
                $results[$key] = $value;
145 43
            }
146
        }
147 43
148 4
        return $results;
149 4
    }
150
151 43
    /**
152 43
     * Stores a value identified by a key into cache.
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 mixed $key a key identifying the value to be cached. This can be a simple string or
157
     * a complex data structure consisting of factors representing the key.
158
     * @param mixed $value the value to be cached
159
     * @param null|int|\DateInterval $ttl the TTL of this value. If not set, default value is used.
160
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
161
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
162
     * @return bool whether the value is successfully stored into cache
163
     * @throws InvalidArgumentException
164
     */
165
    public function set($key, $value, $ttl = null, Dependency $dependency = null): bool
166
    {
167 13
        $ttl = $this->normalizeTtl($ttl);
168
169 13
        if ($dependency !== null) {
170 13
            $dependency->evaluateDependency($this);
171
            $value = [$value, $dependency];
172
        }
173 3
        $key = $this->normalizeKey($key);
174
175 3
        return $this->handler->set($key, $value, $ttl);
176 3
    }
177 3
178
    /**
179 3
     * Stores multiple values in cache. Each value contains a value identified by a key.
180
     * If the cache already contains such a key, the existing value and
181
     * expiration time will be replaced with the new ones, respectively.
182
     *
183
     * @param array $values the values to be cached, as key-value pairs.
184
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
185
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
186
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
187
     * @return bool True on success and false on failure.
188
     * @throws InvalidArgumentException
189
     */
190
    public function setMultiple($values, $ttl = null, Dependency $dependency = null): bool
191
    {
192
        $data = $this->prepareDataForSetOrAddMultiple($values, $dependency);
193 3
        $ttl = $this->normalizeTtl($ttl);
194
        return $this->handler->setMultiple($data, $ttl);
195 3
    }
196 3
197 3
    public function deleteMultiple($keys): bool
198 3
    {
199 3
        $actualKeys = [];
200
        foreach ($keys as $key) {
201
            $actualKeys[] = $this->normalizeKey($key);
202 3
        }
203
        return $this->handler->deleteMultiple($actualKeys);
204
    }
205 16
206
    /**
207 16
     * Stores multiple values in cache. Each value contains a value identified by a key.
208
     * If the cache already contains such a key, the existing value and expiration time will be preserved.
209
     *
210
     * @param array $values the values to be cached, as key-value pairs.
211 16
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
212 16
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
213 16
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
214
     * @return bool
215
     * @throws InvalidArgumentException
216
     */
217 16
    public function addMultiple(array $values, $ttl = null, Dependency $dependency = null): bool
218 16
    {
219
        $data = $this->prepareDataForSetOrAddMultiple($values, $dependency);
220
        $ttl = $this->normalizeTtl($ttl);
221 16
        $existingValues = $this->handler->getMultiple(array_keys($data));
222
        foreach ($existingValues as $key => $value) {
223
            if ($value !== null) {
224
                unset($data[$key]);
225
            }
226
        }
227
        return $this->handler->setMultiple($data, $ttl);
228
    }
229
230
    private function prepareDataForSetOrAddMultiple(iterable $values, ?Dependency $dependency): array
231
    {
232
        if ($dependency !== null) {
233
            $dependency->evaluateDependency($this);
234
        }
235
236 5
        $data = [];
237
        foreach ($values as $key => $value) {
238 5
            if ($dependency !== null) {
239
                $value = [$value, $dependency];
240
            }
241
242
            $key = $this->normalizeKey($key);
243 5
            $data[$key] = $value;
244
        }
245 5
246 3
        return $data;
247
    }
248
249 5
    /**
250
     * Stores a value identified by a key into cache if the cache does not contain this key.
251
     * Nothing will be done if the cache already contains the key.
252
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
253
     * a complex data structure consisting of factors representing the key.
254
     * @param mixed $value the value to be cached
255
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
256
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
257
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
258
     * @return bool whether the value is successfully stored into cache
259 3
     * @throws InvalidArgumentException
260
     */
261 3
    public function add($key, $value, $ttl = null, Dependency $dependency = null): bool
262
    {
263 3
        if ($dependency !== null) {
264
            $dependency->evaluateDependency($this);
265
            $value = [$value, $dependency];
266
        }
267
268
        $key = $this->normalizeKey($key);
269
270
        if ($this->handler->has($key)) {
271 56
            return false;
272
        }
273 56
274
        $ttl = $this->normalizeTtl($ttl);
275
276
        return $this->handler->set($key, $value, $ttl);
277
    }
278
279
    /**
280
     * Deletes a value with the specified key from cache.
281
     * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or
282
     * a complex data structure consisting of factors representing the key.
283
     * @return bool if no error happens during deletion
284
     * @throws InvalidArgumentException
285
     */
286
    public function delete($key): bool
287
    {
288
        $key = $this->normalizeKey($key);
289
290
        return $this->handler->delete($key);
291
    }
292
293
    /**
294
     * Deletes all values from cache.
295
     * Be careful of performing this operation if the cache is shared among multiple applications.
296
     * @return bool whether the flush operation was successful.
297
     */
298
    public function clear(): bool
299
    {
300
        return $this->handler->clear();
301
    }
302
303 6
    /**
304
     * Method combines both {@see CacheInterface::set()} and {@see CacheInterface::get()} methods to retrieve
305 6
     * value identified by a $key, or to store the result of $callable execution if there is no cache available
306 3
     * for the $key.
307
     *
308
     * Usage example:
309 6
     *
310 6
     * ```php
311
     * public function getTopProducts($count = 10) {
312
     *     $cache = $this->cache;
313
     *     return $cache->getOrSet(['top-n-products', 'n' => $count], function ($cache) use ($count) {
314 6
     *         return $this->getTopNProductsFromDatabase($count);
315
     *     }, 1000);
316
     * }
317
     * ```
318
     *
319
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
320
     * a complex data structure consisting of factors representing the key.
321
     * @param callable|\Closure $callable the callable or closure that will be used to generate a value to be cached.
322
     * In case $callable returns `false`, the value will not be cached.
323
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
324
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
325
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
326
     * @return mixed result of $callable execution
327
     * @throws SetCacheException
328
     * @throws InvalidArgumentException
329
     */
330
    public function getOrSet($key, callable $callable, $ttl = null, Dependency $dependency = null)
331
    {
332
        if (($value = $this->get($key)) !== null) {
333
            return $value;
334
        }
335
336
        $value = $callable($this);
337
        $ttl = $this->normalizeTtl($ttl);
338
        if (!$this->set($key, $value, $ttl, $dependency)) {
339
            throw new SetCacheException($key, $value, $this);
340
        }
341
342
        return $value;
343
    }
344
345
    public function __call($name, $arguments)
346
    {
347
        return call_user_func_array([$this->handler, $name], $arguments);
348
    }
349
350
    public function enableKeyNormalization(): void
351
    {
352
        $this->keyNormalization = true;
353
    }
354
355
    public function disableKeyNormalization(): void
356
    {
357
        $this->keyNormalization = false;
358
    }
359
360
    /**
361
     * @param string $keyPrefix a string prefixed to every cache key so that it is unique globally in the whole cache storage.
362
     * It is recommended that you set a unique cache key prefix for each application if the same cache
363
     * storage is being used by different applications.
364
     */
365
    public function setKeyPrefix(string $keyPrefix): void
366
    {
367
        if ($keyPrefix != '' && !ctype_alnum($keyPrefix)) {
368
            throw new Exception\InvalidArgumentException('Cache key prefix should be alphanumeric');
0 ignored issues
show
Bug introduced by
The type Exception\InvalidArgumentException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
369
        }
370
        $this->keyPrefix = $keyPrefix;
371
    }
372
373
    /**
374
     * @return int|null
375
     */
376
    public function getDefaultTtl(): ?int
377
    {
378
        return $this->defaultTtl;
379
    }
380
381
    /**
382
     * @param int|DateInterval|null $defaultTtl
383
     */
384
    public function setDefaultTtl($defaultTtl): void
385
    {
386
        $this->defaultTtl = $this->normalizeTtl($defaultTtl);
387
    }
388
389
    /**
390
     * Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
391
     * @param int|DateInterval|null $ttl raw TTL.
392
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
393
     */
394
    protected function normalizeTtl($ttl): ?int
395
    {
396
        if ($ttl === null) {
397
            return $this->defaultTtl;
398
        }
399
400
        if ($ttl instanceof DateInterval) {
401
            try {
402
                return (new DateTime('@0'))->add($ttl)->getTimestamp();
403
            } catch (Exception $e) {
404
                return $this->defaultTtl;
405
            }
406
        }
407
408
        return $ttl;
409
    }
410
}
411