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