diff --git a/src/Client.php b/src/Client.php index a8eff158..a2453e19 100644 --- a/src/Client.php +++ b/src/Client.php @@ -18,7 +18,6 @@ use Predis\Configuration\Options; use Predis\Configuration\OptionsInterface; use Predis\Connection\ConnectionInterface; use Predis\Connection\ParametersInterface; -use Predis\Connection\Replication\SentinelReplication; use Predis\Monitor\Consumer as MonitorConsumer; use Predis\Pipeline\Pipeline; use Predis\PubSub\Consumer as PubSubConsumer; @@ -126,14 +125,12 @@ class Client implements ClientInterface, \IteratorAggregate if (is_array($parameters)) { if (!isset($parameters[0])) { return $options->connections->create($parameters); - } - - if ($options->defined('cluster')) { - return $this->createAggregateConnection($parameters, 'cluster'); - } elseif ($options->defined('replication')) { - return $this->createAggregateConnection($parameters, 'replication'); - } elseif ($options->defined('aggregate')) { - return $this->createAggregateConnection($parameters, 'aggregate'); + } 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 { throw new \InvalidArgumentException( 'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option' @@ -154,29 +151,6 @@ class Client implements ClientInterface, \IteratorAggregate throw new \InvalidArgumentException('Invalid type for connection parameters'); } - /** - * Creates an aggregate connection. - * - * @param mixed $parameters Connection parameters. - * @param string $option Option for aggregate connections (`aggregate`, `cluster`, `replication`). - * - * @return \Closure - */ - protected function createAggregateConnection($parameters, $option) - { - $options = $this->getOptions(); - - $initializer = $options->$option; - $connection = $initializer($parameters); - - // TODO: this is dirty but we must skip the redis-sentinel backend for now. - if ($option !== 'aggregate' && !$connection instanceof SentinelReplication) { - $options->connections->aggregate($connection, $parameters); - } - - return $connection; - } - /** * {@inheritdoc} */ diff --git a/src/Configuration/Option/Aggregate.php b/src/Configuration/Option/Aggregate.php index 6b816ed1..b363c5f2 100644 --- a/src/Configuration/Option/Aggregate.php +++ b/src/Configuration/Option/Aggregate.php @@ -11,44 +11,79 @@ namespace Predis\Configuration\Option; +use InvalidArgumentException; use Predis\Configuration\OptionInterface; use Predis\Configuration\OptionsInterface; use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\NodeConnectionInterface; /** - * Configures an aggregate connection used for clustering - * multiple Redis nodes using various implementations with - * different algorithms or strategies. + * Client option for configuring generic aggregate connections. + * + * The only value accepted by this option is a callable that must return a valid + * connection instance of Predis\Connection\AggregateConnectionInterface when + * invoked by the client to create a new aggregate connection instance. + * + * Creation and configuration of the aggregate connection is up to the user. * * @author Daniele Alessandri */ class Aggregate implements OptionInterface { /** - * Wraps a callable to ensure that the returned value is a valid connection. - * - * @param OptionsInterface $options Client options. - * @param mixed $callable Callable initializer. - * - * @return \Closure + * {@inheritdoc} */ - protected function getConnectionInitializer(OptionsInterface $options, $callable) + public function filter(OptionsInterface $options, $value) { - if (!is_callable($callable)) { - $class = get_called_class(); - - throw new \InvalidArgumentException("$class expects a valid callable"); + if (!is_callable($value)) { + throw new InvalidArgumentException(sprintf( + '%s expects a callable object acting as an aggregate connection initializer', + static::class + )); } - $option = $this; + return $this->getConnectionInitializer($options, $value); + } - return function ($parameters = null) use ($callable, $options, $option) { - $connection = call_user_func($callable, $options, $parameters); + /** + * Wraps a user-supplied callable used to create a new aggregate connection. + * + * When the original callable acting as a connection initializer is executed + * by the client to create a new aggregate connection, it will receive the + * following arguments: + * + * - $parameters (same as passed to Predis\Client::__construct()) + * - $options (options container, Predis\Configuration\OptionsInterface) + * - $option (current option, Predis\Configuration\OptionInterface) + * + * The original callable must return a valid aggregation connection instance + * of type Predis\Connection\AggregateConnectionInterface, this is enforced + * by the wrapper returned by this method and an exception is thrown when + * invalid values are returned. + * + * @param OptionsInterface $options Client options + * @param callable $callable Callable initializer + * + * @throws InvalidArgumentException + * + * @return callable + */ + protected function getConnectionInitializer(OptionsInterface $options, callable $callable) + { + return function ($parameters = null, $autoaggregate = false) use ($callable, $options) { + $connection = call_user_func_array($callable, [&$parameters, $options, $this]); if (!$connection instanceof AggregateConnectionInterface) { - $class = get_class($option); + throw new InvalidArgumentException(sprintf( + '%s expects the supplied callable to return an instance of %s, but %s was returned', + static::class, + AggregateConnectionInterface::class, + is_object($connection) ? get_class($connection) : gettype($connection) + )); + } - throw new \InvalidArgumentException("$class expects a valid connection type returned by callable initializer"); + if ($parameters && $autoaggregate) { + static::aggregate($options, $connection, $parameters); } return $connection; @@ -56,11 +91,19 @@ class Aggregate implements OptionInterface } /** - * {@inheritdoc} + * Adds single connections to an aggregate connection instance. + * + * @param OptionsInterface $options Client options + * @param AggregateConnectionInterface $connection Target aggregate connection + * @param array $nodes List of nodes to be added to the target aggregate connection */ - public function filter(OptionsInterface $options, $value) + public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes) { - return $this->getConnectionInitializer($options, $value); + $connections = $options->connections; + + foreach ($nodes as $node) { + $connection->add($node instanceof NodeConnectionInterface ? $node : $connections->create($node)); + } } /** diff --git a/src/Configuration/Option/Cluster.php b/src/Configuration/Option/Cluster.php index f4467b74..7af53e3a 100644 --- a/src/Configuration/Option/Cluster.php +++ b/src/Configuration/Option/Cluster.php @@ -11,6 +11,7 @@ namespace Predis\Configuration\Option; +use InvalidArgumentException; use Predis\Cluster\RedisStrategy; use Predis\Configuration\OptionsInterface; use Predis\Connection\Cluster\PredisCluster; @@ -25,41 +26,65 @@ use Predis\Connection\Cluster\RedisCluster; */ class Cluster extends Aggregate { - /** - * Returns a connection initializer from a descriptive name. - * - * @param OptionsInterface $options Client options. - * @param string $description Identifier of a cluster backend (`predis`, `redis`) - * - * @return callable - */ - protected function getConnectionInitializerByDescription(OptionsInterface $options, $description) - { - if ($description === 'predis') { - $callback = $this->getDefault($options); - } elseif ($description === 'redis') { - $callback = function ($options) { - return new RedisCluster($options->connections, new RedisStrategy($options->crc16)); - }; - } else { - throw new \InvalidArgumentException( - 'String value for the cluster option must be either `predis` or `redis`' - ); - } - - return $this->getConnectionInitializer($options, $callback); - } - /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { if (is_string($value)) { - return $this->getConnectionInitializerByDescription($options, $value); - } else { - return $this->getConnectionInitializer($options, $value); + $value = $this->getConnectionInitializerByString($options, $value); } + + if (is_callable($value)) { + return $this->getConnectionInitializer($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects either a string or a callable value, %s given', + static::class, + is_object($value) ? get_class($value) : gettype($value) + )); + } + } + + /** + * Returns a connection initializer from a descriptive name. + * + * @param OptionsInterface $options Client options + * @param string $description Identifier of a replication backend (`predis`, `sentinel`) + * + * @return callable + */ + protected function getConnectionInitializerByString(OptionsInterface $options, string $description) + { + switch ($description) { + case 'redis': + case 'redis-cluster': + return function ($parameters, $options, $option) { + return new RedisCluster($options->connections, new RedisStrategy($options->crc16)); + }; + + case 'predis': + return $this->getDefaultConnectionInitializer(); + + default: + throw new InvalidArgumentException(sprintf( + '%s expects either `predis`, `redis` or `redis-cluster` as valid string values, `%s` given', + static::class, + $description + )); + } + } + + /** + * Returns the default connection initializer. + * + * @return callable + */ + protected function getDefaultConnectionInitializer() + { + return function ($parameters, $options, $option) { + return new PredisCluster(); + }; } /** @@ -67,8 +92,9 @@ class Cluster extends Aggregate */ public function getDefault(OptionsInterface $options) { - return function ($options) { - return new PredisCluster(); - }; + return $this->getConnectionInitializer( + $options, + $this->getDefaultConnectionInitializer() + ); } } diff --git a/src/Configuration/Option/Replication.php b/src/Configuration/Option/Replication.php index 2074ccd4..f3a97b2d 100644 --- a/src/Configuration/Option/Replication.php +++ b/src/Configuration/Option/Replication.php @@ -11,7 +11,9 @@ namespace Predis\Configuration\Option; +use InvalidArgumentException; use Predis\Configuration\OptionsInterface; +use Predis\Connection\AggregateConnectionInterface; use Predis\Connection\Replication\MasterSlaveReplication; use Predis\Connection\Replication\SentinelReplication; @@ -23,49 +25,74 @@ use Predis\Connection\Replication\SentinelReplication; */ class Replication extends Aggregate { - /** - * Returns a connection initializer from a descriptive name. - * - * @param OptionsInterface $options Client options. - * @param string $description Identifier of a replication backend (`predis`, `sentinel`) - * - * @return callable - */ - protected function getConnectionInitializerByDescription(OptionsInterface $options, $description) - { - if ($description === 'predis') { - $callback = $this->getDefault($options); - } elseif ($description === 'sentinel') { - $callback = function ($options, $sentinels) { - return new SentinelReplication($options->service, $sentinels, $options->connections); - }; - } else { - throw new \InvalidArgumentException( - 'String value for the replication option must be either `predis` or `sentinel`' - ); - } - - return $this->getConnectionInitializer($options, $callback); - } - /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { if (is_string($value)) { - return $this->getConnectionInitializerByDescription($options, $value); - } else { + $value = $this->getConnectionInitializerByString($options, $value); + } + + if (is_callable($value)) { return $this->getConnectionInitializer($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects either a string or a callable value, %s given', + static::class, + is_object($value) ? get_class($value) : gettype($value) + )); } } /** - * {@inheritdoc} + * Returns a connection initializer (callable) from a descriptive string. + * + * Each connection initializer is specialized for the specified replication + * backend so that all the necessary steps for the configuration of the new + * aggregate connection are performed inside the initializer and the client + * receives a ready-to-use connection. + * + * Supported configuration values are: + * + * - `predis` for unmanaged replication setups + * - `redis-sentinel` for replication setups managed by redis-sentinel + * - `sentinel` is an alias of `redis-sentinel` + * + * @param OptionsInterface $options Client options + * @param string $description Identifier of a replication backend + * + * @return callable */ - public function getDefault(OptionsInterface $options) + protected function getConnectionInitializerByString(OptionsInterface $options, string $description) { - return function ($options) { + switch ($description) { + case 'sentinel': + case 'redis-sentinel': + return function ($parameters, $options, $option) { + return new SentinelReplication($options->service, $parameters, $options->connections); + }; + + case 'predis': + return $this->getDefaultConnectionInitializer($options); + + default: + throw new InvalidArgumentException(sprintf( + '%s expects either `predis`, `sentinel` or `redis-sentinel` as valid string values, `%s` given', + static::class, + $description + )); + } + } + + /** + * Returns the default connection initializer. + * + * @return callable + */ + protected function getDefaultConnectionInitializer() + { + return function ($parameters, $options, $option) { $connection = new MasterSlaveReplication(); if ($options->autodiscovery) { @@ -76,4 +103,33 @@ class Replication extends Aggregate return $connection; }; } + + /** + * {@inheritdoc} + */ + public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes) + { + // TODO: at least for now we will replicate the previous behaviour of + // skipping automatic aggregation when using the redis-sentinel backend + // because $nodes contains an array of sentinel servers instead of Redis + // servers and SentinelReplication already gets the list of sentinels in + // the first argument of its constructor. SentinelReplication::add() + // actually knows how to handle connections marked with role=sentinel in + // their parameters but relying on it would require an explicit role to + // be set by the user and I would like to avoid enforcing that for now. + if (!$connection instanceof SentinelReplication) { + parent::aggregate($options, $connection, $nodes); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return $this->getConnectionInitializer( + $options, + $this->getDefaultConnectionInitializer() + ); + } } diff --git a/src/Connection/Factory.php b/src/Connection/Factory.php index 4d899dd5..15ddf09c 100644 --- a/src/Connection/Factory.php +++ b/src/Connection/Factory.php @@ -109,16 +109,6 @@ class Factory implements FactoryInterface return $connection; } - /** - * {@inheritdoc} - */ - public function aggregate(AggregateConnectionInterface $connection, array $parameters) - { - foreach ($parameters as $node) { - $connection->add($node instanceof NodeConnectionInterface ? $node : $this->create($node)); - } - } - /** * Assigns a default set of parameters applied to new connections. * diff --git a/src/Connection/FactoryInterface.php b/src/Connection/FactoryInterface.php index 2bae0839..5270f3f6 100644 --- a/src/Connection/FactoryInterface.php +++ b/src/Connection/FactoryInterface.php @@ -41,12 +41,4 @@ interface FactoryInterface * @return NodeConnectionInterface */ public function create($parameters); - - /** - * Aggregates single connections into an aggregate connection instance. - * - * @param AggregateConnectionInterface $aggregate Aggregate connection instance. - * @param array $parameters List of parameters for each connection. - */ - public function aggregate(AggregateConnectionInterface $aggregate, array $parameters); }