blitz-php /
cache
| 1 | <?php |
||||||
| 2 | |||||||
| 3 | /** |
||||||
| 4 | * This file is part of Blitz PHP framework. |
||||||
| 5 | * |
||||||
| 6 | * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]> |
||||||
| 7 | * |
||||||
| 8 | * For the full copyright and license information, please view |
||||||
| 9 | * the LICENSE file that was distributed with this source code. |
||||||
| 10 | */ |
||||||
| 11 | |||||||
| 12 | namespace BlitzPHP\Cache\Handlers; |
||||||
| 13 | |||||||
| 14 | use DateInterval; |
||||||
| 15 | use Redis; |
||||||
| 16 | use RedisException; |
||||||
| 17 | use RuntimeException; |
||||||
| 18 | |||||||
| 19 | /** |
||||||
| 20 | * Moteur de stockage Redis pour le cache. |
||||||
| 21 | */ |
||||||
| 22 | class RedisHandler extends BaseHandler |
||||||
| 23 | { |
||||||
| 24 | /** |
||||||
| 25 | * Wrapper Redis. |
||||||
| 26 | * |
||||||
| 27 | * @var Redis |
||||||
| 28 | */ |
||||||
| 29 | protected $_Redis; |
||||||
| 30 | |||||||
| 31 | /** |
||||||
| 32 | * La configuration par défaut utilisée sauf si elle est remplacée par la configuration d'exécution |
||||||
| 33 | * |
||||||
| 34 | * - Numéro de base de données `database` à utiliser pour la connexion. |
||||||
| 35 | * - `duration` Spécifiez combien de temps durent les éléments de cette configuration de cache. |
||||||
| 36 | * - `groups` Liste des groupes ou 'tags' associés à chaque clé stockée dans cette configuration. |
||||||
| 37 | * pratique pour supprimer un groupe complet du cache. |
||||||
| 38 | * - `password` Mot de passe du serveur Redis. |
||||||
| 39 | * - `persistent` Connectez-vous au serveur Redis avec une connexion persistante |
||||||
| 40 | * - `port` numéro de port vers le serveur Redis. |
||||||
| 41 | * - `prefix` Préfixe ajouté à toutes les entrées. Bon pour quand vous avez besoin de partager un keyspace |
||||||
| 42 | * avec une autre configuration de cache ou une autre application. |
||||||
| 43 | * - URL ou IP `server` vers l'hôte du serveur Redis. |
||||||
| 44 | * - Délai d'expiration de `timeout` en secondes (flottant). |
||||||
| 45 | * - `unix_socket` Chemin vers le fichier socket unix (par défaut : false) |
||||||
| 46 | */ |
||||||
| 47 | protected array $_defaultConfig = [ |
||||||
| 48 | 'database' => 0, |
||||||
| 49 | 'duration' => 3600, |
||||||
| 50 | 'groups' => [], |
||||||
| 51 | 'password' => false, |
||||||
| 52 | 'persistent' => true, |
||||||
| 53 | 'port' => 6379, |
||||||
| 54 | 'prefix' => 'blitz_', |
||||||
| 55 | 'host' => null, |
||||||
| 56 | 'server' => '127.0.0.1', |
||||||
| 57 | 'timeout' => 0, |
||||||
| 58 | 'unix_socket' => false, |
||||||
| 59 | 'scanCount' => 10, |
||||||
| 60 | 'clearUsesFlushDb' => false, |
||||||
| 61 | ]; |
||||||
| 62 | |||||||
| 63 | /** |
||||||
| 64 | * {@inheritDoc} |
||||||
| 65 | */ |
||||||
| 66 | public function init(array $config = []): bool |
||||||
| 67 | { |
||||||
| 68 | if (! extension_loaded('redis')) { |
||||||
| 69 | throw new RuntimeException('L\'extension `redis` doit être activée pour utiliser RedisHandler.'); |
||||||
| 70 | } |
||||||
| 71 | |||||||
| 72 | if (! empty($config['host'])) { |
||||||
| 73 | $config['server'] = $config['host']; |
||||||
| 74 | } |
||||||
| 75 | |||||||
| 76 | parent::init($config); |
||||||
| 77 | |||||||
| 78 | return $this->_connect(); |
||||||
| 79 | } |
||||||
| 80 | |||||||
| 81 | /** |
||||||
| 82 | * Connection au serveur Redis |
||||||
| 83 | * |
||||||
| 84 | * @return bool Vrai si le serveur Redis était connecté |
||||||
| 85 | */ |
||||||
| 86 | protected function _connect(): bool |
||||||
| 87 | { |
||||||
| 88 | $tls = $this->_config['tls'] === true ? 'tls://' : ''; |
||||||
| 89 | |||||||
| 90 | $map = [ |
||||||
| 91 | 'ssl_ca' => 'cafile', |
||||||
| 92 | 'ssl_key' => 'local_pk', |
||||||
| 93 | 'ssl_cert' => 'local_cert', |
||||||
| 94 | ]; |
||||||
| 95 | |||||||
| 96 | $ssl = []; |
||||||
| 97 | foreach ($map as $key => $context) { |
||||||
| 98 | if (!empty($this->_config[$key])) { |
||||||
| 99 | $ssl[$context] = $this->_config[$key]; |
||||||
| 100 | } |
||||||
| 101 | } |
||||||
| 102 | |||||||
| 103 | try { |
||||||
| 104 | $this->_Redis = $this->_createRedisInstance(); |
||||||
| 105 | if (! empty($this->_config['unix_socket'])) { |
||||||
| 106 | $return = $this->_Redis->connect($this->_config['unix_socket']); |
||||||
| 107 | } elseif (empty($this->_config['persistent'])) { |
||||||
| 108 | $return = $this->_connectTransient($tls . $this->_config['server'], $ssl); |
||||||
| 109 | } else { |
||||||
| 110 | $return = $this->_connectPersistent($tls . $this->_config['server'], $ssl); |
||||||
| 111 | } |
||||||
| 112 | } catch (RedisException $e) { |
||||||
| 113 | if (function_exists('logger')) { |
||||||
| 114 | $logger = logger(); |
||||||
| 115 | if (is_object($logger) && method_exists($logger, 'error')) { |
||||||
| 116 | $logger->error('RedisEngine n\'a pas pu se connecter. Erreur: ' . $e->getMessage()); |
||||||
| 117 | } |
||||||
| 118 | } |
||||||
| 119 | |||||||
| 120 | return false; |
||||||
| 121 | } |
||||||
| 122 | if ($return && $this->_config['password']) { |
||||||
| 123 | $return = $this->_Redis->auth($this->_config['password']); |
||||||
| 124 | } |
||||||
| 125 | if ($return) { |
||||||
| 126 | return $this->_Redis->select((int) $this->_config['database']); |
||||||
| 127 | } |
||||||
| 128 | |||||||
| 129 | return $return; |
||||||
| 130 | } |
||||||
| 131 | |||||||
| 132 | /** |
||||||
| 133 | * Se connecte au serveur Redis en utilisant une nouvelle connexion. |
||||||
| 134 | * |
||||||
| 135 | * @throws \RedisException |
||||||
| 136 | */ |
||||||
| 137 | protected function _connectTransient(string $server, array $ssl): bool |
||||||
| 138 | { |
||||||
| 139 | if ($ssl === []) { |
||||||
| 140 | return $this->_Redis->connect( |
||||||
| 141 | $server, |
||||||
| 142 | (int) $this->_config['port'], |
||||||
| 143 | (int) $this->_config['timeout'], |
||||||
| 144 | ); |
||||||
| 145 | } |
||||||
| 146 | |||||||
| 147 | return $this->_Redis->connect( |
||||||
| 148 | $server, |
||||||
| 149 | (int) $this->_config['port'], |
||||||
| 150 | (int) $this->_config['timeout'], |
||||||
| 151 | null, |
||||||
| 152 | 0, |
||||||
| 153 | 0.0, |
||||||
| 154 | ['ssl' => $ssl], |
||||||
|
0 ignored issues
–
show
|
|||||||
| 155 | ); |
||||||
| 156 | } |
||||||
| 157 | |||||||
| 158 | /** |
||||||
| 159 | * Se connecte au serveur Redis en utilisant une connexion persistente. |
||||||
| 160 | * |
||||||
| 161 | * @throws \RedisException |
||||||
| 162 | */ |
||||||
| 163 | protected function _connectPersistent(string $server, array $ssl): bool |
||||||
| 164 | { |
||||||
| 165 | $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database']; |
||||||
| 166 | |||||||
| 167 | if ($ssl === []) { |
||||||
| 168 | return $this->_Redis->pconnect( |
||||||
| 169 | $server, |
||||||
| 170 | (int) $this->_config['port'], |
||||||
| 171 | (int) $this->_config['timeout'], |
||||||
| 172 | $persistentId, |
||||||
| 173 | ); |
||||||
| 174 | } |
||||||
| 175 | |||||||
| 176 | return $this->_Redis->pconnect( |
||||||
| 177 | $server, |
||||||
| 178 | (int) $this->_config['port'], |
||||||
| 179 | (int) $this->_config['timeout'], |
||||||
| 180 | $persistentId, |
||||||
| 181 | 0, |
||||||
| 182 | 0.0, |
||||||
| 183 | ['ssl' => $ssl], |
||||||
|
0 ignored issues
–
show
The call to
Redis::pconnect() has too many arguments starting with array('ssl' => $ssl).
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||
| 184 | ); |
||||||
| 185 | } |
||||||
| 186 | |||||||
| 187 | /** |
||||||
| 188 | * {@inheritDoc} |
||||||
| 189 | */ |
||||||
| 190 | public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool |
||||||
| 191 | { |
||||||
| 192 | $key = $this->_key($key); |
||||||
| 193 | $value = $this->serialize($value); |
||||||
| 194 | |||||||
| 195 | $duration = $this->duration($ttl); |
||||||
| 196 | if ($duration === 0) { |
||||||
| 197 | return $this->_Redis->set($key, $value); |
||||||
| 198 | } |
||||||
| 199 | |||||||
| 200 | return $this->_Redis->setEx($key, $duration, $value); |
||||||
| 201 | } |
||||||
| 202 | |||||||
| 203 | /** |
||||||
| 204 | * {@inheritDoc} |
||||||
| 205 | */ |
||||||
| 206 | public function get(string $key, mixed $default = null): mixed |
||||||
| 207 | { |
||||||
| 208 | $value = $this->_Redis->get($this->_key($key)); |
||||||
| 209 | if ($value === false) { |
||||||
| 210 | return $default; |
||||||
| 211 | } |
||||||
| 212 | |||||||
| 213 | return $this->unserialize($value); |
||||||
| 214 | } |
||||||
| 215 | |||||||
| 216 | /** |
||||||
| 217 | * {@inheritDoc} |
||||||
| 218 | */ |
||||||
| 219 | public function increment(string $key, int $offset = 1) |
||||||
| 220 | { |
||||||
| 221 | $duration = $this->_config['duration']; |
||||||
| 222 | $key = $this->_key($key); |
||||||
| 223 | |||||||
| 224 | $value = $this->_Redis->incrBy($key, $offset); |
||||||
| 225 | if ($duration > 0) { |
||||||
| 226 | $this->_Redis->expire($key, $duration); |
||||||
| 227 | } |
||||||
| 228 | |||||||
| 229 | return $value; |
||||||
| 230 | } |
||||||
| 231 | |||||||
| 232 | /** |
||||||
| 233 | * {@inheritDoc} |
||||||
| 234 | */ |
||||||
| 235 | public function decrement(string $key, int $offset = 1) |
||||||
| 236 | { |
||||||
| 237 | $duration = $this->_config['duration']; |
||||||
| 238 | $key = $this->_key($key); |
||||||
| 239 | |||||||
| 240 | $value = $this->_Redis->decrBy($key, $offset); |
||||||
| 241 | if ($duration > 0) { |
||||||
| 242 | $this->_Redis->expire($key, $duration); |
||||||
| 243 | } |
||||||
| 244 | |||||||
| 245 | return $value; |
||||||
| 246 | } |
||||||
| 247 | |||||||
| 248 | /** |
||||||
| 249 | * {@inheritDoc} |
||||||
| 250 | */ |
||||||
| 251 | public function delete(string $key): bool |
||||||
| 252 | { |
||||||
| 253 | $key = $this->_key($key); |
||||||
| 254 | |||||||
| 255 | return (int) $this->_Redis->del($key) > 0; |
||||||
| 256 | } |
||||||
| 257 | |||||||
| 258 | /** |
||||||
| 259 | * Supprime une clé du cache de manière asynchrone |
||||||
| 260 | * |
||||||
| 261 | * Supprime juste une clé du cahce. Le retrait actuel se fera plutard de manière asynchrone. |
||||||
| 262 | */ |
||||||
| 263 | public function deleteAsync(string $key): bool |
||||||
| 264 | { |
||||||
| 265 | $key = $this->_key($key); |
||||||
| 266 | |||||||
| 267 | return (int) $this->_Redis->unlink($key) > 0; |
||||||
| 268 | } |
||||||
| 269 | |||||||
| 270 | /** |
||||||
| 271 | * {@inheritDoc} |
||||||
| 272 | */ |
||||||
| 273 | public function clear(): bool |
||||||
| 274 | { |
||||||
| 275 | if ($this->getConfig('clearUsesFlushDb')) { |
||||||
| 276 | $this->_Redis->flushDB(false); |
||||||
|
0 ignored issues
–
show
The call to
Redis::flushDB() has too many arguments starting with false.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||
| 277 | |||||||
| 278 | return true; |
||||||
| 279 | } |
||||||
| 280 | |||||||
| 281 | $this->_Redis->setOption(Redis::OPT_SCAN, (string) Redis::SCAN_RETRY); |
||||||
| 282 | |||||||
| 283 | $isAllDeleted = true; |
||||||
| 284 | $iterator = null; |
||||||
| 285 | $pattern = $this->_config['prefix'] . '*'; |
||||||
| 286 | |||||||
| 287 | while (true) { |
||||||
| 288 | $keys = $this->_Redis->scan($iterator, $pattern); |
||||||
| 289 | |||||||
| 290 | if ($keys === false) { |
||||||
| 291 | break; |
||||||
| 292 | } |
||||||
| 293 | |||||||
| 294 | foreach ($keys as $key) { |
||||||
| 295 | $isDeleted = ((int) $this->_Redis->del($key) > 0); |
||||||
| 296 | $isAllDeleted = $isAllDeleted && $isDeleted; |
||||||
| 297 | } |
||||||
| 298 | } |
||||||
| 299 | |||||||
| 300 | return $isAllDeleted; |
||||||
| 301 | } |
||||||
| 302 | |||||||
| 303 | /** |
||||||
| 304 | * Supprime toutes les clés du cache du cache en bloquant l'opération. |
||||||
| 305 | */ |
||||||
| 306 | public function clearBlocking(): bool |
||||||
| 307 | { |
||||||
| 308 | if ($this->getConfig('clearUsesFlushDb')) { |
||||||
| 309 | $this->_Redis->flushDB(true); |
||||||
|
0 ignored issues
–
show
The call to
Redis::flushDB() has too many arguments starting with true.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||
| 310 | |||||||
| 311 | return true; |
||||||
| 312 | } |
||||||
| 313 | |||||||
| 314 | $this->_Redis->setOption(Redis::OPT_SCAN, (string) Redis::SCAN_RETRY); |
||||||
| 315 | |||||||
| 316 | $isAllDeleted = true; |
||||||
| 317 | $iterator = null; |
||||||
| 318 | $pattern = $this->_config['prefix'] . '*'; |
||||||
| 319 | |||||||
| 320 | while (true) { |
||||||
| 321 | $keys = $this->_Redis->scan($iterator, $pattern, (int)$this->_config['scanCount']); |
||||||
| 322 | |||||||
| 323 | if ($keys === false) { |
||||||
| 324 | break; |
||||||
| 325 | } |
||||||
| 326 | |||||||
| 327 | foreach ($keys as $key) { |
||||||
| 328 | $isDeleted = ((int)$this->_Redis->del($key) > 0); |
||||||
| 329 | $isAllDeleted = $isAllDeleted && $isDeleted; |
||||||
| 330 | } |
||||||
| 331 | } |
||||||
| 332 | |||||||
| 333 | return $isAllDeleted; |
||||||
| 334 | } |
||||||
| 335 | |||||||
| 336 | /** |
||||||
| 337 | * {@inheritDoc} |
||||||
| 338 | * |
||||||
| 339 | * @see https://github.com/phpredis/phpredis#set |
||||||
| 340 | */ |
||||||
| 341 | public function add(string $key, mixed $value): bool |
||||||
| 342 | { |
||||||
| 343 | $duration = $this->_config['duration']; |
||||||
| 344 | $key = $this->_key($key); |
||||||
| 345 | $value = $this->serialize($value); |
||||||
| 346 | |||||||
| 347 | return (bool) ($this->_Redis->set($key, $value, ['nx', 'ex' => $duration])); |
||||||
| 348 | } |
||||||
| 349 | |||||||
| 350 | /** |
||||||
| 351 | * {@inheritDoc} |
||||||
| 352 | */ |
||||||
| 353 | public function info() |
||||||
| 354 | { |
||||||
| 355 | return $this->_Redis->info(); |
||||||
| 356 | } |
||||||
| 357 | |||||||
| 358 | /** |
||||||
| 359 | * {@inheritDoc} |
||||||
| 360 | */ |
||||||
| 361 | public function groups(): array |
||||||
| 362 | { |
||||||
| 363 | $result = []; |
||||||
| 364 | |||||||
| 365 | foreach ($this->_config['groups'] as $group) { |
||||||
| 366 | $value = $this->_Redis->get($this->_config['prefix'] . $group); |
||||||
| 367 | if (! $value) { |
||||||
| 368 | $value = $this->serialize(1); |
||||||
| 369 | $this->_Redis->set($this->_config['prefix'] . $group, $value); |
||||||
| 370 | } |
||||||
| 371 | $result[] = $group . $value; |
||||||
| 372 | } |
||||||
| 373 | |||||||
| 374 | return $result; |
||||||
| 375 | } |
||||||
| 376 | |||||||
| 377 | /** |
||||||
| 378 | * {@inheritDoc} |
||||||
| 379 | */ |
||||||
| 380 | public function clearGroup(string $group): bool |
||||||
| 381 | { |
||||||
| 382 | return (bool) $this->_Redis->incr($this->_config['prefix'] . $group); |
||||||
| 383 | } |
||||||
| 384 | |||||||
| 385 | /** |
||||||
| 386 | * Sérialisez la valeur pour l'enregistrer dans Redis. |
||||||
| 387 | * |
||||||
| 388 | * Ceci est nécessaire au lieu d'utiliser la fonction de sérialisation intégrée de Redis |
||||||
| 389 | * car cela crée des problèmes d'incrémentation/décrémentation de la valeur entière initialement définie. |
||||||
| 390 | * |
||||||
| 391 | * @see https://github.com/phpredis/phpredis/issues/81 |
||||||
| 392 | */ |
||||||
| 393 | protected function serialize(mixed $value): string |
||||||
| 394 | { |
||||||
| 395 | if (is_int($value)) { |
||||||
| 396 | return (string) $value; |
||||||
| 397 | } |
||||||
| 398 | |||||||
| 399 | return serialize($value); |
||||||
| 400 | } |
||||||
| 401 | |||||||
| 402 | /** |
||||||
| 403 | * Désérialiser la valeur de chaîne extraite de Redis. |
||||||
| 404 | */ |
||||||
| 405 | protected function unserialize(string $value): mixed |
||||||
| 406 | { |
||||||
| 407 | if (preg_match('/^[-]?\d+$/', $value)) { |
||||||
| 408 | return (int) $value; |
||||||
| 409 | } |
||||||
| 410 | |||||||
| 411 | return unserialize($value); |
||||||
| 412 | } |
||||||
| 413 | |||||||
| 414 | /** |
||||||
| 415 | * Cree une instance Redis. |
||||||
| 416 | */ |
||||||
| 417 | protected function _createRedisInstance(): Redis |
||||||
| 418 | { |
||||||
| 419 | return new Redis(); |
||||||
| 420 | } |
||||||
| 421 | |||||||
| 422 | /** |
||||||
| 423 | * Se déconnecte du serveur redis |
||||||
| 424 | */ |
||||||
| 425 | public function __destruct() |
||||||
| 426 | { |
||||||
| 427 | if (empty($this->_config['persistent']) && $this->_Redis instanceof Redis) { |
||||||
| 428 | $this->_Redis->close(); |
||||||
| 429 | } |
||||||
| 430 | } |
||||||
| 431 | } |
||||||
| 432 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.