• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

predis / predis / 14450042814

14 Apr 2025 03:51PM UTC coverage: 92.662% (-0.09%) from 92.752%
14450042814

push

github

web-flow
Merge pull request #1527 from predis/vv-readme-8.0-support

7147 of 7713 relevant lines covered (92.66%)

111.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.71
/src/Cluster/ClusterStrategy.php
1
<?php
2

3
/*
4
 * This file is part of the Predis package.
5
 *
6
 * (c) 2009-2020 Daniele Alessandri
7
 * (c) 2021-2025 Till Krüss
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace Predis\Cluster;
14

15
use InvalidArgumentException;
16
use Predis\Command\CommandInterface;
17
use Predis\Command\ScriptCommand;
18

19
/**
20
 * Common class implementing the logic needed to support clustering strategies.
21
 */
22
abstract class ClusterStrategy implements StrategyInterface
23
{
24
    protected $commands;
25

26
    public function __construct()
140✔
27
    {
28
        $this->commands = $this->getDefaultCommands();
140✔
29
    }
30

31
    /**
32
     * Returns the default map of supported commands with their handlers.
33
     *
34
     * @return array
35
     */
36
    protected function getDefaultCommands()
140✔
37
    {
38
        $getKeyFromFirstArgument = [$this, 'getKeyFromFirstArgument'];
140✔
39
        $getKeyFromAllArguments = [$this, 'getKeyFromAllArguments'];
140✔
40

41
        return [
140✔
42
            /* commands operating on the key space */
43
            'EXISTS' => $getKeyFromAllArguments,
140✔
44
            'DEL' => $getKeyFromAllArguments,
140✔
45
            'TYPE' => $getKeyFromFirstArgument,
140✔
46
            'EXPIRE' => $getKeyFromFirstArgument,
140✔
47
            'EXPIREAT' => $getKeyFromFirstArgument,
140✔
48
            'PERSIST' => $getKeyFromFirstArgument,
140✔
49
            'PEXPIRE' => $getKeyFromFirstArgument,
140✔
50
            'PEXPIREAT' => $getKeyFromFirstArgument,
140✔
51
            'TTL' => $getKeyFromFirstArgument,
140✔
52
            'PTTL' => $getKeyFromFirstArgument,
140✔
53
            'SORT' => [$this, 'getKeyFromSortCommand'],
140✔
54
            'DUMP' => $getKeyFromFirstArgument,
140✔
55
            'RESTORE' => $getKeyFromFirstArgument,
140✔
56
            'FLUSHDB' => [$this, 'getFakeKey'],
140✔
57

58
            /* commands operating on string values */
59
            'APPEND' => $getKeyFromFirstArgument,
140✔
60
            'DECR' => $getKeyFromFirstArgument,
140✔
61
            'DECRBY' => $getKeyFromFirstArgument,
140✔
62
            'GET' => $getKeyFromFirstArgument,
140✔
63
            'GETBIT' => $getKeyFromFirstArgument,
140✔
64
            'MGET' => $getKeyFromAllArguments,
140✔
65
            'SET' => $getKeyFromFirstArgument,
140✔
66
            'GETRANGE' => $getKeyFromFirstArgument,
140✔
67
            'GETSET' => $getKeyFromFirstArgument,
140✔
68
            'INCR' => $getKeyFromFirstArgument,
140✔
69
            'INCRBY' => $getKeyFromFirstArgument,
140✔
70
            'INCRBYFLOAT' => $getKeyFromFirstArgument,
140✔
71
            'SETBIT' => $getKeyFromFirstArgument,
140✔
72
            'SETEX' => $getKeyFromFirstArgument,
140✔
73
            'MSET' => [$this, 'getKeyFromInterleavedArguments'],
140✔
74
            'MSETNX' => [$this, 'getKeyFromInterleavedArguments'],
140✔
75
            'SETNX' => $getKeyFromFirstArgument,
140✔
76
            'SETRANGE' => $getKeyFromFirstArgument,
140✔
77
            'STRLEN' => $getKeyFromFirstArgument,
140✔
78
            'SUBSTR' => $getKeyFromFirstArgument,
140✔
79
            'BITOP' => [$this, 'getKeyFromBitOp'],
140✔
80
            'BITCOUNT' => $getKeyFromFirstArgument,
140✔
81
            'BITFIELD' => $getKeyFromFirstArgument,
140✔
82

83
            /* commands operating on lists */
84
            'LINSERT' => $getKeyFromFirstArgument,
140✔
85
            'LINDEX' => $getKeyFromFirstArgument,
140✔
86
            'LLEN' => $getKeyFromFirstArgument,
140✔
87
            'LPOP' => $getKeyFromFirstArgument,
140✔
88
            'RPOP' => $getKeyFromFirstArgument,
140✔
89
            'RPOPLPUSH' => $getKeyFromAllArguments,
140✔
90
            'BLPOP' => [$this, 'getKeyFromBlockingListCommands'],
140✔
91
            'BRPOP' => [$this, 'getKeyFromBlockingListCommands'],
140✔
92
            'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'],
140✔
93
            'LPUSH' => $getKeyFromFirstArgument,
140✔
94
            'LPUSHX' => $getKeyFromFirstArgument,
140✔
95
            'RPUSH' => $getKeyFromFirstArgument,
140✔
96
            'RPUSHX' => $getKeyFromFirstArgument,
140✔
97
            'LRANGE' => $getKeyFromFirstArgument,
140✔
98
            'LREM' => $getKeyFromFirstArgument,
140✔
99
            'LSET' => $getKeyFromFirstArgument,
140✔
100
            'LTRIM' => $getKeyFromFirstArgument,
140✔
101

102
            /* commands operating on sets */
103
            'SADD' => $getKeyFromFirstArgument,
140✔
104
            'SCARD' => $getKeyFromFirstArgument,
140✔
105
            'SDIFF' => $getKeyFromAllArguments,
140✔
106
            'SDIFFSTORE' => $getKeyFromAllArguments,
140✔
107
            'SINTER' => $getKeyFromAllArguments,
140✔
108
            'SINTERSTORE' => $getKeyFromAllArguments,
140✔
109
            'SUNION' => $getKeyFromAllArguments,
140✔
110
            'SUNIONSTORE' => $getKeyFromAllArguments,
140✔
111
            'SISMEMBER' => $getKeyFromFirstArgument,
140✔
112
            'SMEMBERS' => $getKeyFromFirstArgument,
140✔
113
            'SSCAN' => $getKeyFromFirstArgument,
140✔
114
            'SPOP' => $getKeyFromFirstArgument,
140✔
115
            'SRANDMEMBER' => $getKeyFromFirstArgument,
140✔
116
            'SREM' => $getKeyFromFirstArgument,
140✔
117

118
            /* commands operating on sorted sets */
119
            'ZADD' => $getKeyFromFirstArgument,
140✔
120
            'ZCARD' => $getKeyFromFirstArgument,
140✔
121
            'ZCOUNT' => $getKeyFromFirstArgument,
140✔
122
            'ZINCRBY' => $getKeyFromFirstArgument,
140✔
123
            'ZINTERSTORE' => [$this, 'getKeyFromZsetAggregationCommands'],
140✔
124
            'ZRANGE' => $getKeyFromFirstArgument,
140✔
125
            'ZRANGEBYSCORE' => $getKeyFromFirstArgument,
140✔
126
            'ZRANK' => $getKeyFromFirstArgument,
140✔
127
            'ZREM' => $getKeyFromFirstArgument,
140✔
128
            'ZREMRANGEBYRANK' => $getKeyFromFirstArgument,
140✔
129
            'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument,
140✔
130
            'ZREVRANGE' => $getKeyFromFirstArgument,
140✔
131
            'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument,
140✔
132
            'ZREVRANK' => $getKeyFromFirstArgument,
140✔
133
            'ZSCORE' => $getKeyFromFirstArgument,
140✔
134
            'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'],
140✔
135
            'ZSCAN' => $getKeyFromFirstArgument,
140✔
136
            'ZLEXCOUNT' => $getKeyFromFirstArgument,
140✔
137
            'ZRANGEBYLEX' => $getKeyFromFirstArgument,
140✔
138
            'ZREMRANGEBYLEX' => $getKeyFromFirstArgument,
140✔
139
            'ZREVRANGEBYLEX' => $getKeyFromFirstArgument,
140✔
140

141
            /* commands operating on hashes */
142
            'HDEL' => $getKeyFromFirstArgument,
140✔
143
            'HEXISTS' => $getKeyFromFirstArgument,
140✔
144
            'HGET' => $getKeyFromFirstArgument,
140✔
145
            'HGETALL' => $getKeyFromFirstArgument,
140✔
146
            'HMGET' => $getKeyFromFirstArgument,
140✔
147
            'HMSET' => $getKeyFromFirstArgument,
140✔
148
            'HINCRBY' => $getKeyFromFirstArgument,
140✔
149
            'HINCRBYFLOAT' => $getKeyFromFirstArgument,
140✔
150
            'HKEYS' => $getKeyFromFirstArgument,
140✔
151
            'HLEN' => $getKeyFromFirstArgument,
140✔
152
            'HSET' => $getKeyFromFirstArgument,
140✔
153
            'HSETNX' => $getKeyFromFirstArgument,
140✔
154
            'HVALS' => $getKeyFromFirstArgument,
140✔
155
            'HSCAN' => $getKeyFromFirstArgument,
140✔
156
            'HSTRLEN' => $getKeyFromFirstArgument,
140✔
157

158
            /* commands operating on HyperLogLog */
159
            'PFADD' => $getKeyFromFirstArgument,
140✔
160
            'PFCOUNT' => $getKeyFromAllArguments,
140✔
161
            'PFMERGE' => $getKeyFromAllArguments,
140✔
162

163
            /* scripting */
164
            'EVAL' => [$this, 'getKeyFromScriptingCommands'],
140✔
165
            'EVALSHA' => [$this, 'getKeyFromScriptingCommands'],
140✔
166
            'EVAL_RO' => [$this, 'getKeyFromScriptingCommands'],
140✔
167
            'EVALSHA_RO' => [$this, 'getKeyFromScriptingCommands'],
140✔
168

169
            /* server */
170
            'INFO' => [$this, 'getFakeKey'],
140✔
171

172
            /* commands performing geospatial operations */
173
            'GEOADD' => $getKeyFromFirstArgument,
140✔
174
            'GEOHASH' => $getKeyFromFirstArgument,
140✔
175
            'GEOPOS' => $getKeyFromFirstArgument,
140✔
176
            'GEODIST' => $getKeyFromFirstArgument,
140✔
177
            'GEORADIUS' => [$this, 'getKeyFromGeoradiusCommands'],
140✔
178
            'GEORADIUSBYMEMBER' => [$this, 'getKeyFromGeoradiusCommands'],
140✔
179

180
            /* sharded pubsub */
181
            'SSUBSCRIBE' => $getKeyFromAllArguments,
140✔
182
            'SUNSUBSCRIBE' => [$this, 'getKeyFromSUnsubscribeCommand'],
140✔
183
            'SPUBLISH' => $getKeyFromFirstArgument,
140✔
184

185
            /* cluster */
186
            'CLUSTER' => [$this, 'getFakeKey'],
140✔
187
        ];
