Passed
Pull Request — master (#8)
by Moiseenko
05:45
created

RedisCache   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 14
Bugs 0 Features 0
Metric Value
wmc 49
eloc 87
c 14
b 0
f 0
dl 0
loc 262
ccs 100
cts 100
cp 1
rs 8.48

16 Methods

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