Test Setup Failed
Pull Request — master (#8)
by Moiseenko
03:53
created

RedisCache   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 49
eloc 87
c 7
b 0
f 0
dl 0
loc 316
ccs 86
cts 86
cp 1
rs 8.48

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A get() 0 6 2
B setMultiple() 0 40 8
A deleteMultiple() 0 13 4
A has() 0 7 2
A validateKey() 0 4 3
A set() 0 16 3
A delete() 0 3 2
A isInfinityTtl() 0 3 1
A getMultiple() 0 29 6
A validateKeys() 0 8 3
A iterableToArray() 0 4 2
A normalizeTtl() 0 13 3
A isCluster() 0 13 4
A isExpiredTtl() 0 3 2
A clear() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like RedisCache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RedisCache, and based on these observations, apply Extract Interface, too.

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