140✔
188
    }
189

190
    /**
191
     * Returns the list of IDs for the supported commands.
192
     *
193
     * @return array
194
     */
195
    public function getSupportedCommands()
2✔
196
    {
197
        return array_keys($this->commands);
2✔
198
    }
199

200
    /**
201
     * Sets an handler for the specified command ID.
202
     *
203
     * The signature of the callback must have a single parameter of type
204
     * Predis\Command\CommandInterface.
205
     *
206
     * When the callback argument is omitted or NULL, the previously associated
207
     * handler for the specified command ID is removed.
208
     *
209
     * @param string $commandID Command ID.
210
     * @param mixed  $callback  A valid callable object, or NULL to unset the handler.
211
     *
212
     * @throws InvalidArgumentException
213
     */
214
    public function setCommandHandler($commandID, $callback = null)
4✔
215
    {
216
        $commandID = strtoupper($commandID);
4✔
217

218
        if (!isset($callback)) {
4✔
219
            unset($this->commands[$commandID]);
2✔
220

221
            return;
2✔
222
        }
223

224
        if (!is_callable($callback)) {
2✔
225
            throw new InvalidArgumentException(
×
226
                'The argument must be a callable object or NULL.'
×
227
            );
×
228
        }
229

230
        $this->commands[$commandID] = $callback;
2✔
231
    }
