Test Failed
Pull Request — master (#8)
by Moiseenko
09:22
created

RedisCache   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 43
eloc 78
c 7
b 0
f 0
dl 0
loc 302
ccs 86
cts 86
cp 1
rs 8.96

16 Methods

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