Passed
Pull Request — master (#74)
by Alexander
01:49
created

Cache::remove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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