232

233
    /**
234
     * Get fake key for commands with no key argument.
235
     *
236
     * @return string
237
     */
238
    protected function getFakeKey(): string
30✔
239
    {
240
        return 'key';
30✔
241
    }
242

243
    /**
244
     * Extracts the key from the first argument of a command instance.
245
     *
246
     * @param CommandInterface $command Command instance.
247
     *
248
     * @return string
249
     */
250
    protected function getKeyFromFirstArgument(CommandInterface $command)
50✔
251
    {
252
        return $command->getArgument(0);
50✔
253
    }
254

255
    /**
256
     * Extracts the key from a command with multiple keys only when all keys in
257
     * the arguments array produce the same hash.
258
     *
259
     * @param CommandInterface $command Command instance.
260
     *
261
     * @return string|null
262
     */
263
    protected function getKeyFromAllArguments(CommandInterface $command)
9✔
264
    {
265
        $arguments = $command->getArguments();
9✔
266

267
        if (!$this->checkSameSlotForKeys($arguments)) {
9✔
268
            return null;
1✔
269
        }
270

271
        return $arguments[0];
8✔
272
    }
273

274
    /**
275
     * Extracts the key from a command with multiple keys only when all keys in
276
     * the arguments array produce the same hash.
277
     *
278
     * @param CommandInterface $command Command instance.
279
     *
280
     * @return string|null
281
     */
