Issues (29)

Handlers/RedisHandler.php (4 issues)

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
The call to Redis::connect() 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 ignore-call  annotation

154
        return $this->_Redis->/** @scrutinizer ignore-call */ connect(

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...
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 ignore-call  annotation

183
        return $this->_Redis->/** @scrutinizer ignore-call */ pconnect(

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 ignore-call  annotation

276
            $this->_Redis->/** @scrutinizer ignore-call */ 
277
                           flushDB(false);

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 ignore-call  annotation

309
            $this->_Redis->/** @scrutinizer ignore-call */ 
310
                           flushDB(true);

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