Passed
Pull Request — master (#1)
by Evgeniy
02:03
created

RedisCache::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Cache\Redis;
6
7
use DateInterval;
8
use DateTime;
9
use Predis\ClientInterface;
10
use Predis\Response\Status;
11
use Psr\SimpleCache\CacheInterface;
12
use Traversable;
13
14
use function array_fill_keys;
15
use function array_keys;
16
use function array_map;
17
use function count;
18
use function gettype;
19
use function is_iterable;
20
use function is_string;
21
use function iterator_to_array;
22
use function serialize;
23
use function strpbrk;
24
use function unserialize;
25
26
/**
27
 * RedisCache stores cache data in a Redis.
28
 *
29
 * Please refer to {@see CacheInterface} for common cache operations that are supported by RedisCache.
30
 */
31
final class RedisCache implements CacheInterface
32
{
33
    /**
34
     * @var ClientInterface $client Predis client instance to use.
35
     */
36
    private ClientInterface $client;
37
38
    /**
39
     * @param ClientInterface $client Predis client instance to use.
40
     */
41 166
    public function __construct(ClientInterface $client)
42
    {
43 166
        $this->client = $client;
44 166
    }
45
46 73
    public function get($key, $default = null)
47
    {
48 73
        if (!$this->has($key)) {
49 25
            return $default;
50
        }
51
52 52
        $result = $this->client->get($key);
53 52
        return $result === null ? $default : unserialize($result);
54
    }
55
56 81
    public function set($key, $value, $ttl = null): bool
57
    {
58 81
        $ttl = $this->normalizeTtl($ttl);
59
60 81
        if ($this->isExpiredTtl($ttl)) {
61 1
            return $this->delete($key);
62
        }
63
64 80
        $this->validateKey($key);
65
66
        /** @var Status|null $result */
67 72
        $result = $this->isInfinityTtl($ttl)
68 72
            ? $this->client->set($key, serialize($value))
69
            : $this->client->set($key, serialize($value), 'EX', $ttl)
70
        ;
71
72 72
        return $result !== null;
73
    }
74
75 22
    public function delete($key): bool
76
    {
77 22
        return !$this->has($key) || $this->client->del($key) === 1;
78
    }
79
80 166
    public function clear(): bool
81
    {
82 166
        return $this->client->flushdb() !== null;
83
    }
84
85 24
    public function getMultiple($keys, $default = null): iterable
86
    {
87 24
        $keys = $this->iterableToArray($keys);
88 16
        $this->validateKeys($keys);
89
        /** @var string[] $keys */
90 7
        $values = array_fill_keys($keys, $default);
91
        /** @var null[]|string[] $valuesFromCache */
92 7
        $valuesFromCache = $this->client->mget($keys);
93
94 7
        $i = 0;
95
        /** @var mixed $default */
96 7
        foreach ($values as $key => $default) {
97
            /** @psalm-suppress MixedAssignment */
98 7
            $values[$key] = isset($valuesFromCache[$i]) ? unserialize($valuesFromCache[$i]) : $default;
99 7
            $i++;
100
        }
101
102 7
        return $values;
103
    }
104
105 21
    public function setMultiple($values, $ttl = null): bool
106
    {
107 21
        $values = $this->iterableToArray($values);
108 13
        $keys = array_map('\strval', array_keys($values));
109 13
        $this->validateKeys($keys);
110 12
        $ttl = $this->normalizeTtl($ttl);
111 12
        $serializeValues = [];
112
113 12
        if ($this->isExpiredTtl($ttl)) {
114
            return $this->deleteMultiple($keys);
115
        }
116
117
        /** @var mixed $value */
118 12
        foreach ($values as $key => $value) {
119 12
            $serializeValues[$key] = serialize($value);
120
        }
121
122 12
        if ($this->isInfinityTtl($ttl)) {
123 9
            $this->client->mset($serializeValues);
124 9
            return true;
125
        }
126
127 3
        $this->client->multi();
128 3
        $this->client->mset($serializeValues);
129
130 3
        foreach ($keys as $key) {
131 3
            $this->client->expire($key, $ttl);
132
        }
133
134 3
        $results = $this->client->exec();
135
136
        /** @var Status|null $result */
137 3
        foreach ((array) $results as $result) {
138 3
            if ($result === null) {
139 1
                return false;
140
            }
141
        }
142
143 2
        return true;
144
    }
145
146 18
    public function deleteMultiple($keys): bool
147
    {
148 18
        $keys = $this->iterableToArray($keys);
149
150
        /** @psalm-suppress MixedAssignment, MixedArgument */
151 10
        foreach ($keys as $index => $key) {
152 10
            if (!$this->has($key)) {
153 1
                unset($keys[$index]);
154
            }
155
        }
156
157 2
        return empty($keys) || $this->client->del($keys) === count($keys);
158
    }
159
160 99
    public function has($key): bool
161
    {
162 99
        $this->validateKey($key);
163 67
        $ttl = $this->client->ttl($key);
164
        /** "-1" - if the key exists but has no associated expire {@see https://redis.io/commands/ttl}. */
165 67
        return $ttl > 0 || $ttl === -1;
166
    }
167
168
    /**
169
     * Normalizes cache TTL handling `null` value, strings and {@see DateInterval} objects.
170
     *
171
     * @param DateInterval|int|string|null $ttl The raw TTL.
172
     *
173
     * @return int TTL value as UNIX timestamp.
174
     */
175 99
    private function normalizeTtl($ttl): ?int
176
    {
177 99
        if ($ttl === null) {
178 90
            return null;
179
        }
180
181 10
        if ($ttl instanceof DateInterval) {
182 2
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
183
        }
184
185 8
        return (int) $ttl;
186
    }
187
188
    /**
189
     * Converts iterable to array. If provided value is not iterable it throws an InvalidArgumentException.
190
     *
191
     * @param mixed $iterable
192
     *
193
     * @return array
194
     */
195 55
    private function iterableToArray($iterable): array
196
    {
197 55
        if (!is_iterable($iterable)) {
198 24
            throw new InvalidArgumentException('Iterable is expected, got ' . gettype($iterable));
199
        }
200
201
        /** @psalm-suppress RedundantCast */
202 31
        return $iterable instanceof Traversable ? iterator_to_array($iterable) : (array) $iterable;
203
    }
204
205
    /**
206
     * @param mixed $key
207
     */
208 133
    private function validateKey($key): void
209
    {
210 133
        if (!is_string($key) || $key === '' || strpbrk($key, '{}()/\@:')) {
211 48
            throw new InvalidArgumentException('Invalid key value.');
212
        }
213 85
    }
214
215
    /**
216
     * @param array $keys
217
     */
218 23
    private function validateKeys(array $keys): void
219
    {
220 23
        if (empty($keys)) {
221 2
            throw new InvalidArgumentException('Invalid key values.');
222
        }
223
224
        /** @var mixed $key */
225 21
        foreach ($keys as $key) {
226 21
            $this->validateKey($key);
227
        }
228 13
    }
229
230 92
    private function isExpiredTtl(?int $ttl): bool
231
    {
232 92
        return $ttl !== null && $ttl <= 0;
233
    }
234
235 84
    private function isInfinityTtl(?int $ttl): bool
236
    {
237 84
        return $ttl === null;
238
    }
239
}
240