Passed
Pull Request — master (#30)
by Alexander
02:06
created

Cache   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Test Coverage

Coverage 90.7%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 99
c 5
b 0
f 0
dl 0
loc 370
rs 6.4799
ccs 78
cts 86
cp 0.907
wmc 54

23 Methods

Rating   Name   Duplication   Size   Complexity  
A enableKeyNormalization() 0 3 1
A disableKeyNormalization() 0 3 1
A serialize() 0 7 2
A deleteMultiple() 0 7 2
A __construct() 0 4 1
B getMultiple() 0 26 8
A addMultiple() 0 10 3
A get() 0 13 5
A __call() 0 3 1
A initSerializer() 0 3 1
A getOrSet() 0 12 3
A has() 0 4 1
A delete() 0 5 1
A setKeyPrefix() 0 6 3
A prepareDataForSetOrAddMultiple() 0 18 4
A unserialize() 0 7 2
A add() 0 16 3
A prepareReturnValue() 0 3 2
A clear() 0 3 1
A setMultiple() 0 4 1
A normalizeKey() 0 11 5
A set() 0 11 2
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 Psr\SimpleCache\InvalidArgumentException;
6
use Yiisoft\Cache\Dependency\Dependency;
7
use Yiisoft\Cache\Exception\SetCacheException;
8
use Yiisoft\Cache\Serializer\PhpSerializer;
9
use Yiisoft\Cache\Serializer\SerializerInterface;
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 57
    /**
44
     * @var string a string prefixed to every cache key so that it is unique globally in the whole cache storage.
45 57
     * 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
    /**
51
     * @var SerializerInterface the serializer to be used for serializing and unserializing of the cached data.
52
     */
53
    private $serializer;
54
55
    private $keyNormalization = true;
56
57
    /**
58 57
     * @param \Psr\SimpleCache\CacheInterface cache handler.
59
     */
60 57
    public function __construct(\Psr\SimpleCache\CacheInterface $handler = null)
61 51
    {
62
        $this->handler = $handler;
63 6
        $this->initSerializer();
64
    }
65
66
    /**
67 51
     * Builds a normalized cache key from a given key.
68
     *
69 51
     * If the given key is a string containing alphanumeric characters only and no more than 32 characters,
70 51
     * 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 51
     *
73 34
     * @param mixed $key the key to be normalized
74
     * @return string the generated cache key
75
     */
76 45
    private function normalizeKey($key): string
77 4
    {
78 4
        if (!$this->keyNormalization) {
79
            $normalizedKey = $key;
80 4
        } elseif (\is_string($key)) {
81
            $normalizedKey = ctype_alnum($key) && mb_strlen($key, '8bit') <= 32 ? $key : md5($key);
82
        } else {
83 41
            $normalizedKey = $this->keyPrefix . md5(json_encode($key));
84
        }
85
86
        return $this->keyPrefix . $normalizedKey;
87 9
    }
88
89 9
90 9
    public function get($key, $default = null)
91
    {
92
        $key = $this->normalizeKey($key);
93
        $value = $this->handler->get($key, $default);
94
95
        if (\is_array($value) && isset($value[1]) && $value[1] instanceof Dependency) {
96
            if ($value[1]->isChanged($this)) {
97
                return $default;
98
            }
99
            $value = $value[0];
100
        }
101
102
        return $this->prepareReturnValue($value, $default);
103
    }
104
105 7
106
    public function has($key): bool
107 7
    {
108 7
        $key = $this->normalizeKey($key);
109 7
        return $this->handler->has($key);
110
    }
111 7
112 7
    /**
113 7
     * Retrieves multiple values from cache with the specified keys.
114 7
     * Some caches, such as memcached or apcu, allow retrieving multiple cached values at the same time,
115 7
     * which may improve the performance. In case a cache does not support this feature natively,
116 7
     * this method will try to simulate it.
117 7
     * @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 7
    public function getMultiple($keys, $default = null): iterable
125
    {
126
        // TODO refactor
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
                $value = $this->prepareReturnValue($value, $default);
145 43
                $results[$key] = $value;
146
            }
147 43
        }
148 4
149 4
        return $results;
150
    }
151 43
152 43
    /**
153
     * Stores a value identified by a key into cache.
154
     * If the cache already contains such a key, the existing value and
155
     * expiration time will be replaced with the new ones, respectively.
156
     *
157
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
158
     * a complex data structure consisting of factors representing the key.
159
     * @param mixed $value the value to be cached
160
     * @param null|int|\DateInterval $ttl the TTL of this value. If not set, default value is used.
161
     * @param Dependency $dependency dependency of the cached value. If the dependency changes,
162
     * the corresponding value in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
163
     * @return bool whether the value is successfully stored into cache
164
     * @throws InvalidArgumentException
165
     */
166
    public function set($key, $value, $ttl = null, Dependency $dependency = null): bool
167 13
    {
168
        $value = $this->serialize($value);
169 13
170 13
        if ($dependency !== null) {
171
            $dependency->evaluateDependency($this);
172
            $value = [$value, $dependency];
173 3
        }
174
        $key = $this->normalizeKey($key);
175 3
176 3
        return $this->handler->set($key, $value, $ttl);
177 3
    }
178
179 3
    /**
180
     * Stores multiple values in cache. Each value contains a value identified by a key.
181
     * If the cache already contains such a key, the existing value and
182
     * expiration time will be replaced with the new ones, respectively.
183
     *
184
     * @param array $values the values to be cached, as key-value pairs.
185
     * @param null|int|\DateInterval $ttl the TTL value of this value. If not set, default value is used.
186
     * @param Dependency $dependency dependency of the cached values. If the dependency changes,
187
     * the corresponding values in the cache will be invalidated when it is fetched via {@see CacheInterface::get()}.
188
     * @return bool True on success and false on failure.
189
     * @throws InvalidArgumentException
190
     */
191
    public function setMultiple($values, $ttl = null, Dependency $dependency = null): bool
192
    {
193 3
        $data = $this->prepareDataForSetOrAddMultiple($values, $dependency);
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
        $existingValues = $this->handler->getMultiple(array_keys($data));
221 16
        foreach ($existingValues as $key => $value) {
222
            if ($value !== null) {
223
                unset($data[$key]);
224
            }
225
        }
226
        return $this->handler->setMultiple($data, $ttl);
227
    }
228
229
    private function prepareDataForSetOrAddMultiple(array $values, ?Dependency $dependency): array
230
    {
231
        if ($dependency !== null) {
232
            $dependency->evaluateDependency($this);
233
        }
234
235
        $data = [];
236 5
        foreach ($values as $key => $value) {
237
            $value = $this->serialize($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
        $value = $this->serialize($value);
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
        if (!$this->set($key, $value, $ttl, $dependency)) {
338
            throw new SetCacheException($key, $value, $this);
339
        }
340
341
        return $value;
342
    }
343
344
    public function __call($name, $arguments)
345
    {
346
        return call_user_func_array([$this->handler, $name], $arguments);
347
    }
348
349
    public function enableKeyNormalization(): void
350
    {
351
        $this->keyNormalization = true;
352
    }
353
354
    public function disableKeyNormalization(): void
355
    {
356
        $this->keyNormalization = false;
357
    }
358
359
    /**
360
     * @param string $keyPrefix a string prefixed to every cache key so that it is unique globally in the whole cache storage.
361
     * It is recommended that you set a unique cache key prefix for each application if the same cache
362
     * storage is being used by different applications.
363
     */
364
    public function setKeyPrefix(string $keyPrefix): void
365
    {
366
        if ($keyPrefix != '' && !ctype_alnum($keyPrefix)) {
367
            throw new Exception\InvalidArgumentException('Cache key prefix should be alphanumeric');
368
        }
369
        $this->keyPrefix = $keyPrefix;
370
    }
371
372
    private function initSerializer()
373
    {
374
        $this->serializer = new PhpSerializer();
375
    }
376
377
    /**
378
     * @param SerializerInterface $serializer
379
     */
380
    public function setSerializer(?SerializerInterface $serializer): void
381
    {
382
        $this->serializer = $serializer;
383
    }
384
385
    private function unserialize($value)
386
    {
387
        if ($this->serializer === null) {
388
            return $value;
389
        }
390
391
        return $this->serializer->unserialize($value);
392
    }
393
394
    private function serialize($value)
395
    {
396
        if ($this->serializer === null) {
397
            return $value;
398
        }
399
400
        return $this->serializer->serialize($value);
401
    }
402
403
    private function prepareReturnValue($value, $default)
404
    {
405
        return $value === $default ? $value : $this->unserialize($value);
406
    }
407
}
408