Predis to v2.2.2

This commit is contained in:
the-djmaze 2024-04-02 20:15:23 +02:00
parent f6b440adef
commit 84ffe1e552
259 changed files with 2407 additions and 9937 deletions

View file

@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
* (c) Daniele Alessandri <suppakilla@gmail.com>
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@ -11,21 +12,35 @@
namespace Predis;
use ArrayIterator;
use InvalidArgumentException;
use IteratorAggregate;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\Command\Redis\Container\ContainerFactory;
use Predis\Command\Redis\Container\ContainerInterface;
use Predis\Command\ScriptCommand;
use Predis\Configuration\Options;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\AggregateConnectionInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Connection\Parameters;
use Predis\Connection\ParametersInterface;
use Predis\Connection\RelayConnection;
use Predis\Monitor\Consumer as MonitorConsumer;
use Predis\Pipeline\Atomic;
use Predis\Pipeline\FireAndForget;
use Predis\Pipeline\Pipeline;
use Predis\Pipeline\RelayAtomic;
use Predis\Pipeline\RelayPipeline;
use Predis\PubSub\Consumer as PubSubConsumer;
use Predis\PubSub\RelayConsumer as RelayPubSubConsumer;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ResponseInterface;
use Predis\Response\ServerException;
use Predis\Transaction\MultiExec as MultiExecTransaction;
use ReturnTypeWillChange;
use RuntimeException;
use Traversable;
/**
* Client class used for connecting and executing commands on Redis.
@ -34,17 +49,20 @@ use Predis\Transaction\MultiExec as MultiExecTransaction;
* abstractions are built. Internally it aggregates various other classes each
* one with its own responsibility and scope.
*
* {@inheritdoc}
*
* @author Daniele Alessandri <suppakilla@gmail.com>
* @template-implements \IteratorAggregate<string, static>
*/
class Client implements ClientInterface
class Client implements ClientInterface, IteratorAggregate
{
const VERSION = '1.0.3';
public const VERSION = '2.2.2';
protected $connection;
protected $options;
private $profile;
/** @var OptionsInterface */
private $options;
/** @var ConnectionInterface */
private $connection;
/** @var Command\FactoryInterface */
private $commands;
/**
* @param mixed $parameters Connection parameters for one or more servers.
@ -52,126 +70,99 @@ class Client implements ClientInterface
*/
public function __construct($parameters = null, $options = null)
{
$this->options = $this->createOptions($options ?: array());
$this->connection = $this->createConnection($parameters ?: array());
$this->profile = $this->options->profile;
$this->options = static::createOptions($options ?? new Options());
$this->connection = static::createConnection($this->options, $parameters ?? new Parameters());
$this->commands = $this->options->commands;
}
/**
* Creates a new instance of Predis\Configuration\Options from different
* types of arguments or simply returns the passed argument if it is an
* instance of Predis\Configuration\OptionsInterface.
* Creates a new set of client options for the client.
*
* @param mixed $options Client options.
*
* @throws \InvalidArgumentException
* @param array|OptionsInterface $options Set of client options
*
* @return OptionsInterface
* @throws InvalidArgumentException
*/
protected function createOptions($options)
protected static function createOptions($options)
{
if (is_array($options)) {
return new Options($options);
}
if ($options instanceof OptionsInterface) {
} elseif ($options instanceof OptionsInterface) {
return $options;
} else {
throw new InvalidArgumentException('Invalid type for client options');
}
throw new \InvalidArgumentException('Invalid type for client options.');
}
/**
* Creates single or aggregate connections from different types of arguments
* (string, array) or returns the passed argument if it is an instance of a
* class implementing Predis\Connection\ConnectionInterface.
* Creates single or aggregate connections from supplied arguments.
*
* Accepted types for connection parameters are:
* This method accepts the following types to create a connection instance:
*
* - Instance of Predis\Connection\ConnectionInterface.
* - Instance of Predis\Connection\ParametersInterface.
* - Array
* - String
* - Callable
* - Array (dictionary: single connection, indexed: aggregate connections)
* - String (URI for a single connection)
* - Callable (connection initializer callback)
* - Instance of Predis\Connection\ParametersInterface (used as-is)
* - Instance of Predis\Connection\ConnectionInterface (returned as-is)
*
* @param mixed $parameters Connection parameters or connection instance.
* When a callable is passed, it receives the original set of client options
* and must return an instance of Predis\Connection\ConnectionInterface.
*
* @throws \InvalidArgumentException
* Connections are created using the connection factory (in case of single
* connections) or a specialized aggregate connection initializer (in case
* of cluster and replication) retrieved from the supplied client options.
*
* @param OptionsInterface $options Client options container
* @param mixed $parameters Connection parameters
*
* @return ConnectionInterface
* @throws InvalidArgumentException
*/
protected function createConnection($parameters)
protected static function createConnection(OptionsInterface $options, $parameters)
{
if ($parameters instanceof ConnectionInterface) {
return $parameters;
}
if ($parameters instanceof ParametersInterface || is_string($parameters)) {
return $this->options->connections->create($parameters);
return $options->connections->create($parameters);
}
if (is_array($parameters)) {
if (!isset($parameters[0])) {
return $this->options->connections->create($parameters);
}
$options = $this->options;
if ($options->defined('aggregate')) {
$initializer = $this->getConnectionInitializerWrapper($options->aggregate);
$connection = $initializer($parameters, $options);
return $options->connections->create($parameters);
} elseif ($options->defined('cluster') && $initializer = $options->cluster) {
return $initializer($parameters, true);
} elseif ($options->defined('replication') && $initializer = $options->replication) {
return $initializer($parameters, true);
} elseif ($options->defined('aggregate') && $initializer = $options->aggregate) {
return $initializer($parameters, false);
} else {
if ($options->defined('replication') && $replication = $options->replication) {
$connection = $replication;
} else {
$connection = $options->cluster;
}
$options->connections->aggregate($connection, $parameters);
throw new InvalidArgumentException(
'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option'
);
}
return $connection;
}
if (is_callable($parameters)) {
$initializer = $this->getConnectionInitializerWrapper($parameters);
$connection = $initializer($this->options);
$connection = call_user_func($parameters, $options);
if (!$connection instanceof ConnectionInterface) {
throw new InvalidArgumentException('Callable parameters must return a valid connection');
}
return $connection;
}
throw new \InvalidArgumentException('Invalid type for connection parameters.');
}
/**
* Wraps a callable to make sure that its returned value represents a valid
* connection type.
*
* @param mixed $callable
*
* @return \Closure
*/
protected function getConnectionInitializerWrapper($callable)
{
return function () use ($callable) {
$connection = call_user_func_array($callable, func_get_args());
if (!$connection instanceof ConnectionInterface) {
throw new \UnexpectedValueException(
'The callable connection initializer returned an invalid type.'
);
}
return $connection;
};
throw new InvalidArgumentException('Invalid type for connection parameters');
}
/**
* {@inheritdoc}
*/
public function getProfile()
public function getCommandFactory()
{
return $this->profile;
return $this->commands;
}
/**
@ -183,23 +174,53 @@ class Client implements ClientInterface
}
/**
* Creates a new client instance for the specified connection ID or alias,
* only when working with an aggregate connection (cluster, replication).
* The new client instances uses the same options of the original one.
* Creates a new client using a specific underlying connection.
*
* @param string $connectionID Identifier of a connection.
* This method allows to create a new client instance by picking a specific
* connection out of an aggregate one, with the same options of the original
* client instance.
*
* @throws \InvalidArgumentException
* The specified selector defines which logic to use to look for a suitable
* connection by the specified value. Supported selectors are:
*
* @return Client
* - `id`
* - `key`
* - `slot`
* - `command`
* - `alias`
* - `role`
*
* Internally the client relies on duck-typing and follows this convention:
*
* $selector string => getConnectionBy$selector($value) method
*
* This means that support for specific selectors may vary depending on the
* actual logic implemented by connection classes and there is no interface
* binding a connection class to implement any of these.
*
* @param string $selector Type of selector.
* @param mixed $value Value to be used by the selector.
*
* @return ClientInterface
*/
public function getClientFor($connectionID)
public function getClientBy($selector, $value)
{
if (!$connection = $this->getConnectionById($connectionID)) {
throw new \InvalidArgumentException("Invalid connection ID: $connectionID.");
$selector = strtolower($selector);
if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) {
throw new InvalidArgumentException("Invalid selector type: `$selector`");
}
return new static($connection, $this->options);
if (!method_exists($this->connection, $method = "getConnectionBy$selector")) {
$class = get_class($this->connection);
throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class");
}
if (!$connection = $this->connection->$method($value)) {
throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`");
}
return new static($connection, $this->getOptions());
}
/**
@ -248,24 +269,29 @@ class Client implements ClientInterface
}
/**
* Retrieves the specified connection from the aggregate connection when the
* client is in cluster or replication mode.
* Applies the configured serializer and compression to given value.
*
* @param string $connectionID Index or alias of the single connection.
*
* @throws NotSupportedException
*
* @return Connection\NodeConnectionInterface
* @param mixed $value
* @return string
*/
public function getConnectionById($connectionID)
public function pack($value)
{
if (!$this->connection instanceof AggregateConnectionInterface) {
throw new NotSupportedException(
'Retrieving connections by ID is supported only by aggregate connections.'
);
}
return $this->connection instanceof RelayConnection
? $this->connection->pack($value)
: $value;
}
return $this->connection->getConnectionById($connectionID);
/**
* Deserializes and decompresses to given value.
*
* @param mixed $value
* @return string
*/
public function unpack($value)
{
return $this->connection instanceof RelayConnection
? $this->connection->unpack($value)
: $value;
}
/**
@ -273,7 +299,7 @@ class Client implements ClientInterface
* applying any prefix to keys or throwing exceptions on Redis errors even
* regardless of client options.
*
* It is possibile to indentify Redis error responses from normal responses
* It is possible to identify Redis error responses from normal responses
* using the second optional argument which is populated by reference.
*
* @param array $arguments Command arguments as defined by the command signature.
@ -284,9 +310,10 @@ class Client implements ClientInterface
public function executeRaw(array $arguments, &$error = null)
{
$error = false;
$commandID = array_shift($arguments);
$response = $this->connection->executeCommand(
new RawCommand($arguments)
new RawCommand($commandID, $arguments)
);
if ($response instanceof ResponseInterface) {
@ -313,9 +340,37 @@ class Client implements ClientInterface
/**
* {@inheritdoc}
*/
public function createCommand($commandID, $arguments = array())
public function createCommand($commandID, $arguments = [])
{
return $this->profile->createCommand($commandID, $arguments);
return $this->commands->create($commandID, $arguments);
}
/**
* @param string $name
* @return ContainerInterface
*/
public function __get(string $name)
{
return ContainerFactory::create($this, $name);
}
/**
* @param string $name
* @param mixed $value
* @return mixed
*/
public function __set(string $name, $value)
{
throw new RuntimeException('Not allowed');
}
/**
* @param string $name
* @return mixed
*/
public function __isset(string $name)
{
throw new RuntimeException('Not allowed');
}
/**
@ -342,17 +397,13 @@ class Client implements ClientInterface
* @param CommandInterface $command Redis command that generated the error.
* @param ErrorResponseInterface $response Instance of the error response.
*
* @throws ServerException
*
* @return mixed
* @throws ServerException
*/
protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response)
{
if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
$eval = $this->createCommand('EVAL');
$eval->setRawArguments($command->getEvalArguments());
$response = $this->executeCommand($eval);
$response = $this->executeCommand($command->getEvalCommand());
if (!$response instanceof ResponseInterface) {
$response = $command->parseResponse($response);
@ -370,7 +421,7 @@ class Client implements ClientInterface
/**
* Executes the specified initializer method on `$this` by adjusting the
* actual invokation depending on the arity (0, 1 or 2 arguments). This is
* actual invocation depending on the arity (0, 1 or 2 arguments). This is
* simply an utility method to create Redis contexts instances since they
* follow a common initialization path.
*
@ -391,7 +442,7 @@ class Client implements ClientInterface
: $this->$initializer(null, $argv[0]);
case 2:
list($arg0, $arg1) = $argv;
[$arg0, $arg1] = $argv;
return $this->$initializer($arg0, $arg1);
@ -404,11 +455,11 @@ class Client implements ClientInterface
* Creates a new pipeline context and returns it, or returns the results of
* a pipeline executed inside the optionally provided callable object.
*
* @param mixed ... Array of options, a callable for execution, or both.
* @param mixed ...$arguments Array of options, a callable for execution, or both.
*
* @return Pipeline|array
*/
public function pipeline(/* arguments */)
public function pipeline(...$arguments)
{
return $this->sharedContextFactory('createPipeline', func_get_args());
}
@ -416,19 +467,29 @@ class Client implements ClientInterface
/**
* Actual pipeline context initializer method.
*
* @param array $options Options for the context.
* @param mixed $callable Optional callable used to execute the context.
* @param array|null $options Options for the context.
* @param mixed $callable Optional callable used to execute the context.
*
* @return Pipeline|array
*/
protected function createPipeline(array $options = null, $callable = null)
{
if (isset($options['atomic']) && $options['atomic']) {
$class = 'Predis\Pipeline\Atomic';
$class = Atomic::class;
} elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
$class = 'Predis\Pipeline\FireAndForget';
$class = FireAndForget::class;
} else {
$class = 'Predis\Pipeline\Pipeline';
$class = Pipeline::class;
}
if ($this->connection instanceof RelayConnection) {
if (isset($options['atomic']) && $options['atomic']) {
$class = RelayAtomic::class;
} elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
throw new NotSupportedException('The "relay" extension does not support fire-and-forget pipelines.');
} else {
$class = RelayPipeline::class;
}
}
/*
@ -447,11 +508,11 @@ class Client implements ClientInterface
* Creates a new transaction context and returns it, or returns the results
* of a transaction executed inside the optionally provided callable object.
*
* @param mixed ... Array of options, a callable for execution, or both.
* @param mixed ...$arguments Array of options, a callable for execution, or both.
*
* @return MultiExecTransaction|array
*/
public function transaction(/* arguments */)
public function transaction(...$arguments)
{
return $this->sharedContextFactory('createTransaction', func_get_args());
}
@ -476,14 +537,14 @@ class Client implements ClientInterface
}
/**
* Creates a new publis/subscribe context and returns it, or starts its loop
* Creates a new publish/subscribe context and returns it, or starts its loop
* inside the optionally provided callable object.
*
* @param mixed ... Array of options, a callable for execution, or both.
* @param mixed ...$arguments Array of options, a callable for execution, or both.
*
* @return PubSubConsumer|null
*/
public function pubSubLoop(/* arguments */)
public function pubSubLoop(...$arguments)
{
return $this->sharedContextFactory('createPubSub', func_get_args());
}
@ -498,7 +559,11 @@ class Client implements ClientInterface
*/
protected function createPubSub(array $options = null, $callable = null)
{
$pubsub = new PubSubConsumer($this, $options);
if ($this->connection instanceof RelayConnection) {
$pubsub = new RelayPubSubConsumer($this, $options);
} else {
$pubsub = new PubSubConsumer($this, $options);
}
if (!isset($callable)) {
return $pubsub;
@ -509,6 +574,8 @@ class Client implements ClientInterface
$pubsub->stop();
}
}
return null;
}
/**
@ -520,4 +587,26 @@ class Client implements ClientInterface
{
return new MonitorConsumer($this);
}
/**
* @return Traversable<string, static>
*/
#[ReturnTypeWillChange]
public function getIterator()
{
$clients = [];
$connection = $this->getConnection();
if (!$connection instanceof Traversable) {
return new ArrayIterator([
(string) $connection => new static($connection, $this->getOptions()),
]);
}
foreach ($connection as $node) {
$clients[(string) $node] = new static($node, $this->getOptions());
}
return new ArrayIterator($clients);
}
}