Passed
Pull Request — master (#30)
by Alexander
01:23
created

Cache   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 419
Duplicated Lines 0 %

Test Coverage

Coverage 90.7%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 115
dl 0
loc 419
ccs 78
cts 86
cp 0.907
rs 3.6
c 5
b 0
f 0
wmc 60

26 Methods

Rating   Name   Duplication   Size   Complexity  
A disableKeyNormalization() 0 3 1
A enableKeyNormalization() 0 3 1
A setDefaultTtl() 0 3 1
A serialize() 0 7 2
A __construct() 0 4 1
A deleteMultiple() 0 7 2
A addMultiple() 0 11 3
B getMultiple() 0 26 8
A __call() 0 3 1
A get() 0 13 5
A normalizeTtl() 0 15 4
A has() 0 4 1
A getOrSet() 0 13 3
A initSerializer() 0 3 1
A setKeyPrefix() 0 6 3
A delete() 0 5 1
A prepareDataForSetOrAddMultiple() 0 18 4
A unserialize() 0 7 2
A prepareReturnValue() 0 3 2
A add() 0 17 3
A clear() 0 3 1
A setMultiple() 0 5 1
A normalizeKey() 0 11 5
A set() 0 12 2
A getDefaultTtl() 0 3 1
A setSerializer() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Cache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cache, and based on these observations, apply Extract Interface, too.

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