Passed
Pull Request — development (#3708)
by Martyn
15:25
created

Client   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 517
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 127
c 1
b 0
f 0
dl 0
loc 517
rs 2.88
wmc 69

28 Methods

Rating   Name   Duplication   Size   Complexity  
A createOptions() 0 8 3
A getClientBy() 0 18 4
A __set() 0 3 1
A getOptions() 0 3 1
A executeCommand() 0 13 3
A getConnection() 0 3 1
A createPubSub() 0 15 4
A getCommandFactory() 0 3 1
A onErrorResponse() 0 17 5
A __construct() 0 5 1
A transaction() 0 3 1
A executeRaw() 0 18 3
A __get() 0 3 1
A getIterator() 0 17 3
A createCommand() 0 3 1
A __isset() 0 3 1
A pipeline() 0 3 1
A createPipeline() 0 20 6
A __call() 0 4 1
A quit() 0 3 1
A isConnected() 0 3 1
C createConnection() 0 37 14
A connect() 0 3 1
A monitor() 0 3 1
A createTransaction() 0 9 2
A pubSubLoop() 0 3 1
A disconnect() 0 3 1
A sharedContextFactory() 0 18 5

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Predis package.
5
 *
6
 * (c) 2009-2020 Daniele Alessandri
7
 * (c) 2021-2023 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;
14
15
use ArrayIterator;
16
use InvalidArgumentException;
17
use IteratorAggregate;
18
use Predis\Command\CommandInterface;
19
use Predis\Command\RawCommand;
20
use Predis\Command\Redis\Container\ContainerFactory;
21
use Predis\Command\Redis\Container\ContainerInterface;
22
use Predis\Command\ScriptCommand;
23
use Predis\Configuration\Options;
24
use Predis\Configuration\OptionsInterface;
25
use Predis\Connection\ConnectionInterface;
26
use Predis\Connection\Parameters;
27
use Predis\Connection\ParametersInterface;
28
use Predis\Monitor\Consumer as MonitorConsumer;
29
use Predis\Pipeline\Pipeline;
30
use Predis\PubSub\Consumer as PubSubConsumer;
31
use Predis\Response\ErrorInterface as ErrorResponseInterface;
32
use Predis\Response\ResponseInterface;
33
use Predis\Response\ServerException;
34
use Predis\Transaction\MultiExec as MultiExecTransaction;
35
use ReturnTypeWillChange;
0 ignored issues
show
Bug introduced by
The type ReturnTypeWillChange was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
use RuntimeException;
37
use Traversable;
38
39
/**
40
 * Client class used for connecting and executing commands on Redis.
41
 *
42
 * This is the main high-level abstraction of Predis upon which various other
43
 * abstractions are built. Internally it aggregates various other classes each
44
 * one with its own responsibility and scope.
45
 *
46
 * @template-implements \IteratorAggregate<string, static>
47
 */
48
class Client implements ClientInterface, IteratorAggregate
49
{
50
    public const VERSION = '2.1.2';
51
52
    /** @var OptionsInterface */
53
    private $options;
54
55
    /** @var ConnectionInterface */
56
    private $connection;
57
58
    /** @var Command\FactoryInterface */
59
    private $commands;
60
61
    /**
62
     * @param mixed $parameters Connection parameters for one or more servers.
63
     * @param mixed $options    Options to configure some behaviours of the client.
64
     */
65
    public function __construct($parameters = null, $options = null)
66
    {
67
        $this->options = static::createOptions($options ?? new Options());
68
        $this->connection = static::createConnection($this->options, $parameters ?? new Parameters());
69
        $this->commands = $this->options->commands;
70
    }
71
72
    /**
73
     * Creates a new set of client options for the client.
74
     *
75
     * @param array|OptionsInterface $options Set of client options
76
     *
77
     * @return OptionsInterface
78
     * @throws InvalidArgumentException
79
     */
80
    protected static function createOptions($options)
81
    {
82
        if (is_array($options)) {
83
            return new Options($options);
84
        } elseif ($options instanceof OptionsInterface) {
0 ignored issues
show
introduced by
$options is always a sub-type of Predis\Configuration\OptionsInterface.
Loading history...
85
            return $options;
86
        } else {
87
            throw new InvalidArgumentException('Invalid type for client options');
88
        }
89
    }
90
91
    /**
92
     * Creates single or aggregate connections from supplied arguments.
93
     *
94
     * This method accepts the following types to create a connection instance:
95
     *
96
     *  - Array (dictionary: single connection, indexed: aggregate connections)
97
     *  - String (URI for a single connection)
98
     *  - Callable (connection initializer callback)
99
     *  - Instance of Predis\Connection\ParametersInterface (used as-is)
100
     *  - Instance of Predis\Connection\ConnectionInterface (returned as-is)
101
     *
102
     * When a callable is passed, it receives the original set of client options
103
     * and must return an instance of Predis\Connection\ConnectionInterface.
104
     *
105
     * Connections are created using the connection factory (in case of single
106
     * connections) or a specialized aggregate connection initializer (in case
107
     * of cluster and replication) retrieved from the supplied client options.
108
     *
109
     * @param OptionsInterface $options    Client options container
110
     * @param mixed            $parameters Connection parameters
111
     *
112
     * @return ConnectionInterface
113
     * @throws InvalidArgumentException
114
     */
115
    protected static function createConnection(OptionsInterface $options, $parameters)
116
    {
117
        if ($parameters instanceof ConnectionInterface) {
118
            return $parameters;
119
        }
120
121
        if ($parameters instanceof ParametersInterface || is_string($parameters)) {
122
            return $options->connections->create($parameters);
123
        }
124
125
        if (is_array($parameters)) {
126
            if (!isset($parameters[0])) {
127
                return $options->connections->create($parameters);
128
            } elseif ($options->defined('cluster') && $initializer = $options->cluster) {
129
                return $initializer($parameters, true);
130
            } elseif ($options->defined('replication') && $initializer = $options->replication) {
131
                return $initializer($parameters, true);
132
            } elseif ($options->defined('aggregate') && $initializer = $options->aggregate) {
133
                return $initializer($parameters, false);
134
            } else {
135
                throw new InvalidArgumentException(
136
                    'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option'
137
                );
138
            }
139
        }
140
141
        if (is_callable($parameters)) {
142
            $connection = call_user_func($parameters, $options);
143
144
            if (!$connection instanceof ConnectionInterface) {
145
                throw new InvalidArgumentException('Callable parameters must return a valid connection');
146
            }
147
148
            return $connection;
149
        }
150
151
        throw new InvalidArgumentException('Invalid type for connection parameters');
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function getCommandFactory()
158
    {
159
        return $this->commands;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function getOptions()
166
    {
167
        return $this->options;
168
    }
169
170
    /**
171
     * Creates a new client using a specific underlying connection.
172
     *
173
     * This method allows to create a new client instance by picking a specific
174
     * connection out of an aggregate one, with the same options of the original
175
     * client instance.
176
     *
177
     * The specified selector defines which logic to use to look for a suitable
178
     * connection by the specified value. Supported selectors are:
179
     *
180
     *   - `id`
181
     *   - `key`
182
     *   - `slot`
183
     *   - `command`
184
     *   - `alias`
185
     *   - `role`
186
     *
187
     * Internally the client relies on duck-typing and follows this convention:
188
     *
189
     *   $selector string => getConnectionBy$selector($value) method
190
     *
191
     * This means that support for specific selectors may vary depending on the
192
     * actual logic implemented by connection classes and there is no interface
193
     * binding a connection class to implement any of these.
194
     *
195
     * @param string $selector Type of selector.
196
     * @param mixed  $value    Value to be used by the selector.
197
     *
198
     * @return ClientInterface
199
     */
200
    public function getClientBy($selector, $value)
201
    {
202
        $selector = strtolower($selector);
203
204
        if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) {
205
            throw new InvalidArgumentException("Invalid selector type: `$selector`");
206
        }
207
208
        if (!method_exists($this->connection, $method = "getConnectionBy$selector")) {
209
            $class = get_class($this->connection);
210
            throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class");
211
        }
212
213
        if (!$connection = $this->connection->$method($value)) {
214
            throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`");
215
        }
216
217
        return new static($connection, $this->getOptions());
218
    }
219
220
    /**
221
     * Opens the underlying connection and connects to the server.
222
     */
223
    public function connect()
224
    {
225
        $this->connection->connect();
226
    }
227
228
    /**
229
     * Closes the underlying connection and disconnects from the server.
230
     */
231
    public function disconnect()
232
    {
233
        $this->connection->disconnect();
234
    }
235
236
    /**
237
     * Closes the underlying connection and disconnects from the server.
238
     *
239
     * This is the same as `Client::disconnect()` as it does not actually send
240
     * the `QUIT` command to Redis, but simply closes the connection.
241
     */
242
    public function quit()
243
    {
244
        $this->disconnect();
245
    }
246
247
    /**
248
     * Returns the current state of the underlying connection.
249
     *
250
     * @return bool
251
     */
252
    public function isConnected()
253
    {
254
        return $this->connection->isConnected();
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function getConnection()
261
    {
262
        return $this->connection;
263
    }
264
265
    /**
266
     * Executes a command without filtering its arguments, parsing the response,
267
     * applying any prefix to keys or throwing exceptions on Redis errors even
268
     * regardless of client options.
269
     *
270
     * It is possible to identify Redis error responses from normal responses
271
     * using the second optional argument which is populated by reference.
272
     *
273
     * @param array $arguments Command arguments as defined by the command signature.
274
     * @param bool  $error     Set to TRUE when Redis returned an error response.
275
     *
276
     * @return mixed
277
     */
278
    public function executeRaw(array $arguments, &$error = null)
279
    {
280
        $error = false;
281
        $commandID = array_shift($arguments);
282
283
        $response = $this->connection->executeCommand(
284
            new RawCommand($commandID, $arguments)
285
        );
286
287
        if ($response instanceof ResponseInterface) {
288
            if ($response instanceof ErrorResponseInterface) {
289
                $error = true;
290
            }
291
292
            return (string) $response;
293
        }
294
295
        return $response;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    public function __call($commandID, $arguments)
302
    {
303
        return $this->executeCommand(
304
            $this->createCommand($commandID, $arguments)
305
        );
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function createCommand($commandID, $arguments = [])
312
    {
313
        return $this->commands->create($commandID, $arguments);
314
    }
315
316
    /**
317
     * @param $name
318
     * @return ContainerInterface
319
     */
320
    public function __get($name)
321
    {
322
        return ContainerFactory::create($this, $name);
323
    }
324
325
    /**
326
     * @param $name
327
     * @param $value
328
     * @return mixed
329
     */
330
    public function __set($name, $value)
331
    {
332
        throw new RuntimeException('Not allowed');
333
    }
334
335
    /**
336
     * @param $name
337
     * @return mixed
338
     */
339
    public function __isset($name)
340
    {
341
        throw new RuntimeException('Not allowed');
342
    }
343
344
    /**
345
     * {@inheritdoc}
346
     */
347
    public function executeCommand(CommandInterface $command)
348
    {
349
        $response = $this->connection->executeCommand($command);
350
351
        if ($response instanceof ResponseInterface) {
352
            if ($response instanceof ErrorResponseInterface) {
353
                $response = $this->onErrorResponse($command, $response);
354
            }
355
356
            return $response;
357
        }
358
359
        return $command->parseResponse($response);
360
    }
361
362
    /**
363
     * Handles -ERR responses returned by Redis.
364
     *
365
     * @param CommandInterface       $command  Redis command that generated the error.
366
     * @param ErrorResponseInterface $response Instance of the error response.
367
     *
368
     * @return mixed
369
     * @throws ServerException
370
     */
371
    protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response)
372
    {
373
        if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
374
            $response = $this->executeCommand($command->getEvalCommand());
375
376
            if (!$response instanceof ResponseInterface) {
377
                $response = $command->parseResponse($response);
378
            }
379
380
            return $response;
381
        }
382
383
        if ($this->options->exceptions) {
384
            throw new ServerException($response->getMessage());
385
        }
386
387
        return $response;
388
    }
389
390
    /**
391
     * Executes the specified initializer method on `$this` by adjusting the
392
     * actual invocation depending on the arity (0, 1 or 2 arguments). This is
393
     * simply an utility method to create Redis contexts instances since they
394
     * follow a common initialization path.
395
     *
396
     * @param string $initializer Method name.
397
     * @param array  $argv        Arguments for the method.
398
     *
399
     * @return mixed
400
     */
401
    private function sharedContextFactory($initializer, $argv = null)
402
    {
403
        switch (count($argv)) {
0 ignored issues
show
Bug introduced by
It seems like $argv can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

403
        switch (count(/** @scrutinizer ignore-type */ $argv)) {
Loading history...
404
            case 0:
405
                return $this->$initializer();
406
407
            case 1:
408
                return is_array($argv[0])
409
                    ? $this->$initializer($argv[0])
410
                    : $this->$initializer(null, $argv[0]);
411
412
            case 2:
413
                [$arg0, $arg1] = $argv;
414
415
                return $this->$initializer($arg0, $arg1);
416
417
            default:
418
                return $this->$initializer($this, $argv);
419
        }
420
    }
421
422
    /**
423
     * Creates a new pipeline context and returns it, or returns the results of
424
     * a pipeline executed inside the optionally provided callable object.
425
     *
426
     * @param mixed ...$arguments Array of options, a callable for execution, or both.
427
     *
428
     * @return Pipeline|array
429
     */
430
    public function pipeline(...$arguments)
431
    {
432
        return $this->sharedContextFactory('createPipeline', func_get_args());
433
    }
434
435
    /**
436
     * Actual pipeline context initializer method.
437
     *
438
     * @param array $options  Options for the context.
439
     * @param mixed $callable Optional callable used to execute the context.
440
     *
441
     * @return Pipeline|array
442
     */
443
    protected function createPipeline(array $options = null, $callable = null)
444
    {
445
        if (isset($options['atomic']) && $options['atomic']) {
446
            $class = 'Predis\Pipeline\Atomic';
447
        } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
448
            $class = 'Predis\Pipeline\FireAndForget';
449
        } else {
450
            $class = 'Predis\Pipeline\Pipeline';
451
        }
452
453
        /*
454
         * @var ClientContextInterface
455
         */
456
        $pipeline = new $class($this);
457
458
        if (isset($callable)) {
459
            return $pipeline->execute($callable);
460
        }
461
462
        return $pipeline;
463
    }
464
465
    /**
466
     * Creates a new transaction context and returns it, or returns the results
467
     * of a transaction executed inside the optionally provided callable object.
468
     *
469
     * @param mixed ...$arguments Array of options, a callable for execution, or both.
470
     *
471
     * @return MultiExecTransaction|array
472
     */
473
    public function transaction(...$arguments)
474
    {
475
        return $this->sharedContextFactory('createTransaction', func_get_args());
476
    }
477
478
    /**
479
     * Actual transaction context initializer method.
480
     *
481
     * @param array $options  Options for the context.
482
     * @param mixed $callable Optional callable used to execute the context.
483
     *
484
     * @return MultiExecTransaction|array
485
     */
486
    protected function createTransaction(array $options = null, $callable = null)
487
    {
488
        $transaction = new MultiExecTransaction($this, $options);
489
490
        if (isset($callable)) {
491
            return $transaction->execute($callable);
492
        }
493
494
        return $transaction;
495
    }
496
497
    /**
498
     * Creates a new publish/subscribe context and returns it, or starts its loop
499
     * inside the optionally provided callable object.
500
     *
501
     * @param mixed ...$arguments Array of options, a callable for execution, or both.
502
     *
503
     * @return PubSubConsumer|null
504
     */
505
    public function pubSubLoop(...$arguments)
506
    {
507
        return $this->sharedContextFactory('createPubSub', func_get_args());
508
    }
509
510
    /**
511
     * Actual publish/subscribe context initializer method.
512
     *
513
     * @param array $options  Options for the context.
514
     * @param mixed $callable Optional callable used to execute the context.
515
     *
516
     * @return PubSubConsumer|null
517
     */
518
    protected function createPubSub(array $options = null, $callable = null)
519
    {
520
        $pubsub = new PubSubConsumer($this, $options);
521
522
        if (!isset($callable)) {
523
            return $pubsub;
524
        }
525
526
        foreach ($pubsub as $message) {
527
            if (call_user_func($callable, $pubsub, $message) === false) {
528
                $pubsub->stop();
529
            }
530
        }
531
532
        return null;
533
    }
534
535
    /**
536
     * Creates a new monitor consumer and returns it.
537
     *
538
     * @return MonitorConsumer
539
     */
540
    public function monitor()
541
    {
542
        return new MonitorConsumer($this);
543
    }
544
545
    /**
546
     * @return Traversable<string, static>
547
     */
548
    #[ReturnTypeWillChange]
549
    public function getIterator()
550
    {
551
        $clients = [];
552
        $connection = $this->getConnection();
553
554
        if (!$connection instanceof Traversable) {
555
            return new ArrayIterator([
556
                (string) $connection => new static($connection, $this->getOptions()),
557
            ]);
558
        }
559
560
        foreach ($connection as $node) {
561
            $clients[(string) $node] = new static($node, $this->getOptions());
562
        }
563
564
        return new ArrayIterator($clients);
565
    }
566
}
567