Passed
Push — master ( ab102c...5f4e36 )
by Alexander
02:10
created

Cache::getValueOrDefaultIfDependencyChanged()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 5
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 5
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Cache;
6
7
use DateInterval;
8
use DateTime;
9
use Yiisoft\Cache\Dependency\Dependency;
10
use Yiisoft\Cache\Exception\InvalidArgumentException;
11
use Yiisoft\Cache\Exception\RemoveCacheException;
12
use Yiisoft\Cache\Exception\SetCacheException;
13
use Yiisoft\Cache\Metadata\CacheItem;
14
use Yiisoft\Cache\Metadata\CacheItems;
15
16
use function ctype_alnum;
17
use function gettype;
18
use function is_array;
19
use function is_int;
20
use function is_string;
21
use function json_encode;
22
use function json_last_error_msg;
23
use function mb_strlen;
24
use function md5;
25
26
/**
27
 * Cache provides support for the data caching, including cache key composition and dependencies, and uses
28
 * "Probably early expiration" for cache stampede prevention. The actual data caching is performed via
29
 * {@see Cache::$handler}, which should be configured to be {@see \Psr\SimpleCache\CacheInterface} instance.
30
 *
31
 * @see \Yiisoft\Cache\CacheInterface
32
 */
33
final class Cache implements CacheInterface
34
{
35
    /**
36
     * @var \Psr\SimpleCache\CacheInterface The actual cache handler.
37
     */
38
    private \Psr\SimpleCache\CacheInterface $handler;
39
40
    /**
41
     * @var CacheItems The items that store the metadata of each cache.
42
     */
43
    private CacheItems $items;
44
45
    /**
46
     * @var int|null The default TTL for a cache entry. null meaning infinity, negative or zero results in the
47
     * cache key deletion. This value is used by {@see getOrSet()}, if the duration is not explicitly given.
48
     */
49
    private ?int $defaultTtl;
50
51
    /**
52
     * @var string The string prefixed to every cache key so that it is unique globally in the whole cache storage.
53
     * It is recommended that you set a unique cache key prefix for each application if the same cache
54
     * storage is being used by different applications.
55
     */
56
    private string $keyPrefix;
57
58
    /**
59
     * @param \Psr\SimpleCache\CacheInterface $handler The actual cache handler.
60
     * @param DateInterval|int|null $defaultTtl The default TTL for a cache entry.
61
     * null meaning infinity, negative orzero results in the cache key deletion.
62
     * This value is used by {@see getOrSet()}, if the duration is not explicitly given.
63
     * @param string $keyPrefix The string prefixed to every cache key so that it is unique globally
64
     * in the whole cache storage. It is recommended that you set a unique cache key prefix for each
65
     * application if the same cache storage is being used by different applications.
66
     */
67 57
    public function __construct(\Psr\SimpleCache\CacheInterface $handler, $defaultTtl = null, string $keyPrefix = '')
68
    {
69 57
        $this->handler = $handler;
70 57
        $this->items = new CacheItems();
71 57
        $this->defaultTtl = $this->normalizeTtl($defaultTtl);
72 51
        $this->keyPrefix = $keyPrefix;
73 51
    }
74
75 48
    public function getOrSet($key, callable $callable, $ttl = null, Dependency $dependency = null, float $beta = 1.0)
76
    {
77 48
        $key = $this->buildKey($key);
78 47
        $value = $this->getValue($key, $beta);
79
80 47
        return $value ?? $this->setAndGet($key, $callable, $ttl, $dependency);
81
    }
82
83 15
    public function remove($key): void
84
    {
85 15
        $key = $this->buildKey($key);
86
87 14
        if (!$this->handler->delete($key)) {
88 2
            throw new RemoveCacheException($key);
89
        }
90
91 12
        $this->items->remove($key);
92 12
    }
93
94
    /**
95
     * Gets the cache value.
96
     *
97
     * @param string $key The unique key of this item in the cache.
98
     * @param float $beta The value for calculating the range that is used for "Probably early expiration" algorithm.
99
     *
100
     * @return mixed|null The cache value or `null` if the cache is outdated or a dependency has been changed.
101
     */
102 47
    private function getValue(string $key, float $beta)
103
    {
104 47
        if ($this->items->expired($key, $beta, $this->handler)) {
105 2
            return null;
106
        }
107
108 47
        $value = $this->handler->get($key);
109
110 47
        if (is_array($value) && isset($value[1]) && $value[1] instanceof CacheItem) {
111 5
            [$value, $item] = $value;
112
113 5
            if ($item->key() !== $key || $item->expired($beta, $this->handler)) {
114
                return null;
115
            }
116
117 5
            $this->items->set($item);
118
        }
119
120 47
        return $value;
121
    }
122
123
    /**
124
     * Sets the cache value and metadata, and returns the cache value.
125
     *
126
     * @param string $key The unique key of this item in the cache.
127
     * @param callable $callable The callable or closure that will be used to generate a value to be cached.
128
     * @param DateInterval|int|null $ttl The TTL of this value. If not set, default value is used.
129
     * @param Dependency|null $dependency The dependency of the cache value.
130
     *
131
     * @throws InvalidArgumentException Must be thrown if the `$key` or `$ttl` is not a legal value.
132
     * @throws SetCacheException Must be thrown if the data could not be set in the cache.
133
     *
134
     * @return mixed|null The cache value.
135
     */
136 47
    private function setAndGet(string $key, callable $callable, $ttl, ?Dependency $dependency)
137
    {
138 47
        $ttl = $this->normalizeTtl($ttl);
139 41
        $ttl ??= $this->defaultTtl;
140 41
        $value = $callable($this->handler);
141
142 41
        if ($dependency !== null) {
143 6
            $dependency->evaluateDependency($this->handler);
144
        }
145
146 41
        $item = new CacheItem($key, $ttl, $dependency);
147
148 41
        if (!$this->handler->set($key, [$value, $item], $ttl)) {
149 2
            throw new SetCacheException($key, $value, $item);
150
        }
151
152 39
        $this->items->set($item);
153 39
        return $value;
154
    }
155
156
    /**
157
     * Builds a normalized cache key from a given key by appending key prefix.
158
     *
159
     * @param mixed $key The key to be normalized.
160
     *
161
     * @return string The generated cache key.
162
     */
163 51
    private function buildKey($key): string
164
    {
165 51
        return $this->keyPrefix . $this->normalizeKey($key);
166
    }
167
168
    /**
169
     * Normalizes the cache key from a given key.
170
     *
171
     * If the given key is a string containing alphanumeric characters only and no more than 32 characters,
172
     * then the key will be returned back as it is, integers will be converted to strings. Otherwise,
173
     * a normalized key is generated by serializing the given key and applying MD5 hashing.
174
     *
175
     * @param mixed $key The key to be normalized.
176
     *
177
     * @throws InvalidArgumentException For invalid key.
178
     *
179
     * @return string The normalized cache key.
180
     */
181 51
    private function normalizeKey($key): string
182
    {
183 51
        if (is_string($key) || is_int($key)) {
184 33
            $key = (string) $key;
185 33
            return ctype_alnum($key) && mb_strlen($key, '8bit') <= 32 ? $key : md5($key);
186
        }
187
188 18
        if (($key = json_encode($key)) === false) {
189 2
            throw new InvalidArgumentException('Invalid key. ' . json_last_error_msg());
190
        }
191
192 16
        return md5($key);
193
    }
194
195
    /**
196
     * Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
197
     *
198
     * @param mixed $ttl raw TTL.
199
     *
200
     * @throws InvalidArgumentException For invalid TTL.
201
     *
202
     * @return int|null TTL value as UNIX timestamp or null meaning infinity.
203
     */
204 57
    private function normalizeTtl($ttl): ?int
205
    {
206 57
        if ($ttl === null) {
207 51
            return null;
208
        }
209
210 19
        if ($ttl instanceof DateInterval) {
211 2
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
212
        }
213
214 17
        if (is_int($ttl)) {
215 5
            return $ttl;
216
        }
217
218 12
        throw new InvalidArgumentException(sprintf(
219 12
            'Invalid TTL "%s" specified. It must be a \DateInterval instance, an integer, or null.',
220 12
            gettype($ttl),
221
        ));
222
    }
223
}
224