Passed
Pull Request — master (#8)
by Moiseenko
15:57 queued 10:10
created

RedisCache::isCluster()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 5
c 2
b 0
f 0
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 10
cc 4
nc 3
nop 0
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Cache\Redis;
6
7
use DateInterval;
8
use DateTime;
9
use Predis\Client;
10
use Predis\ClientInterface;
11
use Predis\Connection\ConnectionInterface;
12
use Predis\Connection\StreamConnection;
13
use Predis\Response\Status;
14
use Psr\SimpleCache\CacheInterface;
15
use Traversable;
16
17
use function array_fill_keys;
18
use function array_keys;
19
use function array_map;
20
use function count;
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 array<ConnectionInterface>|ConnectionInterface $connections Predis connection instances to use.
35
     */
36
    private ConnectionInterface|array $connections;
37
38
    /**
39
     * @param ClientInterface $client Predis client instance to use.
40
     */
41 227
    public function __construct(private ClientInterface $client)
42
    {
43 227
        $this->connections = $this->client->getConnection();
44
    }
45
46
    /**
47
     * Returns whether Predis cluster is used.
48
     *
49
     * @return bool Whether Predis cluster is used.
50
     */
51 227
    public function isCluster(): bool
52
    {
53
        /** @psalm-suppress MixedAssignment, PossibleRawObjectIteration */
54 227
        foreach ($this->connections as $connection) {
55
            /** @var StreamConnection $connection */
56 109
            $cluster = (new Client($connection->getParameters()))->info('Cluster');
57
            /** @psalm-suppress MixedArrayAccess */
58 109
            if (isset($cluster['Cluster']['cluster_enabled']) && 1 === (int)$cluster['Cluster']['cluster_enabled']) {
59 109
                return true;
60
            }
61
        }
62
63 118
        return false;
64
    }
65
66 134
    public function get(string $key, mixed $default = null): mixed
67
    {
68 134
        $this->validateKey($key);
69
        /** @var string|null $value */
70 130
        $value = $this->client->get($key);
71 130
        return $value === null ? $default : unserialize($value);
72
    }
73
74
    /**
75
     * @param string $key
76
     * @param mixed $value
77
     * @param DateInterval|int|null $ttl
78
     *
79
     * @throws InvalidArgumentException
80
     *
81
     * @return bool
82
     */
83 175
    public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
84
    {
85 175
        $ttl = $this->normalizeTtl($ttl);
86
87 175
        if ($this->isExpiredTtl($ttl)) {
88 1
            return $this->delete($key);
89
        }
90
91 174
        $this->validateKey($key);
92
93
        /** @var Status|null $result */
94 172
        $result = $this->isInfinityTtl($ttl)
95 148
            ? $this->client->set($key, serialize($value))
96 24
            : $this->client->set($key, serialize($value), 'EX', $ttl);
97
98 172
        return $result !== null;
99
    }
100
101
    /**
102
     * @param string $key
103
     *
104
     * @throws InvalidArgumentException
105
     *
106
     * @return bool
107
     */
108 31
    public function delete(string $key): bool
109
    {
110 31
        return !$this->has($key) || $this->client->del($key) === 1;
111
    }
112
113
    /**
114
     * @inheritDoc
115
     *
116
     * If a cluster is used, all nodes will be cleared.
117
     */
118 227
    public function clear(): bool
119
    {
120 227
        if ($this->isCluster()) {
121
            /** @psalm-suppress MixedAssignment, PossibleRawObjectIteration */
122 109
            foreach ($this->connections as $connection) {
123
                /** @var StreamConnection $connection */
124 109
                $client = new Client($connection->getParameters());
125 109
                $client->flushdb();
126
            }
127 109
            return true;
128
        }
129
130 118
        return $this->client->flushdb() !== null;
131
    }
132
133 18
    public function getMultiple(iterable $keys, mixed $default = null): iterable
134
    {
135
        /** @var string[] $keys */
136 18
        $keys = $this->iterableToArray($keys);
137 18
        $this->validateKeys($keys);
138 12
        $values = array_fill_keys($keys, $default);
139
140 12
        if ($this->isCluster()) {
141 5
            foreach ($keys as $key) {
142
                /** @var string|null $value */
143 5
                $value = $this->get($key);
144 5
                if (null !== $value) {
145
                    /** @psalm-suppress MixedAssignment */
146 4
                    $values[$key] = unserialize($value);
147
                }
148
            }
149
        } else {
150
            /** @var null[]|string[] $valuesFromCache */
151 7
            $valuesFromCache = $this->client->mget($keys);
152
153 7
            $i = 0;
154
            /** @psalm-suppress MixedAssignment */
155 7
            foreach ($values as $key => $value) {
156 7
                $values[$key] = isset($valuesFromCache[$i]) ? unserialize($valuesFromCache[$i]) : $value;
157 7
                $i++;
158
            }
159
        }
160
161 12
        return $values;
162
    }
163
164 18
    public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
165
    {
166 18
        $values = $this->iterableToArray($values);
167 18
        $keys = array_map('\strval', array_keys($values));
168 18
        $this->validateKeys($keys);
169 16
        $ttl = $this->normalizeTtl($ttl);
170 16
        $serializeValues = [];
171
172 16
        if ($this->isExpiredTtl($ttl)) {
173 1
            return $this->deleteMultiple($keys);
174
        }
175
176
        /** @var mixed $value */
177 15
        foreach ($values as $key => $value) {
178 15
            $serializeValues[$key] = serialize($value);
179
        }
180
181 15
        $results = [];
182 15
        if ($this->isCluster()) {
183 4
            foreach ($serializeValues as $key => $value) {
184 4
                $this->set((string)$key, $value, $this->isInfinityTtl($ttl) ? null : $ttl);
185
            }
186
        } else {
187 11
            if ($this->isInfinityTtl($ttl)) {
188 8
                $this->client->mset($serializeValues);
189 8
                return true;
190
            }
191
192 3
            $this->client->multi();
193 3
            $this->client->mset($serializeValues);
194
195 3
            foreach ($keys as $key) {
196 3
                $this->client->expire($key, (int)$ttl);
197
            }
198
199
            /** @var array|null $results */
200 3
            $results = $this->client->exec();
201
        }
202
203 7
        return !in_array(null, (array)$results, true);
204
    }
205
206 8
    public function deleteMultiple(iterable $keys): bool
207
    {
208 8
        $keys = $this->iterableToArray($keys);
209
210
        /** @psalm-suppress MixedAssignment, MixedArgument */
211 8
        foreach ($keys as $index => $key) {
212 8
            if (!$this->has($key)) {
213 3
                unset($keys[$index]);
214
            }
215
        }
216
217
        /** @psalm-suppress MixedArgumentTypeCoercion */
218 4
        return empty($keys) || $this->client->del($keys) === count($keys);
219
    }
220
221 64
    public function has(string $key): bool
222
    {
223 64
        $this->validateKey($key);
224
        /** @var int $ttl */
225 52
        $ttl = $this->client->ttl($key);
226
        /** "-1" - if the key exists but has no associated expire {@see https://redis.io/commands/ttl}. */
227 52
        return $ttl > 0 || $ttl === -1;
228
    }
229
230
    /**
231
     * Normalizes cache TTL handling `null` value, strings and {@see DateInterval} objects.
232
     *
233
     * @param DateInterval|int|string|null $ttl The raw TTL.
234
     *
235
     * @return int|null TTL value as UNIX timestamp.
236
     */
237 200
    private function normalizeTtl(null|int|string|DateInterval $ttl): ?int
238
    {
239 200
        if ($ttl === null) {
240 160
            return null;
241
        }
242
243 40
        if ($ttl instanceof DateInterval) {
244 4
            return (new DateTime('@0'))
245 4
                ->add($ttl)
246 4
                ->getTimestamp();
247
        }
248
249 36
        return (int) $ttl;
250
    }
251
252
    /**
253
     * Converts iterable to array.
254
     *
255
     * @return array
256
     */
257 30
    private function iterableToArray(iterable $iterable): array
258
    {
259
        /** @psalm-suppress RedundantCast */
260 30
        return $iterable instanceof Traversable ? iterator_to_array($iterable) : (array) $iterable;
261
    }
262
263
    /**
264
     * @param string $key
265
     *
266
     * @throws InvalidArgumentException
267
     */
268 208
    private function validateKey(string $key): void
269
    {
270 208
        if ($key === '' || strpbrk($key, '{}()/\@:')) {
271 22
            throw new InvalidArgumentException('Invalid key value.');
272
        }
273
    }
274
275
    /**
276
     * @param string[] $keys
277
     *
278
     * @throws InvalidArgumentException
279
     */
280 26
    private function validateKeys(array $keys): void
281
    {
282 26
        if ([] === $keys) {
283 4
            throw new InvalidArgumentException('Invalid key values.');
284
        }
285
286 22
        foreach ($keys as $key) {
287 22
            $this->validateKey($key);
288
        }
289
    }
290
291
    /**
292
     * @param int|null $ttl
293
     *
294
     * @return bool
295
     */
296 186
    private function isExpiredTtl(?int $ttl): bool
297
    {
298 186
        return $ttl !== null && $ttl <= 0;
299
    }
300
301
    /**
302
     * @param int|null $ttl
303
     *
304
     * @return bool
305
     */
306 183
    private function isInfinityTtl(?int $ttl): bool
307
    {
308 183
        return $ttl === null;
309
    }
310
}
311