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 InvalidArgumentException; |
16
|
|
|
use Memcached as BaseMemcached; |
17
|
|
|
use RuntimeException; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Moteur de stockage Memcached pour le cache. Memcached a certaines limitations dans la quantité de |
21
|
|
|
* le contrôle que vous avez sur les délais d'expiration lointains dans le futur. Voir MemcachedEngine::write() pour |
22
|
|
|
* Plus d'information. |
23
|
|
|
* |
24
|
|
|
* Le moteur Memcached prend en charge le protocole binaire et igbinary |
25
|
|
|
* sérialisation (si l'extension memcached est compilée avec --enable-igbinary). |
26
|
|
|
* Les touches compressées peuvent également être incrémentées/décrémentées. |
27
|
|
|
*/ |
28
|
|
|
class Memcached extends BaseHandler |
29
|
|
|
{ |
30
|
|
|
/** |
31
|
|
|
* Wrapper Memcached. |
32
|
|
|
* |
33
|
|
|
* @var BaseMemcached |
34
|
|
|
*/ |
35
|
|
|
protected $_Memcached; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* La configuration par défaut utilisée sauf si elle est remplacée par la configuration d'exécution |
39
|
|
|
* |
40
|
|
|
* - `compress` Indique s'il faut compresser les données |
41
|
|
|
* - `duration` Spécifiez combien de temps durent les éléments de cette configuration de cache. |
42
|
|
|
* - `groups` Liste des groupes ou 'tags' associés à chaque clé stockée dans cette configuration. |
43
|
|
|
* pratique pour supprimer un groupe complet du cache. |
44
|
|
|
* - `nom d'utilisateur` Connectez-vous pour accéder au serveur Memcache |
45
|
|
|
* - `password` Mot de passe pour accéder au serveur Memcache |
46
|
|
|
* - `persistent` Le nom de la connexion persistante. Toutes les configurations utilisant |
47
|
|
|
* la même valeur persistante partagera une seule connexion sous-jacente. |
48
|
|
|
* - `prefix` Préfixé à toutes les entrées. Bon pour quand vous avez besoin de partager un keyspace |
49
|
|
|
* avec une autre configuration de cache ou une autre application. |
50
|
|
|
* - `serialize` Le moteur de sérialisation utilisé pour sérialiser les données. Les moteurs disponibles sont 'php', |
51
|
|
|
* 'igbinaire' et 'json'. A côté de 'php', l'extension memcached doit être compilée avec le |
52
|
|
|
* Prise en charge appropriée du sérialiseur. |
53
|
|
|
* - `servers` Chaîne ou tableau de serveurs memcached. Si un tableau MemcacheEngine utilisera |
54
|
|
|
* eux comme une piscine. |
55
|
|
|
* - `options` - Options supplémentaires pour le client memcached. Doit être un tableau d'option => valeur. |
56
|
|
|
* Utilisez les constantes \Memcached::OPT_* comme clés. |
57
|
|
|
*/ |
58
|
|
|
protected array $_defaultConfig = [ |
59
|
|
|
'compress' => false, |
60
|
|
|
'duration' => 3600, |
61
|
|
|
'groups' => [], |
62
|
|
|
'host' => null, |
63
|
|
|
'username' => null, |
64
|
|
|
'password' => null, |
65
|
|
|
'persistent' => null, |
66
|
|
|
'port' => null, |
67
|
|
|
'prefix' => 'blitz_', |
68
|
|
|
'serialize' => 'php', |
69
|
|
|
'servers' => ['127.0.0.1'], |
70
|
|
|
'options' => [], |
71
|
|
|
]; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Liste des moteurs de sérialisation disponibles |
75
|
|
|
* |
76
|
|
|
* Memcached doit être compilé avec JSON et le support igbinary pour utiliser ces moteurs |
77
|
|
|
*/ |
78
|
|
|
protected array $_serializers = []; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @var string[] |
82
|
|
|
*/ |
83
|
|
|
protected array $_compiledGroupNames = []; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* {@inheritDoc} |
87
|
|
|
*/ |
88
|
|
|
public function init(array $config = []): bool |
89
|
|
|
{ |
90
|
|
|
if (! extension_loaded('memcached')) { |
91
|
|
|
throw new RuntimeException('L\'extension `memcached` doit être activée pour utiliser MemcachedHandler.'); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
$this->_serializers = [ |
95
|
|
|
'igbinary' => BaseMemcached::SERIALIZER_IGBINARY, |
96
|
|
|
'json' => BaseMemcached::SERIALIZER_JSON, |
97
|
|
|
'php' => BaseMemcached::SERIALIZER_PHP, |
98
|
|
|
]; |
99
|
|
|
if (defined('Memcached::HAVE_MSGPACK')) { |
100
|
|
|
$this->_serializers['msgpack'] = BaseMemcached::SERIALIZER_MSGPACK; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
parent::init($config); |
104
|
|
|
|
105
|
|
|
if (! empty($config['host'])) { |
106
|
|
|
if (empty($config['port'])) { |
107
|
|
|
$config['servers'] = [$config['host']]; |
108
|
|
|
} else { |
109
|
|
|
$config['servers'] = [sprintf('%s:%d', $config['host'], $config['port'])]; |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
if (isset($config['servers'])) { |
114
|
|
|
$this->setConfig('servers', $config['servers'], false); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
if (! is_array($this->_config['servers'])) { |
118
|
|
|
$this->_config['servers'] = [$this->_config['servers']]; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** @psalm-suppress RedundantPropertyInitializationCheck */ |
122
|
|
|
if (isset($this->_Memcached)) { |
123
|
|
|
return true; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
if ($this->_config['persistent']) { |
127
|
|
|
$this->_Memcached = new BaseMemcached($this->_config['persistent']); |
128
|
|
|
} else { |
129
|
|
|
$this->_Memcached = new BaseMemcached(); |
130
|
|
|
} |
131
|
|
|
$this->_setOptions(); |
132
|
|
|
|
133
|
|
|
$serverList = $this->_Memcached->getServerList(); |
134
|
|
|
if ($serverList) { |
|
|
|
|
135
|
|
|
if ($this->_Memcached->isPersistent()) { |
136
|
|
|
foreach ($serverList as $server) { |
137
|
|
|
if (! in_array($server['host'] . ':' . $server['port'], $this->_config['servers'], true)) { |
138
|
|
|
throw new InvalidArgumentException( |
139
|
|
|
'Configuration du cache invalide. Plusieurs configurations de cache persistant sont détectées' . |
140
|
|
|
' avec des valeurs `servers` différentes. `valeurs` des serveurs pour les configurations de cache persistant' . |
141
|
|
|
' doit être le même lors de l\'utilisation du même identifiant de persistance.' |
142
|
|
|
); |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return true; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$servers = []; |
151
|
|
|
|
152
|
|
|
foreach ($this->_config['servers'] as $server) { |
153
|
|
|
$servers[] = $this->parseServerString($server); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
if (! $this->_Memcached->addServers($servers)) { |
157
|
|
|
return false; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
if (is_array($this->_config['options'])) { |
161
|
|
|
foreach ($this->_config['options'] as $opt => $value) { |
162
|
|
|
$this->_Memcached->setOption($opt, $value); |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
if (empty($this->_config['username']) && ! empty($this->_config['login'])) { |
167
|
|
|
throw new InvalidArgumentException( |
168
|
|
|
'Veuillez passer "nom d\'utilisateur" au lieu de "login" pour vous connecter à Memcached' |
169
|
|
|
); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
if ($this->_config['username'] !== null && $this->_config['password'] !== null) { |
173
|
|
|
if (! method_exists($this->_Memcached, 'setSaslAuthData')) { |
174
|
|
|
throw new InvalidArgumentException( |
175
|
|
|
"L'extension Memcached n'est pas construite avec le support SASL" |
176
|
|
|
); |
177
|
|
|
} |
178
|
|
|
$this->_Memcached->setOption(BaseMemcached::OPT_BINARY_PROTOCOL, true); |
179
|
|
|
$this->_Memcached->setSaslAuthData( |
180
|
|
|
$this->_config['username'], |
181
|
|
|
$this->_config['password'] |
182
|
|
|
); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
return true; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Paramétrage de l'instance memcached |
190
|
|
|
* |
191
|
|
|
* @throws InvalidArgumentException Lorsque l'extension Memcached n'est pas construite avec le moteur de sérialisation souhaité. |
192
|
|
|
*/ |
193
|
|
|
protected function _setOptions(): void |
194
|
|
|
{ |
195
|
|
|
$this->_Memcached->setOption(BaseMemcached::OPT_LIBKETAMA_COMPATIBLE, true); |
196
|
|
|
|
197
|
|
|
$serializer = strtolower($this->_config['serialize']); |
198
|
|
|
if (! isset($this->_serializers[$serializer])) { |
199
|
|
|
throw new InvalidArgumentException( |
200
|
|
|
sprintf('%s n\'est pas un moteur de sérialisation valide pour Memcached', $serializer) |
201
|
|
|
); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
if ( |
205
|
|
|
$serializer !== 'php' |
206
|
|
|
&& ! constant('Memcached::HAVE_' . strtoupper($serializer)) |
207
|
|
|
) { |
208
|
|
|
throw new InvalidArgumentException( |
209
|
|
|
sprintf('L\'extension Memcached n\'est pas compilée avec la prise en charge de %s', $serializer) |
210
|
|
|
); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$this->_Memcached->setOption( |
214
|
|
|
BaseMemcached::OPT_SERIALIZER, |
215
|
|
|
$this->_serializers[$serializer] |
216
|
|
|
); |
217
|
|
|
|
218
|
|
|
// Check for Amazon ElastiCache instance |
219
|
|
|
if ( |
220
|
|
|
defined('Memcached::OPT_CLIENT_MODE') |
221
|
|
|
&& defined('Memcached::DYNAMIC_CLIENT_MODE') |
222
|
|
|
) { |
223
|
|
|
$this->_Memcached->setOption( |
224
|
|
|
BaseMemcached::OPT_CLIENT_MODE, |
|
|
|
|
225
|
|
|
BaseMemcached::DYNAMIC_CLIENT_MODE |
|
|
|
|
226
|
|
|
); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
$this->_Memcached->setOption( |
230
|
|
|
BaseMemcached::OPT_COMPRESSION, |
231
|
|
|
(bool) $this->_config['compress'] |
232
|
|
|
); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Analyse l'adresse du serveur dans l'hôte/port. Gère à la fois les adresses IPv6 et IPv4 et sockets Unix |
237
|
|
|
* |
238
|
|
|
* @param string $server La chaîne d'adresse du serveur. |
239
|
|
|
* |
240
|
|
|
* @return array Tableau contenant l'hôte, le port |
241
|
|
|
*/ |
242
|
|
|
public function parseServerString(string $server): array |
243
|
|
|
{ |
244
|
|
|
$socketTransport = 'unix://'; |
245
|
|
|
if (str_starts_with($server, $socketTransport)) { |
246
|
|
|
return [substr($server, strlen($socketTransport)), 0]; |
247
|
|
|
} |
248
|
|
|
if (substr($server, 0, 1) === '[') { |
249
|
|
|
$position = strpos($server, ']:'); |
250
|
|
|
if ($position !== false) { |
251
|
|
|
$position++; |
252
|
|
|
} |
253
|
|
|
} else { |
254
|
|
|
$position = strpos($server, ':'); |
255
|
|
|
} |
256
|
|
|
$port = 11211; |
257
|
|
|
$host = $server; |
258
|
|
|
if ($position !== false) { |
259
|
|
|
$host = substr($server, 0, $position); |
260
|
|
|
$port = substr($server, $position + 1); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return [$host, (int) $port]; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Lire une valeur d'option à partir de la connexion memcached. |
268
|
|
|
* |
269
|
|
|
* @return bool|int|string|null |
270
|
|
|
* |
271
|
|
|
* @see https://secure.php.net/manual/en/memcached.getoption.php |
272
|
|
|
*/ |
273
|
|
|
public function getOption(int $name) |
274
|
|
|
{ |
275
|
|
|
return $this->_Memcached->getOption($name); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* {@inheritDoc} |
280
|
|
|
* |
281
|
|
|
* @see https://www.php.net/manual/en/memcached.set.php |
282
|
|
|
*/ |
283
|
|
|
public function set(string $key, mixed $value, null|DateInterval|int $ttl = null): bool |
284
|
|
|
{ |
285
|
|
|
$duration = $this->duration($ttl); |
286
|
|
|
|
287
|
|
|
return $this->_Memcached->set($this->_key($key), $value, $duration); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* {@inheritDoc} |
292
|
|
|
*/ |
293
|
|
|
public function setMultiple(iterable $values, null|DateInterval|int $ttl = null): bool |
294
|
|
|
{ |
295
|
|
|
$cacheData = []; |
296
|
|
|
|
297
|
|
|
foreach ($values as $key => $value) { |
298
|
|
|
$cacheData[$this->_key($key)] = $value; |
299
|
|
|
} |
300
|
|
|
$duration = $this->duration($ttl); |
301
|
|
|
|
302
|
|
|
return $this->_Memcached->setMulti($cacheData, $duration); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* {@inheritDoc} |
307
|
|
|
*/ |
308
|
|
|
public function get(string $key, mixed $default = null): mixed |
309
|
|
|
{ |
310
|
|
|
$key = $this->_key($key); |
311
|
|
|
$value = $this->_Memcached->get($key); |
312
|
|
|
if ($this->_Memcached->getResultCode() === BaseMemcached::RES_NOTFOUND) { |
313
|
|
|
return $default; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
return $value; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* {@inheritDoc} |
321
|
|
|
* |
322
|
|
|
* @return array Un tableau contenant, pour chacune des $keys données, les données mises en cache ou false si les données mises en cache n'ont pas pu être récupérées. |
323
|
|
|
*/ |
324
|
|
|
public function getMultiple(iterable $keys, mixed $default = null): iterable |
325
|
|
|
{ |
326
|
|
|
$cacheKeys = []; |
327
|
|
|
|
328
|
|
|
foreach ($keys as $key) { |
329
|
|
|
$cacheKeys[$key] = $this->_key($key); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
$values = $this->_Memcached->getMulti($cacheKeys); |
333
|
|
|
$return = []; |
334
|
|
|
|
335
|
|
|
foreach ($cacheKeys as $original => $prefixed) { |
336
|
|
|
$return[$original] = $values[$prefixed] ?? $default; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
return $return; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* {@inheritDoc} |
344
|
|
|
*/ |
345
|
|
|
public function increment(string $key, int $offset = 1) |
346
|
|
|
{ |
347
|
|
|
return $this->_Memcached->increment($this->_key($key), $offset); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* {@inheritDoc} |
352
|
|
|
*/ |
353
|
|
|
public function decrement(string $key, int $offset = 1) |
354
|
|
|
{ |
355
|
|
|
return $this->_Memcached->decrement($this->_key($key), $offset); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* {@inheritDoc} |
360
|
|
|
*/ |
361
|
|
|
public function delete(string $key): bool |
362
|
|
|
{ |
363
|
|
|
return $this->_Memcached->delete($this->_key($key)); |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* {@inheritDoc} |
368
|
|
|
*/ |
369
|
|
|
public function deleteMultiple(iterable $keys): bool |
370
|
|
|
{ |
371
|
|
|
$cacheKeys = []; |
372
|
|
|
|
373
|
|
|
foreach ($keys as $key) { |
374
|
|
|
$cacheKeys[] = $this->_key($key); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
return (bool) $this->_Memcached->deleteMulti($cacheKeys); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* {@inheritDoc} |
382
|
|
|
*/ |
383
|
|
|
public function clear(): bool |
384
|
|
|
{ |
385
|
|
|
$keys = $this->_Memcached->getAllKeys(); |
386
|
|
|
if ($keys === false) { |
387
|
|
|
return false; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
foreach ($keys as $key) { |
391
|
|
|
if (str_starts_with($key, $this->_config['prefix'])) { |
392
|
|
|
$this->_Memcached->delete($key); |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
return true; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* {@inheritDoc} |
401
|
|
|
*/ |
402
|
|
|
public function add(string $key, mixed $value): bool |
403
|
|
|
{ |
404
|
|
|
$duration = $this->_config['duration']; |
405
|
|
|
$key = $this->_key($key); |
406
|
|
|
|
407
|
|
|
return $this->_Memcached->add($key, $value, $duration); |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* {@inheritDoc} |
412
|
|
|
*/ |
413
|
|
|
public function info() |
414
|
|
|
{ |
415
|
|
|
return $this->_Memcached->getStats(); |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* {@inheritDoc} |
420
|
|
|
*/ |
421
|
|
|
public function groups(): array |
422
|
|
|
{ |
423
|
|
|
if (empty($this->_compiledGroupNames)) { |
424
|
|
|
foreach ($this->_config['groups'] as $group) { |
425
|
|
|
$this->_compiledGroupNames[] = $this->_config['prefix'] . $group; |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
$groups = $this->_Memcached->getMulti($this->_compiledGroupNames) ?: []; |
430
|
|
|
if (count($groups) !== count($this->_config['groups'])) { |
431
|
|
|
foreach ($this->_compiledGroupNames as $group) { |
432
|
|
|
if (! isset($groups[$group])) { |
433
|
|
|
$this->_Memcached->set($group, 1, 0); |
434
|
|
|
$groups[$group] = 1; |
435
|
|
|
} |
436
|
|
|
} |
437
|
|
|
ksort($groups); |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
$result = []; |
441
|
|
|
$groups = array_values($groups); |
442
|
|
|
|
443
|
|
|
foreach ($this->_config['groups'] as $i => $group) { |
444
|
|
|
$result[] = $group . $groups[$i]; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
return $result; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* {@inheritDoc} |
452
|
|
|
*/ |
453
|
|
|
public function clearGroup(string $group): bool |
454
|
|
|
{ |
455
|
|
|
return (bool) $this->_Memcached->increment($this->_config['prefix'] . $group); |
456
|
|
|
} |
457
|
|
|
} |
458
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.