Completed
Push — master ( e5ad74...d561b1 )
by Alexander
01:38
created

Cache::prepareDataForSetOrAddMultiple()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.128

Importance

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