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

RedisCache::deleteMultiple()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

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