282
    protected function getKeyFromInterleavedArguments(CommandInterface $command)
3✔
283
    {
284
        $arguments = $command->getArguments();
3✔
285
        $keys = [];
3✔
286

287
        for ($i = 0; $i < count($arguments); $i += 2) {
3✔
288
            $keys[] = $arguments[$i];
3✔
289
        }
290

291
        if (!$this->checkSameSlotForKeys($keys)) {
3✔
292
            return null;
1✔
293
        }
294

295
        return $arguments[0];
2✔
296
    }
297

298
    /**
299
     * Extracts the key from SORT command.
300
     *
301
     * @param CommandInterface $command Command instance.
302
     *
303
     * @return string|null
304
     */
305
    protected function getKeyFromSortCommand(CommandInterface $command)
3✔
306
    {
307
        $arguments = $command->getArguments();
3✔
308
        $firstKey = $arguments[0];
3✔
309

310
        if (1 === $argc = count($arguments)) {
3✔
311
            return $firstKey;
3✔
312
        }
313

314
        $keys = [$firstKey];
2✔
315

316
        for ($i = 1; $i < $argc; ++$i) {
2✔
317
            if (strtoupper($arguments[$i]) === 'STORE') {
2✔
318
                $keys[] = $arguments[++$i];
2✔
319
            }
320
        }
321

322
        if (!$this->checkSameSlotForKeys($keys)) {
2✔
323
            return null;
×
324
        }
325

326
        return $firstKey;
2✔
327
    }
328

329
    /**
330
     * Extracts the key from BLPOP and BRPOP commands.
331
     *
332
     * @param CommandInterface $command Command instance.
333
     *
334
     * @return string|null
335
     */
336
    protected function getKeyFromBlockingListCommands(CommandInterface $command)
3✔
337
    {
338
        $arguments = $command->getArguments();
3✔
339

340
        if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
3✔
341
            return null;
1✔
342
        }
343

344
        return $arguments[0];
2✔
345
    }
346

347
    /**
348
     * Extracts the key from BITOP command.
349
     *
350
     * @param CommandInterface $command Command instance.
351
     *
352
     * @return string|null
353
     */
354
    protected function getKeyFromBitOp(CommandInterface $command)
1✔
355
    {
356
        $arguments = $command->getArguments();
1✔
357

358
        if (!$this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
1✔
359
            return null;
×
360
        }
361

362
        return $arguments[1];
1✔
363
    }
364

365
    /**
366
     * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands.
367
     *
368
     * @param CommandInterface $command Command instance.
369
     *
370
     * @return string|null
371
     */
372
    protected function getKeyFromGeoradiusCommands(CommandInterface $command)
