vendor/predis/predis/src/Client.php line 336

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Predis package.
  4.  *
  5.  * (c) 2009-2020 Daniele Alessandri
  6.  * (c) 2021-2023 Till Krüss
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Predis;
  12. use ArrayIterator;
  13. use InvalidArgumentException;
  14. use IteratorAggregate;
  15. use Predis\Command\CommandInterface;
  16. use Predis\Command\RawCommand;
  17. use Predis\Command\Redis\Container\ContainerFactory;
  18. use Predis\Command\Redis\Container\ContainerInterface;
  19. use Predis\Command\ScriptCommand;
  20. use Predis\Configuration\Options;
  21. use Predis\Configuration\OptionsInterface;
  22. use Predis\Connection\ConnectionInterface;
  23. use Predis\Connection\Parameters;
  24. use Predis\Connection\ParametersInterface;
  25. use Predis\Connection\RelayConnection;
  26. use Predis\Monitor\Consumer as MonitorConsumer;
  27. use Predis\Pipeline\Atomic;
  28. use Predis\Pipeline\FireAndForget;
  29. use Predis\Pipeline\Pipeline;
  30. use Predis\Pipeline\RelayAtomic;
  31. use Predis\Pipeline\RelayPipeline;
  32. use Predis\PubSub\Consumer as PubSubConsumer;
  33. use Predis\PubSub\RelayConsumer as RelayPubSubConsumer;
  34. use Predis\Response\ErrorInterface as ErrorResponseInterface;
  35. use Predis\Response\ResponseInterface;
  36. use Predis\Response\ServerException;
  37. use Predis\Transaction\MultiExec as MultiExecTransaction;
  38. use ReturnTypeWillChange;
  39. use RuntimeException;
  40. use Traversable;
  41. /**
  42.  * Client class used for connecting and executing commands on Redis.
  43.  *
  44.  * This is the main high-level abstraction of Predis upon which various other
  45.  * abstractions are built. Internally it aggregates various other classes each
  46.  * one with its own responsibility and scope.
  47.  *
  48.  * @template-implements \IteratorAggregate<string, static>
  49.  */
  50. class Client implements ClientInterfaceIteratorAggregate
  51. {
  52.     public const VERSION '2.2.2';
  53.     /** @var OptionsInterface */
  54.     private $options;
  55.     /** @var ConnectionInterface */
  56.     private $connection;
  57.     /** @var Command\FactoryInterface */
  58.     private $commands;
  59.     /**
  60.      * @param mixed $parameters Connection parameters for one or more servers.
  61.      * @param mixed $options    Options to configure some behaviours of the client.
  62.      */
  63.     public function __construct($parameters null$options null)
  64.     {
  65.         $this->options = static::createOptions($options ?? new Options());
  66.         $this->connection = static::createConnection($this->options$parameters ?? new Parameters());
  67.         $this->commands $this->options->commands;
  68.     }
  69.     /**
  70.      * Creates a new set of client options for the client.
  71.      *
  72.      * @param array|OptionsInterface $options Set of client options
  73.      *
  74.      * @return OptionsInterface
  75.      * @throws InvalidArgumentException
  76.      */
  77.     protected static function createOptions($options)
  78.     {
  79.         if (is_array($options)) {
  80.             return new Options($options);
  81.         } elseif ($options instanceof OptionsInterface) {
  82.             return $options;
  83.         } else {
  84.             throw new InvalidArgumentException('Invalid type for client options');
  85.         }
  86.     }
  87.     /**
  88.      * Creates single or aggregate connections from supplied arguments.
  89.      *
  90.      * This method accepts the following types to create a connection instance:
  91.      *
  92.      *  - Array (dictionary: single connection, indexed: aggregate connections)
  93.      *  - String (URI for a single connection)
  94.      *  - Callable (connection initializer callback)
  95.      *  - Instance of Predis\Connection\ParametersInterface (used as-is)
  96.      *  - Instance of Predis\Connection\ConnectionInterface (returned as-is)
  97.      *
  98.      * When a callable is passed, it receives the original set of client options
  99.      * and must return an instance of Predis\Connection\ConnectionInterface.
  100.      *
  101.      * Connections are created using the connection factory (in case of single
  102.      * connections) or a specialized aggregate connection initializer (in case
  103.      * of cluster and replication) retrieved from the supplied client options.
  104.      *
  105.      * @param OptionsInterface $options    Client options container
  106.      * @param mixed            $parameters Connection parameters
  107.      *
  108.      * @return ConnectionInterface
  109.      * @throws InvalidArgumentException
  110.      */
  111.     protected static function createConnection(OptionsInterface $options$parameters)
  112.     {
  113.         if ($parameters instanceof ConnectionInterface) {
  114.             return $parameters;
  115.         }
  116.         if ($parameters instanceof ParametersInterface || is_string($parameters)) {
  117.             return $options->connections->create($parameters);
  118.         }
  119.         if (is_array($parameters)) {
  120.             if (!isset($parameters[0])) {
  121.                 return $options->connections->create($parameters);
  122.             } elseif ($options->defined('cluster') && $initializer $options->cluster) {
  123.                 return $initializer($parameterstrue);
  124.             } elseif ($options->defined('replication') && $initializer $options->replication) {
  125.                 return $initializer($parameterstrue);
  126.             } elseif ($options->defined('aggregate') && $initializer $options->aggregate) {
  127.                 return $initializer($parametersfalse);
  128.             } else {
  129.                 throw new InvalidArgumentException(
  130.                     'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option'
  131.                 );
  132.             }
  133.         }
  134.         if (is_callable($parameters)) {
  135.             $connection call_user_func($parameters$options);
  136.             if (!$connection instanceof ConnectionInterface) {
  137.                 throw new InvalidArgumentException('Callable parameters must return a valid connection');
  138.             }
  139.             return $connection;
  140.         }
  141.         throw new InvalidArgumentException('Invalid type for connection parameters');
  142.     }
  143.     /**
  144.      * {@inheritdoc}
  145.      */
  146.     public function getCommandFactory()
  147.     {
  148.         return $this->commands;
  149.     }
  150.     /**
  151.      * {@inheritdoc}
  152.      */
  153.     public function getOptions()
  154.     {
  155.         return $this->options;
  156.     }
  157.     /**
  158.      * Creates a new client using a specific underlying connection.
  159.      *
  160.      * This method allows to create a new client instance by picking a specific
  161.      * connection out of an aggregate one, with the same options of the original
  162.      * client instance.
  163.      *
  164.      * The specified selector defines which logic to use to look for a suitable
  165.      * connection by the specified value. Supported selectors are:
  166.      *
  167.      *   - `id`
  168.      *   - `key`
  169.      *   - `slot`
  170.      *   - `command`
  171.      *   - `alias`
  172.      *   - `role`
  173.      *
  174.      * Internally the client relies on duck-typing and follows this convention:
  175.      *
  176.      *   $selector string => getConnectionBy$selector($value) method
  177.      *
  178.      * This means that support for specific selectors may vary depending on the
  179.      * actual logic implemented by connection classes and there is no interface
  180.      * binding a connection class to implement any of these.
  181.      *
  182.      * @param string $selector Type of selector.
  183.      * @param mixed  $value    Value to be used by the selector.
  184.      *
  185.      * @return ClientInterface
  186.      */
  187.     public function getClientBy($selector$value)
  188.     {
  189.         $selector strtolower($selector);
  190.         if (!in_array($selector, ['id''key''slot''role''alias''command'])) {
  191.             throw new InvalidArgumentException("Invalid selector type: `$selector`");
  192.         }
  193.         if (!method_exists($this->connection$method "getConnectionBy$selector")) {
  194.             $class get_class($this->connection);
  195.             throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class");
  196.         }
  197.         if (!$connection $this->connection->$method($value)) {
  198.             throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`");
  199.         }
  200.         return new static($connection$this->getOptions());
  201.     }
  202.     /**
  203.      * Opens the underlying connection and connects to the server.
  204.      */
  205.     public function connect()
  206.     {
  207.         $this->connection->connect();
  208.     }
  209.     /**
  210.      * Closes the underlying connection and disconnects from the server.
  211.      */
  212.     public function disconnect()
  213.     {
  214.         $this->connection->disconnect();
  215.     }
  216.     /**
  217.      * Closes the underlying connection and disconnects from the server.
  218.      *
  219.      * This is the same as `Client::disconnect()` as it does not actually send
  220.      * the `QUIT` command to Redis, but simply closes the connection.
  221.      */
  222.     public function quit()
  223.     {
  224.         $this->disconnect();
  225.     }
  226.     /**
  227.      * Returns the current state of the underlying connection.
  228.      *
  229.      * @return bool
  230.      */
  231.     public function isConnected()
  232.     {
  233.         return $this->connection->isConnected();
  234.     }
  235.     /**
  236.      * {@inheritdoc}
  237.      */
  238.     public function getConnection()
  239.     {
  240.         return $this->connection;
  241.     }
  242.     /**
  243.      * Applies the configured serializer and compression to given value.
  244.      *
  245.      * @param  mixed  $value
  246.      * @return string
  247.      */
  248.     public function pack($value)
  249.     {
  250.         return $this->connection instanceof RelayConnection
  251.             $this->connection->pack($value)
  252.             : $value;
  253.     }
  254.     /**
  255.      * Deserializes and decompresses to given value.
  256.      *
  257.      * @param  mixed  $value
  258.      * @return string
  259.      */
  260.     public function unpack($value)
  261.     {
  262.         return $this->connection instanceof RelayConnection
  263.             $this->connection->unpack($value)
  264.             : $value;
  265.     }
  266.     /**
  267.      * Executes a command without filtering its arguments, parsing the response,
  268.      * applying any prefix to keys or throwing exceptions on Redis errors even
  269.      * regardless of client options.
  270.      *
  271.      * It is possible to identify Redis error responses from normal responses
  272.      * using the second optional argument which is populated by reference.
  273.      *
  274.      * @param array $arguments Command arguments as defined by the command signature.
  275.      * @param bool  $error     Set to TRUE when Redis returned an error response.
  276.      *
  277.      * @return mixed
  278.      */
  279.     public function executeRaw(array $arguments, &$error null)
  280.     {
  281.         $error false;
  282.         $commandID array_shift($arguments);
  283.         $response $this->connection->executeCommand(
  284.             new RawCommand($commandID$arguments)
  285.         );
  286.         if ($response instanceof ResponseInterface) {
  287.             if ($response instanceof ErrorResponseInterface) {
  288.                 $error true;
  289.             }
  290.             return (string) $response;
  291.         }
  292.         return $response;
  293.     }
  294.     /**
  295.      * {@inheritdoc}
  296.      */
  297.     public function __call($commandID$arguments)
  298.     {
  299.         return $this->executeCommand(
  300.             $this->createCommand($commandID$arguments)
  301.         );
  302.     }
  303.     /**
  304.      * {@inheritdoc}
  305.      */
  306.     public function createCommand($commandID$arguments = [])
  307.     {
  308.         return $this->commands->create($commandID$arguments);
  309.     }
  310.     /**
  311.      * @param  string             $name
  312.      * @return ContainerInterface
  313.      */
  314.     public function __get(string $name)
  315.     {
  316.         return ContainerFactory::create($this$name);
  317.     }
  318.     /**
  319.      * @param  string $name
  320.      * @param  mixed  $value
  321.      * @return mixed
  322.      */
  323.     public function __set(string $name$value)
  324.     {
  325.         throw new RuntimeException('Not allowed');
  326.     }
  327.     /**
  328.      * @param  string $name
  329.      * @return mixed
  330.      */
  331.     public function __isset(string $name)
  332.     {
  333.         throw new RuntimeException('Not allowed');
  334.     }
  335.     /**
  336.      * {@inheritdoc}
  337.      */
  338.     public function executeCommand(CommandInterface $command)
  339.     {
  340.         $response $this->connection->executeCommand($command);
  341.         if ($response instanceof ResponseInterface) {
  342.             if ($response instanceof ErrorResponseInterface) {
  343.                 $response $this->onErrorResponse($command$response);
  344.             }
  345.             return $response;
  346.         }
  347.         return $command->parseResponse($response);
  348.     }
  349.     /**
  350.      * Handles -ERR responses returned by Redis.
  351.      *
  352.      * @param CommandInterface       $command  Redis command that generated the error.
  353.      * @param ErrorResponseInterface $response Instance of the error response.
  354.      *
  355.      * @return mixed
  356.      * @throws ServerException
  357.      */
  358.     protected function onErrorResponse(CommandInterface $commandErrorResponseInterface $response)
  359.     {
  360.         if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
  361.             $response $this->executeCommand($command->getEvalCommand());
  362.             if (!$response instanceof ResponseInterface) {
  363.                 $response $command->parseResponse($response);
  364.             }
  365.             return $response;
  366.         }
  367.         if ($this->options->exceptions) {
  368.             throw new ServerException($response->getMessage());
  369.         }
  370.         return $response;
  371.     }
  372.     /**
  373.      * Executes the specified initializer method on `$this` by adjusting the
  374.      * actual invocation depending on the arity (0, 1 or 2 arguments). This is
  375.      * simply an utility method to create Redis contexts instances since they
  376.      * follow a common initialization path.
  377.      *
  378.      * @param string $initializer Method name.
  379.      * @param array  $argv        Arguments for the method.
  380.      *
  381.      * @return mixed
  382.      */
  383.     private function sharedContextFactory($initializer$argv null)
  384.     {
  385.         switch (count($argv)) {
  386.             case 0:
  387.                 return $this->$initializer();
  388.             case 1:
  389.                 return is_array($argv[0])
  390.                     ? $this->$initializer($argv[0])
  391.                     : $this->$initializer(null$argv[0]);
  392.             case 2:
  393.                 [$arg0$arg1] = $argv;
  394.                 return $this->$initializer($arg0$arg1);
  395.             default:
  396.                 return $this->$initializer($this$argv);
  397.         }
  398.     }
  399.     /**
  400.      * Creates a new pipeline context and returns it, or returns the results of
  401.      * a pipeline executed inside the optionally provided callable object.
  402.      *
  403.      * @param mixed ...$arguments Array of options, a callable for execution, or both.
  404.      *
  405.      * @return Pipeline|array
  406.      */
  407.     public function pipeline(...$arguments)
  408.     {
  409.         return $this->sharedContextFactory('createPipeline'func_get_args());
  410.     }
  411.     /**
  412.      * Actual pipeline context initializer method.
  413.      *
  414.      * @param array|null $options  Options for the context.
  415.      * @param mixed      $callable Optional callable used to execute the context.
  416.      *
  417.      * @return Pipeline|array
  418.      */
  419.     protected function createPipeline(array $options null$callable null)
  420.     {
  421.         if (isset($options['atomic']) && $options['atomic']) {
  422.             $class Atomic::class;
  423.         } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
  424.             $class FireAndForget::class;
  425.         } else {
  426.             $class Pipeline::class;
  427.         }
  428.         if ($this->connection instanceof RelayConnection) {
  429.             if (isset($options['atomic']) && $options['atomic']) {
  430.                 $class RelayAtomic::class;
  431.             } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
  432.                 throw new NotSupportedException('The "relay" extension does not support fire-and-forget pipelines.');
  433.             } else {
  434.                 $class RelayPipeline::class;
  435.             }
  436.         }
  437.         /*
  438.          * @var ClientContextInterface
  439.          */
  440.         $pipeline = new $class($this);
  441.         if (isset($callable)) {
  442.             return $pipeline->execute($callable);
  443.         }
  444.         return $pipeline;
  445.     }
  446.     /**
  447.      * Creates a new transaction context and returns it, or returns the results
  448.      * of a transaction executed inside the optionally provided callable object.
  449.      *
  450.      * @param mixed ...$arguments Array of options, a callable for execution, or both.
  451.      *
  452.      * @return MultiExecTransaction|array
  453.      */
  454.     public function transaction(...$arguments)
  455.     {
  456.         return $this->sharedContextFactory('createTransaction'func_get_args());
  457.     }
  458.     /**
  459.      * Actual transaction context initializer method.
  460.      *
  461.      * @param array $options  Options for the context.
  462.      * @param mixed $callable Optional callable used to execute the context.
  463.      *
  464.      * @return MultiExecTransaction|array
  465.      */
  466.     protected function createTransaction(array $options null$callable null)
  467.     {
  468.         $transaction = new MultiExecTransaction($this$options);
  469.         if (isset($callable)) {
  470.             return $transaction->execute($callable);
  471.         }
  472.         return $transaction;
  473.     }
  474.     /**
  475.      * Creates a new publish/subscribe context and returns it, or starts its loop
  476.      * inside the optionally provided callable object.
  477.      *
  478.      * @param mixed ...$arguments Array of options, a callable for execution, or both.
  479.      *
  480.      * @return PubSubConsumer|null
  481.      */
  482.     public function pubSubLoop(...$arguments)
  483.     {
  484.         return $this->sharedContextFactory('createPubSub'func_get_args());
  485.     }
  486.     /**
  487.      * Actual publish/subscribe context initializer method.
  488.      *
  489.      * @param array $options  Options for the context.
  490.      * @param mixed $callable Optional callable used to execute the context.
  491.      *
  492.      * @return PubSubConsumer|null
  493.      */
  494.     protected function createPubSub(array $options null$callable null)
  495.     {
  496.         if ($this->connection instanceof RelayConnection) {
  497.             $pubsub = new RelayPubSubConsumer($this$options);
  498.         } else {
  499.             $pubsub = new PubSubConsumer($this$options);
  500.         }
  501.         if (!isset($callable)) {
  502.             return $pubsub;
  503.         }
  504.         foreach ($pubsub as $message) {
  505.             if (call_user_func($callable$pubsub$message) === false) {
  506.                 $pubsub->stop();
  507.             }
  508.         }
  509.         return null;
  510.     }
  511.     /**
  512.      * Creates a new monitor consumer and returns it.
  513.      *
  514.      * @return MonitorConsumer
  515.      */
  516.     public function monitor()
  517.     {
  518.         return new MonitorConsumer($this);
  519.     }
  520.     /**
  521.      * @return Traversable<string, static>
  522.      */
  523.     #[ReturnTypeWillChange]
  524.     public function getIterator()
  525.     {
  526.         $clients = [];
  527.         $connection $this->getConnection();
  528.         if (!$connection instanceof Traversable) {
  529.             return new ArrayIterator([
  530.                 (string) $connection => new static($connection$this->getOptions()),
  531.             ]);
  532.         }
  533.         foreach ($connection as $node) {
  534.             $clients[(string) $node] = new static($node$this->getOptions());
  535.         }
  536.         return new ArrayIterator($clients);
  537.     }
  538. }