yiisoft /
cache-redis
| 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 | foreach ($values as $key => $value) { |
||
| 161 | 15 | $serializeValues[$key] = serialize($value); |
|
| 162 | 15 | } |
|
| 163 | |||
| 164 | $results = []; |
||
| 165 | 15 | if ($this->isCluster()) { |
|
| 166 | 15 | foreach ($serializeValues as $key => $value) { |
|
| 167 | 4 | $this->set((string)$key, $value, $this->isInfinityTtl($ttl) ? null : $ttl); |
|
| 168 | 4 | } |
|
| 169 | } else { |
||
| 170 | if ($this->isInfinityTtl($ttl)) { |
||
| 171 | 11 | $this->client->mset($serializeValues); |
|
| 172 | 8 | return true; |
|
| 173 | 8 | } |
|
| 174 | |||
| 175 | $this->client->multi(); |
||
| 176 | 3 | $this->client->mset($serializeValues); |
|
| 177 | 3 | ||
| 178 | foreach ($keys as $key) { |
||
| 179 | 3 | $this->client->expire($key, (int)$ttl); |
|
| 180 | 3 | } |
|
| 181 | |||
| 182 | /** @var array|null $results */ |
||
| 183 | $results = $this->client->exec(); |
||
| 184 | 3 | } |
|
| 185 | |||
| 186 | return !in_array(null, (array)$results, true); |
||
| 187 | 7 | } |
|
| 188 | |||
| 189 | public function deleteMultiple(iterable $keys): bool |
||
| 190 | 8 | { |
|
| 191 | $keys = $this->iterableToArray($keys); |
||
| 192 | 8 | ||
| 193 | /** @psalm-suppress MixedAssignment, MixedArgument */ |
||
| 194 | foreach ($keys as $index => $key) { |
||
| 195 | 8 | if (!$this->has($key)) { |
|
| 196 | 8 | unset($keys[$index]); |
|
| 197 | 3 | } |
|
| 198 | } |
||
| 199 | |||
| 200 | /** @psalm-suppress MixedArgumentTypeCoercion */ |
||
| 201 | return empty($keys) || $this->client->del($keys) === count($keys); |
||
| 202 | 4 | } |
|
| 203 | |||
| 204 | public function has(string $key): bool |
||
| 205 | 64 | { |
|
| 206 | $this->validateKey($key); |
||
| 207 | 64 | /** @var int $ttl */ |
|
| 208 | $ttl = $this->client->ttl($key); |
||
| 209 | 52 | /** "-1" - if the key exists but has no associated expire {@see https://redis.io/commands/ttl}. */ |
|
| 210 | return $ttl > 0 || $ttl === -1; |
||
| 211 | 52 | } |
|
| 212 | |||
| 213 | /** |
||
| 214 | * Normalizes cache TTL handling `null` value, strings and {@see DateInterval} objects. |
||
| 215 | * |
||
| 216 | * @param DateInterval|int|string|null $ttl The raw TTL. |
||
| 217 | * |
||
| 218 | * @return int|null TTL value as UNIX timestamp. |
||
| 219 | */ |
||
| 220 | private function normalizeTtl(null|int|string|DateInterval $ttl): ?int |
||
| 221 | 200 | { |
|
| 222 | if ($ttl === null) { |
||
| 223 | 200 | return null; |
|
| 224 | 160 | } |
|
| 225 | |||
| 226 | if ($ttl instanceof DateInterval) { |
||
| 227 | 40 | return (new DateTime('@0')) |
|
| 228 | 4 | ->add($ttl) |
|
| 229 | 4 | ->getTimestamp(); |
|
| 230 | 4 | } |
|
| 231 | |||
| 232 | return (int) $ttl; |
||
| 233 | 36 | } |
|
| 234 | |||
| 235 | /** |
||
| 236 | * Converts iterable to array. |
||
| 237 | * |
||
| 238 | * @psalm-template T |
||
| 239 | * @psalm-param iterable<T> $iterable |
||
| 240 | * @psalm-return array<array-key,T> |
||
| 241 | 30 | */ |
|
| 242 | private function iterableToArray(iterable $iterable): array |
||
| 243 | { |
||
| 244 | 30 | return $iterable instanceof Traversable ? iterator_to_array($iterable) : $iterable; |
|
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 245 | } |
||
| 246 | |||
| 247 | /** |
||
| 248 | * @throws InvalidArgumentException |
||
| 249 | */ |
||
| 250 | 208 | private function validateKey(string $key): void |
|
| 251 | { |
||
| 252 | 208 | if ($key === '' || strpbrk($key, '{}()/\@:')) { |
|
| 253 | 22 | throw new InvalidArgumentException('Invalid key value.'); |
|
| 254 | } |
||
| 255 | } |
||
| 256 | |||
| 257 | /** |
||
| 258 | * @param string[] $keys |
||
| 259 | * |
||
| 260 | * @throws InvalidArgumentException |
||
| 261 | */ |
||
| 262 | 26 | private function validateKeys(array $keys): void |
|
| 263 | { |
||
| 264 | 26 | if ([] === $keys) { |
|
| 265 | 4 | throw new InvalidArgumentException('Invalid key values.'); |
|
| 266 | } |
||
| 267 | |||
| 268 | 22 | foreach ($keys as $key) { |
|
| 269 | 22 | $this->validateKey($key); |
|
| 270 | } |
||
| 271 | } |
||
| 272 | |||
| 273 | /** |
||
| 274 | * @return bool |
||
| 275 | */ |
||
| 276 | 186 | private function isExpiredTtl(?int $ttl): bool |
|
| 277 | { |
||
| 278 | 186 | return $ttl !== null && $ttl <= 0; |
|
| 279 | } |
||
| 280 | |||
| 281 | /** |
||
| 282 | * @return bool |
||
| 283 | */ |
||
| 284 | 183 | private function isInfinityTtl(?int $ttl): bool |
|
| 285 | { |
||
| 286 | 183 | return $ttl === null; |
|
| 287 | } |
||
| 288 | } |
||
| 289 |