4✔
373
    {
374
        $arguments = $command->getArguments();
4✔
375
        $argc = count($arguments);
4✔
376
        $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
4✔
377

378
        if ($argc > $startIndex) {
4✔
379
            $keys = [$arguments[0]];
4✔
380

381
            for ($i = $startIndex; $i < $argc; ++$i) {
4✔
382
                $argument = strtoupper($arguments[$i]);
4✔
383
                if ($argument === 'STORE' || $argument === 'STOREDIST') {
4✔
384
                    $keys[] = $arguments[++$i];
4✔
385
                }
386
            }
387

388
            if (!$this->checkSameSlotForKeys($keys)) {
4✔
389
                return null;
×
390
            }
391
        }
392

393
        return $arguments[0];
4✔
394
    }
395

396
    /**
397
     * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
398
     *
399
     * @param CommandInterface $command Command instance.
400
     *
401
     * @return string|null
402
     */
403
    protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
1✔
404
    {
405
        $arguments = $command->getArguments();
1✔
406
        $keys = array_merge([$arguments[0]], array_slice($arguments, 2, $arguments[1]));
1✔
407

408
        if (!$this->checkSameSlotForKeys($keys)) {
1✔
409
            return null;
×
410
        }
411

412
        return $arguments[0];
1✔
413
    }
414

415
    /**
416
     * Extracts key from SUNSUBSCRIBE command if it's given.
417
     *
418
     * @param  CommandInterface $command
419
     * @return string
420
     */
421
    protected function getKeyFromSUnsubscribeCommand(CommandInterface $command): ?string
2✔
422
    {
423
        $arguments = $command->getArguments();
2✔
424

425
        // SUNSUBSCRIBE command could be called without arguments, so it doesn't matter on each node it will be called.
426
        if (empty($arguments)) {
2✔
427
            return 'fake';
2✔
428
        }
429

430
        return $this->getKeyFromAllArguments($command);
×
431
    }
432

433
    /**
434
     * Extracts the key from EVAL and EVALSHA commands.
435
     *
436
     * @param CommandInterface $command Command instance.
437
     *
438
     * @return string|null
439
     */
440
    protected function getKeyFromScriptingCommands(CommandInterface $command)
6✔
441
    {
442
        $keys = $command instanceof ScriptCommand
6✔
443
            ? $command->getKeys()
2✔
444
            : array_slice($args = $command->getArguments(), 2, $args[1]);
4✔
445

446
        if (!$keys || !$this->checkSameSlotForKeys($keys)) {
6✔
447
            return null;
×
448
        }
449

450
        return $keys[0];
6✔
451
    }
452

453
    /**
454
     * {@inheritdoc}
455
     */
456
    public function getSlot(CommandInterface $command)
88✔
457
    {
458
        $slot = $command->getSlot();
88✔
459

460
        if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) {
88✔
461
            $key = call_user_func($this->commands[$cmdID], $command);
82✔
462

463
            if (isset($key)) {
82✔
464
                $slot = $this->getSlotByKey($key);
79✔
465
                $command->setSlot($slot);
79✔
466
            }
467
        }
468

469
        return $slot;
88✔
470
    }
471

472
    /**
473
     * {@inheritdoc}
474
     */
475
    public function checkSameSlotForKeys(array $keys): bool
22✔
476
    {
477
        if (!$count = count($keys)) {
22✔
478
            return false;
×
479
        }
480

481
        $currentSlot = $this->getSlotByKey($keys[0]);
22✔
482

483
        for ($i = 1; $i < $count; ++$i) {
22✔
484
            $nextSlot = $this->getSlotByKey($keys[$i]);
10✔
485

486
            if ($currentSlot !== $nextSlot) {
10✔
487
                return false;
4✔
488
            }
489
        }
490

491
        return true;
18✔
492
    }
493

494
    /**
495
     * Returns only the hashable part of a key (delimited by "{...}"), or the
496
     * whole key if a key tag is not found in the string.
497
     *
498
     * @param string $key A key.
499
     *
500
     * @return string
501
     */
502
    protected function extractKeyTag($key)
86✔
503
    {
504
        if (false !== $start = strpos($key, '{')) {
86✔
505
            if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
27✔
506
                $key = substr($key, $start, $end - $start);
27✔
507
            }
508
        }
509

510
        return $key;
86✔
511
    }
512
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc