Completed
Pull Request — master (#16)
by Alexander
01:24
created

Cache::getOrSet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 12
rs 10
ccs 6
cts 7
cp 0.8571
cc 3
nc 3
nop 4
crap 3.0261
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 [[handler]], which should be configured to be [[\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 data item can be stored in the cache by calling [[set()]] and be retrieved back
32
 * later (in the same or different request) by [[get()]]. In both operations,
33
 * a key identifying the data item is required. An expiration time and/or a [[Dependency|dependency]]
34
 * can also be specified when calling [[set()]]. If the data item expires or the dependency
35
 * changes at the time of calling [[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 [[\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 value of this item. If not set, default value is used.
182
     * @param Dependency $dependency dependency of the cached item. If the dependency changes,
183
     * the corresponding value in the cache will be invalidated when it is fetched via [[get()]].
184
     * This parameter is ignored if [[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 items in cache. Each item 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 $items the items to be cached, as key-value pairs.
204
     * @param null|int|\DateInterval $ttl the TTL value of this item. If not set, default value is used.
205
     * @param Dependency $dependency dependency of the cached items. If the dependency changes,
206
     * the corresponding values in the cache will be invalidated when it is fetched via [[get()]].
207
     * This parameter is ignored if [[serializer]] is false.
208
     * @return bool True on success and false on failure.
209
     * @throws InvalidArgumentException
210
     */
211 13
    public function setMultiple($items, $ttl = null, Dependency $dependency = null): bool
212
    {
213 13
        if ($dependency !== null) {
214
            $dependency->evaluateDependency($this);
215
        }
216
217 13
        $data = [];
218 13
        foreach ($items as $key => $value) {
219 13
            if ($dependency !== null) {
220
                $value = [$value, $dependency];
221
            }
222 13
            $key = $this->buildKey($key);
223 13
            $data[$key] = $value;
224
        }
225
226 13
        return $this->handler->setMultiple($data, $ttl);
227
    }
228
229
    public function deleteMultiple($keys): bool
230
    {
231
        $actualKeys = [];
232
        foreach ($keys as $key) {
233
            $actualKeys[] = $this->buildKey($key);
234
        }
235
        return $this->handler->deleteMultiple($actualKeys);
236
    }
237
238
    /**
239
     * Stores multiple items in cache. Each item contains a value identified by a key.
240
     * If the cache already contains such a key, the existing value and expiration time will be preserved.
241
     *
242
     * @param array $values the items to be cached, as key-value pairs.
243
     * @param null|int|\DateInterval $ttl the TTL value of this item. If not set, default value is used.
244
     * @param Dependency $dependency dependency of the cached items. If the dependency changes,
245
     * the corresponding values in the cache will be invalidated when it is fetched via [[get()]].
246
     * This parameter is ignored if [[serializer]] is false.
247
     * @return bool
248
     * @throws InvalidArgumentException
249
     */
250 4
    public function addMultiple(array $values, $ttl = 0, Dependency $dependency = null): bool
251
    {
252 4
        if ($dependency !== null) {
253
            $dependency->evaluateDependency($this);
254
        }
255
256 4
        $data = [];
257 4
        foreach ($values as $key => $value) {
258 4
            if ($dependency !== null) {
259
                $value = [$value, $dependency];
260
            }
261
262 4
            $key = $this->buildKey($key);
263 4
            $data[$key] = $value;
264
        }
265
266 4
        $existingValues = $this->handler->getMultiple(array_keys($data));
267 4
        foreach ($existingValues as $key => $value) {
268 4
            if ($value !== null) {
269 4
                unset($data[$key]);
270
            }
271
        }
272 4
        return $this->handler->setMultiple($data, $ttl);
273
    }
274
275
    /**
276
     * Stores a value identified by a key into cache if the cache does not contain this key.
277
     * Nothing will be done if the cache already contains the key.
278
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
279
     * a complex data structure consisting of factors representing the key.
280
     * @param mixed $value the value to be cached
281
     * @param null|int|\DateInterval $ttl the TTL value of this item. If not set, default value is used.
282
     * @param Dependency $dependency dependency of the cached item. If the dependency changes,
283
     * the corresponding value in the cache will be invalidated when it is fetched via [[get()]].
284
     * This parameter is ignored if [[serializer]] is false.
285
     * @return bool whether the value is successfully stored into cache
286
     * @throws InvalidArgumentException
287
     */
288 6
    public function add($key, $value, $ttl = null, Dependency $dependency = null): bool
289
    {
290 6
        if ($dependency !== null) {
291
            $dependency->evaluateDependency($this);
292
            $value = [$value, $dependency];
293
        }
294
295 6
        $key = $this->buildKey($key);
296
297 6
        if ($this->handler->has($key)) {
298 4
            return false;
299
        }
300
301 6
        return $this->handler->set($key, $value, $ttl);
302
    }
303
304
    /**
305
     * Deletes a value with the specified key from cache.
306
     * @param mixed $key a key identifying the value to be deleted from cache. This can be a simple string or
307
     * a complex data structure consisting of factors representing the key.
308
     * @return bool if no error happens during deletion
309
     * @throws InvalidArgumentException
310
     */
311 4
    public function delete($key): bool
312
    {
313 4
        $key = $this->buildKey($key);
314
315 4
        return $this->handler->delete($key);
316
    }
317
318
    /**
319
     * Deletes all values from cache.
320
     * Be careful of performing this operation if the cache is shared among multiple applications.
321
     * @return bool whether the flush operation was successful.
322
     */
323 44
    public function clear(): bool
324
    {
325 44
        return $this->handler->clear();
326
    }
327
328
    /**
329
     * Method combines both [[set()]] and [[get()]] methods to retrieve value identified by a $key,
330
     * or to store the result of $callable execution if there is no cache available for the $key.
331
     *
332
     * Usage example:
333
     *
334
     * ```php
335
     * public function getTopProducts($count = 10) {
336
     *     $cache = $this->cache;
337
     *     return $cache->getOrSet(['top-n-products', 'n' => $count], function ($cache) use ($count) {
338
     *         return Products::find()->mostPopular()->limit(10)->all();
339
     *     }, 1000);
340
     * }
341
     * ```
342
     *
343
     * @param mixed $key a key identifying the value to be cached. This can be a simple string or
344
     * a complex data structure consisting of factors representing the key.
345
     * @param callable|\Closure $callable the callable or closure that will be used to generate a value to be cached.
346
     * In case $callable returns `false`, the value will not be cached.
347
     * @param null|int|\DateInterval $ttl the TTL value of this item. If not set, default value is used.
348
     * @param Dependency $dependency dependency of the cached item. If the dependency changes,
349
     * the corresponding value in the cache will be invalidated when it is fetched via [[get()]].
350
     * This parameter is ignored if [[serializer]] is `false`.
351
     * @return mixed result of $callable execution
352
     * @throws SetCacheException
353
     * @throws InvalidArgumentException
354
     */
355 8
    public function getOrSet($key, callable $callable, $ttl = null, Dependency $dependency = null)
356
    {
357 8
        if (($value = $this->get($key)) !== null) {
358 4
            return $value;
359
        }
360
361 8
        $value = $callable($this);
362 8
        if (!$this->set($key, $value, $ttl, $dependency)) {
363
            throw new SetCacheException($key, $value, $this);
364
        }
365
366 8
        return $value;
367
    }
368
}
369