Browse Source

Reorganize and improve client options.

All option classes have been moved in the Predis\Configuration\Option
namespace and some have been optimized to have less impact on client
initialization timings.

Furthermore the accepted values for some options have been changed,
this is the complete list of accepted values:

- _aggregate_: callable returning an aggregate connection.
- cluster: string value ("predis", "redis"), callable returning an
  aggregate connection.
- replication: string value ("predis", "sentinel"), callable returning
  an aggregate connection.
- commands: command factory, named array mapping command IDs to PHP
  classes, callable returning a command factory or a named array.
- connections: connection factory, callable returning a connection
  factory, named array mapping connection schemes to PHP classes.
- _prefix_: string value, command processor, callable.
- _exceptions_: boolean value.

Note that the cluster and replication options now return a closure
acting as initializer instead of an aggregate connection.
Daniele Alessandri 9 years ago
parent
commit
d34bdd38c3
30 changed files with 1302 additions and 701 deletions
  1. 20 0
      CHANGELOG.md
  2. 14 8
      README.md
  3. 3 6
      examples/lua_scripting_abstraction.php
  4. 3 6
      examples/replication_complex.php
  5. 1 1
      examples/replication_simple.php
  6. 32 36
      src/Client.php
  7. 0 76
      src/Configuration/ClusterOption.php
  8. 0 64
      src/Configuration/CommandsOption.php
  9. 71 0
      src/Configuration/Option/Aggregate.php
  10. 73 0
      src/Configuration/Option/Cluster.php
  11. 67 0
      src/Configuration/Option/Commands.php
  12. 11 5
      src/Configuration/Option/Connections.php
  13. 5 2
      src/Configuration/Option/Exceptions.php
  14. 9 3
      src/Configuration/Option/Prefix.php
  15. 79 0
      src/Configuration/Option/Replication.php
  16. 10 11
      src/Configuration/Options.php
  17. 0 75
      src/Configuration/ReplicationOption.php
  18. 112 35
      tests/Predis/ClientTest.php
  19. 0 90
      tests/Predis/Configuration/ClusterOptionTest.php
  20. 0 100
      tests/Predis/Configuration/CommandsOptionTest.php
  21. 90 0
      tests/Predis/Configuration/Option/AggregateTest.php
  22. 140 0
      tests/Predis/Configuration/Option/ClusterTest.php
  23. 178 0
      tests/Predis/Configuration/Option/CommandsTest.php
  24. 32 11
      tests/Predis/Configuration/Option/ConnectionsTest.php
  25. 5 5
      tests/Predis/Configuration/Option/ExceptionsTest.php
  26. 127 0
      tests/Predis/Configuration/Option/PrefixTest.php
  27. 190 0
      tests/Predis/Configuration/Option/ReplicationTest.php
  28. 30 12
      tests/Predis/Configuration/OptionsTest.php
  29. 0 60
      tests/Predis/Configuration/PrefixOptionTest.php
  30. 0 95
      tests/Predis/Configuration/ReplicationOptionTest.php

+ 20 - 0
CHANGELOG.md

@@ -1,6 +1,26 @@
 v2.0.0 (201x-xx-xx)
 ================================================================================
 
+- Accepted values for some client options have changed, this is the new list of
+  accepted values:
+
+  - `aggregate`: callable returning an aggregate connection.
+  - `cluster`: string value (`predis`, `redis`), callable returning an aggregate
+    connection.
+  - `replication`: string value (`predis`, `sentinel`), callable returning an
+     aggregate connection.
+  - `commands`: command factory, named array mapping command IDs to PHP classes,
+    callable returning a command factory or a named array.
+  - `connections`: connection factory, callable returning a connection factory,
+    named array mapping connection schemes to PHP classes.
+  - `prefix`: string value, command processor, callable.
+  - `exceptions`: boolean.
+
+  Note that both the `cluster` and `replication` options now return a closure
+  acting as initializer instead of an aggregate connection instance.
+
+- Client option classes now live in the `Predis\Configuration\Option` namespace.
+
 - Classes for Redis commands have been moved into the new `Predis\Command\Redis`
   namespace and each class name mirrors the respective Redis command ID.
 

+ 14 - 8
README.md

@@ -154,12 +154,12 @@ $client = new Predis\Client($parameters, ['prefix' => 'sample:']);
 Options are managed using a mini DI-alike container and their values can be lazily initialized only
 when needed. The client options supported by default in Predis are:
 
-  - `prefix`: prefix string automatically applied to keys found in commands.
+  - `prefix`: prefix string applied to every key found in commands.
   - `exceptions`: whether the client should throw or return responses upon Redis errors.
   - `connections`: list of connection backends or a connection factory instance.
-  - `cluster`: specifies a cluster backend (`predis`, `redis` or callable object).
-  - `replication`: specifies a replication backend (`TRUE`, `sentinel` or callable object).
-  - `aggregate`: overrides `cluster` and `replication` to provide a custom connections aggregator.
+  - `cluster`: specifies a cluster backend (`predis`, `redis` or callable).
+  - `replication`: specifies a replication backend (`predis`, `sentinel` or callable).
+  - `aggregate`: configures the client with a custom aggregate connection (callable).
   - `parameters`: list of default connection parameters for aggregate connections.
   - `commands`: specifies a command factory instance to use through the library.
 
@@ -337,8 +337,11 @@ class BrandNewRedisCommand extends Predis\Command\Command
 }
 
 // Inject your command in the current command factory:
-$client = new Predis\Client();
-$client->getCommandFactory()->defineCommand('newcmd', 'BrandNewRedisCommand');
+$client = new Predis\Client($parameters, [
+    'commands' => [
+        'newcmd' => 'BrandNewRedisCommand',
+    ],
+]);
 
 $response = $client->newcmd();
 ```
@@ -384,8 +387,11 @@ LUA;
 }
 
 // Inject the script command in the current command factory:
-$client = new Predis\Client();
-$client->getCommandFactory()->defineCommand('lpushrand', 'ListPushRandomValue');
+$client = new Predis\Client($parameters, [
+    'commands' => [
+        'lpushrand' => 'ListPushRandomValue',
+    ],
+]);
 
 $response = $client->lpushrand('random_values', $seed = mt_rand());
 ```

+ 3 - 6
examples/lua_scripting_abstraction.php

@@ -49,12 +49,9 @@ LUA;
 }
 
 $client = new Predis\Client($single_server, array(
-    'commands' => function ($options) {
-        $commands = $options->getDefault('commands');
-        $commands->defineCommand('increxby', 'IncrementExistingKeysBy');
-
-        return $commands;
-    },
+    'commands' => array(
+        'increxby' => 'IncrementExistingKeysBy',
+    ),
 ));
 
 $client->mset('foo', 10, 'foobar', 100);

+ 3 - 6
examples/replication_complex.php

@@ -52,12 +52,9 @@ $parameters = array(
 );
 
 $options = array(
-    'commands' => function ($options) {
-        $commands = $options->getDefault('commands');
-        $commands->defineCommand('hmgetall', 'HashMultipleGetAll');
-
-        return $commands;
-    },
+    'commands' => array(
+        'hmgetall' => 'HashMultipleGetAll',
+    ),
     'replication' => function () {
         $strategy = new ReplicationStrategy();
         $strategy->setScriptReadOnly(HashMultipleGetAll::BODY);

+ 1 - 1
examples/replication_simple.php

@@ -26,7 +26,7 @@ $parameters = array(
     'tcp://127.0.0.1:6380?database=15&alias=slave',
 );
 
-$options = array('replication' => true);
+$options = array('replication' => 'predis');
 
 $client = new Predis\Client($parameters, $options);
 

+ 32 - 36
src/Client.php

@@ -19,6 +19,7 @@ use Predis\Configuration\OptionsInterface;
 use Predis\Connection\AggregateConnectionInterface;
 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;
@@ -102,73 +103,68 @@ class Client implements ClientInterface, \IteratorAggregate
      */
     protected function createConnection($parameters)
     {
+        $options = $this->getOptions();
+
         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);
+                return $options->connections->create($parameters);
             }
 
-            $options = $this->options;
-
-            if ($options->defined('aggregate')) {
-                $initializer = $this->getConnectionInitializerWrapper($options->aggregate);
-                $connection = $initializer($parameters, $options);
+            if ($options->defined('cluster')) {
+                return $this->createAggregateConnection($parameters, 'cluster');
             } elseif ($options->defined('replication')) {
-                $replication = $options->replication;
-
-                if ($replication instanceof AggregateConnectionInterface) {
-                    $connection = $replication;
-                    $options->connections->aggregate($connection, $parameters);
-                } else {
-                    $initializer = $this->getConnectionInitializerWrapper($replication);
-                    $connection = $initializer($parameters, $options);
-                }
+                return $this->createAggregateConnection($parameters, 'replication');
+            } elseif ($options->defined('aggregate')) {
+                return $this->createAggregateConnection($parameters, 'aggregate');
             } 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.');
+        throw new \InvalidArgumentException('Invalid type for connection parameters');
     }
 
     /**
-     * Wraps a callable to make sure that its returned value represents a valid
-     * connection type.
+     * Creates an aggregate connection.
      *
-     * @param mixed $callable
+     * @param mixed  $parameters Connection parameters.
+     * @param string $option     Option for aggregate connections (`aggregate`, `cluster`, `replication`).
      *
      * @return \Closure
      */
-    protected function getConnectionInitializerWrapper($callable)
+    protected function createAggregateConnection($parameters, $option)
     {
-        return function () use ($callable) {
-            $connection = call_user_func_array($callable, func_get_args());
+        $options = $this->getOptions();
 
-            if (!$connection instanceof ConnectionInterface) {
-                throw new \UnexpectedValueException(
-                    'The callable connection initializer returned an invalid type.'
-                );
-            }
+        $initializer = $options->$option;
+        $connection = $initializer($parameters);
 
-            return $connection;
-        };
+        // 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;
     }
 
     /**

+ 0 - 76
src/Configuration/ClusterOption.php

@@ -1,76 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use Predis\Connection\Cluster\ClusterInterface;
-use Predis\Connection\Cluster\PredisCluster;
-use Predis\Connection\Cluster\RedisCluster;
-
-/**
- * Configures an aggregate connection used for clustering
- * multiple Redis nodes using various implementations with
- * different algorithms or strategies.
- *
- * @author Daniele Alessandri <suppakilla@gmail.com>
- */
-class ClusterOption implements OptionInterface
-{
-    /**
-     * Creates a new cluster connection from on a known descriptive name.
-     *
-     * @param OptionsInterface $options Instance of the client options.
-     * @param string           $id      Descriptive identifier of the cluster type (`predis`, `redis-cluster`)
-     *
-     * @return ClusterInterface|null
-     */
-    protected function createByDescription(OptionsInterface $options, $id)
-    {
-        switch ($id) {
-            case 'predis':
-            case 'predis-cluster':
-                return new PredisCluster();
-
-            case 'redis':
-            case 'redis-cluster':
-                return new RedisCluster($options->connections);
-
-            default:
-                return;
-        }
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function filter(OptionsInterface $options, $value)
-    {
-        if (is_string($value)) {
-            $value = $this->createByDescription($options, $value);
-        }
-
-        if (!$value instanceof ClusterInterface) {
-            throw new \InvalidArgumentException(
-                "An instance of type 'Predis\Connection\Cluster\ClusterInterface' was expected."
-            );
-        }
-
-        return $value;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getDefault(OptionsInterface $options)
-    {
-        return new PredisCluster();
-    }
-}

+ 0 - 64
src/Configuration/CommandsOption.php

@@ -1,64 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use Predis\Command\FactoryInterface;
-use Predis\Command\RedisFactory;
-
-/**
- * Configures a connection factory to be used by the client.
- *
- * @author Daniele Alessandri <suppakilla@gmail.com>
- */
-class CommandsOption implements OptionInterface
-{
-    /**
-     * Sets the commands processors to be used by the command factory.
-     *
-     * @param OptionsInterface $options Client options.
-     * @param FactoryInterface $factory Command factory.
-     */
-    protected function setProcessors(OptionsInterface $options, FactoryInterface $factory)
-    {
-        if (isset($options->prefix)) {
-            // NOTE: directly using __get('prefix') is actually a workaround for
-            // HHVM 2.3.0. It's correct and respects the options interface, it's
-            // just ugly. We will remove this hack when HHVM will fix re-entrant
-            // calls to __get() once and for all.
-
-            $factory->setProcessor($options->__get('prefix'));
-        }
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function filter(OptionsInterface $options, $value)
-    {
-        if (!$value instanceof FactoryInterface) {
-            throw new \InvalidArgumentException('Invalid value for the commands option.');
-        }
-
-        return $value;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getDefault(OptionsInterface $options)
-    {
-        $commands = new RedisFactory();
-        $this->setProcessors($options, $commands);
-
-        return $commands;
-    }
-}

+ 71 - 0
src/Configuration/Option/Aggregate.php

@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
+use Predis\Connection\AggregateConnectionInterface;
+
+/**
+ * Configures an aggregate connection used for clustering
+ * multiple Redis nodes using various implementations with
+ * different algorithms or strategies.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+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
+     */
+    protected function getConnectionInitializer(OptionsInterface $options, $callable)
+    {
+        if (!is_callable($callable)) {
+            $class = get_class($this);
+
+            throw new \InvalidArgumentException("$class expects a valid callable");
+        }
+
+        return function ($parameters = null) use ($callable, $options) {
+            $connection = call_user_func($callable, $options, $parameters);
+
+            if (!$connection instanceof AggregateConnectionInterface) {
+                $class = get_class($this);
+
+                throw new \InvalidArgumentException("$class expects a valid connection type returned by callable initializer");
+            }
+
+            return $connection;
+        };
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function filter(OptionsInterface $options, $value)
+    {
+        return $this->getConnectionInitializer($options, $value);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefault(OptionsInterface $options)
+    {
+        return;
+    }
+}

+ 73 - 0
src/Configuration/Option/Cluster.php

@@ -0,0 +1,73 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use Predis\Configuration\OptionsInterface;
+use Predis\Connection\Cluster\PredisCluster;
+use Predis\Connection\Cluster\RedisCluster;
+
+/**
+ * Configures an aggregate connection used for clustering
+ * multiple Redis nodes using various implementations with
+ * different algorithms or strategies.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+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);
+            };
+        } 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);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefault(OptionsInterface $options)
+    {
+        return function ($options) {
+            return new PredisCluster();
+        };
+    }
+}

+ 67 - 0
src/Configuration/Option/Commands.php

@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use Predis\Command\FactoryInterface;
+use Predis\Command\RedisFactory;
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
+
+/**
+ * Configures a connection factory to be used by the client.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class Commands implements OptionInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function filter(OptionsInterface $options, $value)
+    {
+        if (is_callable($value)) {
+            $value = call_user_func($value, $options);
+        }
+
+        if (is_array($value)) {
+            $commands = $this->getDefault($options);
+
+            foreach ($value as $commandID => $classFQN) {
+                $commands->defineCommand($commandID, $classFQN);
+            }
+
+            return $commands;
+        }
+
+        if (!$value instanceof FactoryInterface) {
+            $class = get_class($this);
+
+            throw new \InvalidArgumentException("$class expects a valid command factory");
+        }
+
+        return $value;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefault(OptionsInterface $options)
+    {
+        $commands = new RedisFactory();
+
+        if (isset($options->prefix)) {
+            $commands->setProcessor($options->prefix);
+        }
+
+        return $commands;
+    }
+}

+ 11 - 5
src/Configuration/ConnectionFactoryOption.php → src/Configuration/Option/Connections.php

@@ -9,8 +9,10 @@
  * file that was distributed with this source code.
  */
 
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
 
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
 use Predis\Connection\Factory;
 use Predis\Connection\FactoryInterface;
 
@@ -20,13 +22,17 @@ use Predis\Connection\FactoryInterface;
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
-class ConnectionFactoryOption implements OptionInterface
+class Connections implements OptionInterface
 {
     /**
      * {@inheritdoc}
      */
     public function filter(OptionsInterface $options, $value)
     {
+        if (is_callable($value)) {
+            $value = call_user_func($value, $options);
+        }
+
         if ($value instanceof FactoryInterface) {
             return $value;
         } elseif (is_array($value)) {
@@ -38,9 +44,9 @@ class ConnectionFactoryOption implements OptionInterface
 
             return $factory;
         } else {
-            throw new \InvalidArgumentException(
-                'Invalid value provided for the connections option.'
-            );
+            $class = get_class($this);
+
+            throw new \InvalidArgumentException("$class expects a valid connection factory");
         }
     }
 

+ 5 - 2
src/Configuration/ExceptionsOption.php → src/Configuration/Option/Exceptions.php

@@ -9,7 +9,10 @@
  * file that was distributed with this source code.
  */
 
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
+
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
 
 /**
  * Configures whether consumers (such as the client) should throw exceptions on
@@ -17,7 +20,7 @@ namespace Predis\Configuration;
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
-class ExceptionsOption implements OptionInterface
+class Exceptions implements OptionInterface
 {
     /**
      * {@inheritdoc}

+ 9 - 3
src/Configuration/PrefixOption.php → src/Configuration/Option/Prefix.php

@@ -9,10 +9,12 @@
  * file that was distributed with this source code.
  */
 
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
 
 use Predis\Command\Processor\KeyPrefixProcessor;
 use Predis\Command\Processor\ProcessorInterface;
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
 
 /**
  * Configures a command processor that apply the specified prefix string to a
@@ -20,18 +22,22 @@ use Predis\Command\Processor\ProcessorInterface;
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
-class PrefixOption implements OptionInterface
+class Prefix implements OptionInterface
 {
     /**
      * {@inheritdoc}
      */
     public function filter(OptionsInterface $options, $value)
     {
+        if (is_callable($value)) {
+            $value = call_user_func($value, $options);
+        }
+
         if ($value instanceof ProcessorInterface) {
             return $value;
         }
 
-        return new KeyPrefixProcessor($value);
+        return new KeyPrefixProcessor((string) $value);
     }
 
     /**

+ 79 - 0
src/Configuration/Option/Replication.php

@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use Predis\Configuration\OptionsInterface;
+use Predis\Connection\Replication\MasterSlaveReplication;
+use Predis\Connection\Replication\SentinelReplication;
+
+/**
+ * Configures an aggregate connection used for master/slave replication among
+ * multiple Redis nodes.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+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 {
+            return $this->getConnectionInitializer($options, $value);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefault(OptionsInterface $options)
+    {
+        return function ($options) {
+            $connection = new MasterSlaveReplication();
+
+            if ($options->autodiscovery) {
+                $connection->setConnectionFactory($options->connections);
+                $connection->setAutoDiscovery(true);
+            }
+
+            return $connection;
+        };
+    }
+}

+ 10 - 11
src/Configuration/Options.php

@@ -21,8 +21,8 @@ namespace Predis\Configuration;
  */
 class Options implements OptionsInterface
 {
+    protected $options = array();
     protected $input;
-    protected $options;
     protected $handlers;
 
     /**
@@ -43,12 +43,13 @@ class Options implements OptionsInterface
     protected function getHandlers()
     {
         return array(
-            'cluster' => 'Predis\Configuration\ClusterOption',
-            'connections' => 'Predis\Configuration\ConnectionFactoryOption',
-            'exceptions' => 'Predis\Configuration\ExceptionsOption',
-            'prefix' => 'Predis\Configuration\PrefixOption',
-            'commands' => 'Predis\Configuration\CommandsOption',
-            'replication' => 'Predis\Configuration\ReplicationOption',
+            'aggregate' => 'Predis\Configuration\Option\Aggregate',
+            'cluster' => 'Predis\Configuration\Option\Cluster',
+            'replication' => 'Predis\Configuration\Option\Replication',
+            'connections' => 'Predis\Configuration\Option\Connections',
+            'commands' => 'Predis\Configuration\Option\Commands',
+            'exceptions' => 'Predis\Configuration\Option\Exceptions',
+            'prefix' => 'Predis\Configuration\Option\Prefix',
         );
     }
 
@@ -100,14 +101,12 @@ class Options implements OptionsInterface
             $value = $this->input[$option];
             unset($this->input[$option]);
 
-            if (is_object($value) && method_exists($value, '__invoke')) {
-                $value = $value($this, $option);
-            }
-
             if (isset($this->handlers[$option])) {
                 $handler = $this->handlers[$option];
                 $handler = new $handler();
                 $value = $handler->filter($this, $value);
+            } elseif (is_object($value) && method_exists($value, '__invoke')) {
+                $value = $value($this);
             }
 
             return $this->options[$option] = $value;

+ 0 - 75
src/Configuration/ReplicationOption.php

@@ -1,75 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use Predis\Connection\Replication\MasterSlaveReplication;
-use Predis\Connection\Replication\ReplicationInterface;
-use Predis\Connection\Replication\SentinelReplication;
-
-/**
- * Configures an aggregate connection used for master/slave replication among
- * multiple Redis nodes.
- *
- * @author Daniele Alessandri <suppakilla@gmail.com>
- */
-class ReplicationOption implements OptionInterface
-{
-    /**
-     * {@inheritdoc}
-     *
-     * @todo There's more code than needed due to a bug in filter_var() as
-     *       discussed here https://bugs.php.net/bug.php?id=49510 and  different
-     *       behaviours when encountering NULL values on PHP 5.3.
-     */
-    public function filter(OptionsInterface $options, $value)
-    {
-        if ($value instanceof ReplicationInterface) {
-            return $value;
-        }
-
-        if (is_bool($value) || $value === null) {
-            return $value ? $this->getDefault($options) : null;
-        }
-
-        if ($value === 'sentinel') {
-            return function ($sentinels, $options) {
-                return new SentinelReplication($options->service, $sentinels, $options->connections);
-            };
-        }
-
-        if (
-            !is_object($value) &&
-            null !== $asbool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
-        ) {
-            return $asbool ? $this->getDefault($options) : null;
-        }
-
-        throw new \InvalidArgumentException(
-            "An instance of type 'Predis\Connection\Replication\ReplicationInterface' was expected."
-        );
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getDefault(OptionsInterface $options)
-    {
-        $replication = new MasterSlaveReplication();
-
-        if ($options->autodiscovery) {
-            $replication->setConnectionFactory($options->connections);
-            $replication->setAutoDiscovery(true);
-        }
-
-        return $replication;
-    }
-}

+ 112 - 35
tests/Predis/ClientTest.php

@@ -92,8 +92,10 @@ class ClientTest extends PredisTestCase
 
     /**
      * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Array of connection parameters requires `cluster`, `replication` or `aggregate` client option
      */
-    public function testConstructorWithArrayOfArrayArgument()
+    public function testConstructorThrowsExceptionWithArrayOfParametersArgumentAndMissingOption()
     {
         $arg1 = array(
             array('host' => 'localhost', 'port' => 7000),
@@ -101,8 +103,23 @@ class ClientTest extends PredisTestCase
         );
 
         $client = new Client($arg1);
+    }
 
-        $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $client->getConnection());
+    /**
+     * @group disconnected
+     */
+    public function testConstructorWithArrayOfArrayArgumentAndClusterOption()
+    {
+        $arg1 = array(
+            array('host' => 'localhost', 'port' => 7000),
+            array('host' => 'localhost', 'port' => 7001),
+        );
+
+        $client = new Client($arg1, array(
+            'aggregate' => $this->getAggregateInitializer($arg1),
+        ));
+
+        $this->assertInstanceOf('Predis\Connection\AggregateConnectionInterface', $client->getConnection());
     }
 
     /**
@@ -122,9 +139,13 @@ class ClientTest extends PredisTestCase
      */
     public function testConstructorWithArrayOfStringArgument()
     {
-        $client = new Client($arg1 = array('tcp://localhost:7000', 'tcp://localhost:7001'));
+        $arg1 = array('tcp://localhost:7000', 'tcp://localhost:7001');
 
-        $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $client->getConnection());
+        $client = new Client($arg1, array(
+            'aggregate' => $this->getAggregateInitializer($arg1),
+        ));
+
+        $this->assertInstanceOf('Predis\Connection\AggregateConnectionInterface', $client->getConnection());
     }
 
     /**
@@ -132,14 +153,16 @@ class ClientTest extends PredisTestCase
      */
     public function testConstructorWithArrayOfConnectionsArgument()
     {
-        $connection1 = $this->getMock('Predis\Connection\NodeConnectionInterface');
-        $connection2 = $this->getMock('Predis\Connection\NodeConnectionInterface');
+        $arg1 = array(
+            $this->getMock('Predis\Connection\NodeConnectionInterface'),
+            $this->getMock('Predis\Connection\NodeConnectionInterface'),
+        );
 
-        $client = new Client(array($connection1, $connection2));
+        $client = new Client($arg1, array(
+            'aggregate' => $this->getAggregateInitializer($arg1),
+        ));
 
-        $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $cluster = $client->getConnection());
-        $this->assertSame($connection1, $cluster->getConnectionById(0));
-        $this->assertSame($connection2, $cluster->getConnectionById(1));
+        $this->assertInstanceOf('Predis\Connection\AggregateConnectionInterface', $client->getConnection());
     }
 
     /**
@@ -212,8 +235,8 @@ class ClientTest extends PredisTestCase
 
     /**
      * @group disconnected
-     * @expectedException \UnexpectedValueException
-     * @expectedExceptionMessage The callable connection initializer returned an invalid type.
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Callable parameters must return a valid connection
      */
     public function testConstructorWithCallableConnectionInitializerThrowsExceptionOnInvalidReturnType()
     {
@@ -249,7 +272,7 @@ class ClientTest extends PredisTestCase
     public function testConstructorWithArrayAndOptionReplication()
     {
         $arg1 = array('tcp://host1?alias=master', 'tcp://host2?alias=slave');
-        $arg2 = array('replication' => true);
+        $arg2 = array('replication' => 'predis');
         $client = new Client($arg1, $arg2);
 
         $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $connection = $client->getConnection());
@@ -260,28 +283,56 @@ class ClientTest extends PredisTestCase
     /**
      * @group disconnected
      */
-    public function testConstructorWithArrayAndOptionAggregate()
+    public function testClusterOptionHasPrecedenceOverReplicationOptionAndAggregateOption()
     {
         $arg1 = array('tcp://host1', 'tcp://host2');
 
-        $connection = $this->getMock('Predis\Connection\ConnectionInterface');
-
-        $fnaggregate = $this->getMock('stdClass', array('__invoke'));
-        $fnaggregate->expects($this->once())
-                    ->method('__invoke')
-                    ->with($arg1)
-                    ->will($this->returnValue($connection));
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
 
         $fncluster = $this->getMock('stdClass', array('__invoke'));
-        $fncluster->expects($this->never())->method('__invoke');
+        $fncluster->expects($this->once())
+                  ->method('__invoke')
+                  ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'), $arg1)
+                  ->will($this->returnValue($connection));
 
         $fnreplication = $this->getMock('stdClass', array('__invoke'));
         $fnreplication->expects($this->never())->method('__invoke');
 
+        $fnaggregate = $this->getMock('stdClass', array('__invoke'));
+        $fnaggregate->expects($this->never())->method('__invoke');
+
+        $arg2 = array(
+            'cluster' => $fncluster,
+            'replication' => $fnreplication,
+            'aggregate' => $fnaggregate,
+        );
+
+        $client = new Client($arg1, $arg2);
+
+        $this->assertSame($connection, $client->getConnection());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testReplicationOptionHasPrecedenceOverAggregateOption()
+    {
+        $arg1 = array('tcp://host1', 'tcp://host2');
+
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $fnreplication = $this->getMock('stdClass', array('__invoke'));
+        $fnreplication->expects($this->once())
+                      ->method('__invoke')
+                      ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'), $arg1)
+                      ->will($this->returnValue($connection));
+
+        $fnaggregate = $this->getMock('stdClass', array('__invoke'));
+        $fnaggregate->expects($this->never())->method('__invoke');
+
         $arg2 = array(
-            'aggregate' => function () use ($fnaggregate) { return $fnaggregate; },
-            'cluster' => function () use ($fncluster) { return $fncluster; },
-            'replication' => function () use ($fnreplication) { return $fnreplication; },
+            'replication' => $fnreplication,
+            'aggregate' => $fnaggregate,
         );
 
         $client = new Client($arg1, $arg2);
@@ -291,22 +342,28 @@ class ClientTest extends PredisTestCase
 
     /**
      * @group disconnected
-     * @expectedException \UnexpectedValueException
-     * @expectedExceptionMessage The callable connection initializer returned an invalid type.
      */
-    public function testConstructorWithArrayAndOptionAggregateThrowsExceptionOnInvalidReturnType()
+    public function testAggregateOptionDoesNotTriggerAggregationInClient()
     {
         $arg1 = array('tcp://host1', 'tcp://host2');
 
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
         $fnaggregate = $this->getMock('stdClass', array('__invoke'));
         $fnaggregate->expects($this->once())
                     ->method('__invoke')
-                    ->with($arg1)
-                    ->will($this->returnValue(false));
+                    ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'), $arg1)
+                    ->will($this->returnValue($connection));
+
+        $connections = $this->getMock('Predis\Connection\FactoryInterface');
+        $connections->expects($this->never())
+                    ->method('aggregate');
 
-        $arg2 = array('aggregate' => function () use ($fnaggregate) { return $fnaggregate; });
+        $arg2 = array('aggregate' => $fnaggregate, 'connections' => $connections);
 
-        new Client($arg1, $arg2);
+        $client = new Client($arg1, $arg2);
+
+        $this->assertSame($connection, $client->getConnection());
     }
 
     /**
@@ -577,7 +634,7 @@ class ClientTest extends PredisTestCase
      */
     public function testGetConnectionFromAggregateConnectionWithAlias()
     {
-        $client = new Client(array('tcp://host1?alias=node01', 'tcp://host2?alias=node02'));
+        $client = new Client(array('tcp://host1?alias=node01', 'tcp://host2?alias=node02'), array('cluster' => 'predis'));
 
         $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $cluster = $client->getConnection());
         $this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $node01 = $client->getConnectionById('node01'));
@@ -604,7 +661,7 @@ class ClientTest extends PredisTestCase
      */
     public function testCreateClientWithConnectionFromAggregateConnection()
     {
-        $client = new Client(array('tcp://host1?alias=node01', 'tcp://host2?alias=node02'), array('prefix' => 'pfx:'));
+        $client = new Client(array('tcp://host1?alias=node01', 'tcp://host2?alias=node02'), array('prefix' => 'pfx:', 'cluster' => 'predis'));
 
         $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $cluster = $client->getConnection());
         $this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $node01 = $client->getConnectionById('node01'));
@@ -623,7 +680,7 @@ class ClientTest extends PredisTestCase
     public function testGetClientForReturnsInstanceOfSubclass()
     {
         $nodes = array('tcp://host1?alias=node01', 'tcp://host2?alias=node02');
-        $client = $this->getMock('Predis\Client', array('dummy'), array($nodes), 'SubclassedClient');
+        $client = $this->getMock('Predis\Client', array('dummy'), array($nodes, array('cluster' => 'predis')), 'SubclassedClient');
 
         $this->assertInstanceOf('SubclassedClient', $client->getClientFor('node02'));
     }
@@ -891,4 +948,24 @@ class ClientTest extends PredisTestCase
 
         return $uriString;
     }
+
+    /**
+     * Returns a mock callable simulating an aggregate connection initializer.
+     *
+     * @param mixed $parameters Expected connection parameters
+     *
+     * @return callable
+     */
+    protected function getAggregateInitializer($parameters)
+    {
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'), $parameters)
+                 ->will($this->returnValue($connection));
+
+        return $callable;
+    }
 }

+ 0 - 90
tests/Predis/Configuration/ClusterOptionTest.php

@@ -1,90 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use PredisTestCase;
-
-/**
- *
- */
-class ClusterOptionTest extends PredisTestCase
-{
-    /**
-     * @group disconnected
-     */
-    public function testDefaultOptionValue()
-    {
-        $option = new ClusterOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $this->assertInstanceOf('Predis\Connection\Cluster\PredisCluster', $option->getDefault($options));
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAcceptsInstanceOfClusterInterface()
-    {
-        $option = new ClusterOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $cluster = $this->getMock('Predis\Connection\Cluster\ClusterInterface');
-
-        $this->assertSame($cluster, $option->filter($options, $cluster));
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAcceptsPredefinedShortNameString()
-    {
-        $option = new ClusterOption();
-
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $options->expects($this->any())
-                ->method('__get')
-                ->with('connections')
-                ->will($this->returnValue(
-                    $this->getMock('Predis\Connection\FactoryInterface')
-                ));
-
-        $this->assertInstanceOf('Predis\Connection\Cluster\PredisCluster', $option->filter($options, 'predis'));
-        $this->assertInstanceOf('Predis\Connection\Cluster\PredisCluster', $option->filter($options, 'predis-cluster'));
-
-        $this->assertInstanceOf('Predis\Connection\Cluster\RedisCluster', $option->filter($options, 'redis'));
-        $this->assertInstanceOf('Predis\Connection\Cluster\RedisCluster', $option->filter($options, 'redis-cluster'));
-    }
-
-    /**
-     * @group disconnected
-     * @expectedException \InvalidArgumentException
-     */
-    public function testThrowsExceptionOnInvalidInstanceType()
-    {
-        $option = new ClusterOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $class = $this->getMock('Predis\Connection\NodeConnectionInterface');
-
-        $option->filter($options, $class);
-    }
-
-    /**
-     * @group disconnected
-     * @expectedException \InvalidArgumentException
-     */
-    public function testThrowsExceptionOnInvalidShortNameString()
-    {
-        $option = new ClusterOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $option->filter($options, 'unknown');
-    }
-}

+ 0 - 100
tests/Predis/Configuration/CommandsOptionTest.php

@@ -1,100 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use Predis\Command\RedisFactory;
-use Predis\Command\Processor\KeyPrefixProcessor;
-use PredisTestCase;
-
-/**
- *
- */
-class CommandsOptionTest extends PredisTestCase
-{
-    /**
-     * @group disconnected
-     */
-    public function testDefaultOptionValue()
-    {
-        $option = new CommandsOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $commands = $option->getDefault($options);
-
-        $this->assertInstanceOf('Predis\Command\FactoryInterface', $commands);
-        $this->assertNull($commands->getProcessor());
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAcceptsCommandFactoryInstanceAsValue()
-    {
-        $option = new CommandsOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $value = new RedisFactory();
-
-        $commands = $option->filter($options, $value);
-
-        $this->assertSame($commands, $value);
-        $this->assertNull($commands->getProcessor());
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAppliesPrefixOnDefaultOptionValue()
-    {
-        $option = new CommandsOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $options->expects($this->once())
-                ->method('__isset')
-                ->with('prefix')
-                ->will($this->returnValue(true));
-        $options->expects($this->once())
-                ->method('__get')
-                ->with('prefix')
-                ->will($this->returnValue(new KeyPrefixProcessor('prefix:')));
-
-        $commands = $option->getDefault($options);
-
-        $this->assertInstanceOf('Predis\Command\FactoryInterface', $commands);
-        $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $commands->getProcessor());
-        $this->assertSame('prefix:', $commands->getProcessor()->getPrefix());
-    }
-
-    /**
-     * @group disconnected
-     * @expectedException InvalidArgumentException
-     */
-    public function testThrowsExceptionOnStringValue()
-    {
-        $option = new CommandsOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $option->filter($options, '3.2');
-    }
-
-    /**
-     * @group disconnected
-     * @expectedException InvalidArgumentException
-     */
-    public function testThrowsExceptionOnInvalidValue()
-    {
-        $option = new CommandsOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $option->filter($options, new \stdClass());
-    }
-}

+ 90 - 0
tests/Predis/Configuration/Option/AggregateTest.php

@@ -0,0 +1,90 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use PredisTestCase;
+
+/**
+ *
+ */
+class AggregateTest extends PredisTestCase
+{
+    /**
+     * @group disconnected
+     */
+    public function testDefaultOptionValue()
+    {
+        $option = new Aggregate();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $this->assertNull($option->getDefault($options));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableAsConnectionInitializer()
+    {
+        $option = new Aggregate();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, $callable));
+        $this->assertSame($connection, $initializer($parameters = array()));
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Aggregate expects a valid connection type returned by callable initializer
+     */
+    public function testThrowsExceptionOnInvalidReturnTypeOfConnectionInitializer()
+    {
+        $option = new Aggregate();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\NodeConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, $callable));
+
+        $initializer($parameters = array());
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Aggregate expects a valid callable
+     */
+    public function testThrowsExceptionOnInstanceOfAggregateConnectionInterface()
+    {
+        $option = new Aggregate();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $cluster = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $option->filter($options, $cluster);
+    }
+}

+ 140 - 0
tests/Predis/Configuration/Option/ClusterTest.php

@@ -0,0 +1,140 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use PredisTestCase;
+
+/**
+ *
+ */
+class ClusterTest extends PredisTestCase
+{
+    /**
+     * @group disconnected
+     */
+    public function testDefaultOptionValue()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $this->assertInstanceOf('Closure', $initializer = $option->getDefault($options));
+        $this->assertInstanceOf('Predis\Connection\Cluster\PredisCluster', $initializer($options));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableAsConnectionInitializer()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, $callable));
+        $this->assertSame($connection, $initializer($parameters = array()));
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Cluster expects a valid connection type returned by callable initializer
+     */
+    public function testThrowsExceptionOnInvalidReturnTypeOfConnectionInitializer()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\NodeConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, $callable));
+
+        $initializer($parameters = array());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsShortNameStringPredis()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $options->expects($this->never())
+                ->method('__get')
+                ->with('connections');
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, 'predis'));
+        $this->assertInstanceOf('Predis\Connection\Cluster\PredisCluster', $initializer($parameters = array()));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsShortNameStringRedis()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $options->expects($this->once())
+                ->method('__get')
+                ->with('connections')
+                ->will($this->returnValue(
+                    $this->getMock('Predis\Connection\FactoryInterface')
+                ));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, 'redis'));
+        $this->assertInstanceOf('Predis\Connection\Cluster\RedisCluster', $initializer($parameters = array()));
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage String value for the cluster option must be either `predis` or `redis`
+     */
+    public function testThrowsExceptionOnInvalidShortNameString()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $option->filter($options, 'unknown');
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Cluster expects a valid callable
+     */
+    public function testThrowsExceptionOnInstanceOfClusterInterface()
+    {
+        $option = new Cluster();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\Cluster\ClusterInterface');
+
+        $option->filter($options, $connection);
+    }
+}

+ 178 - 0
tests/Predis/Configuration/Option/CommandsTest.php

@@ -0,0 +1,178 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use Predis\Command\Processor\KeyPrefixProcessor;
+use Predis\Command\RedisFactory;
+use PredisTestCase;
+
+/**
+ *
+ */
+class CommandsTest extends PredisTestCase
+{
+    /**
+     * @group disconnected
+     */
+    public function testDefaultOptionValue()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $commands = $option->getDefault($options);
+
+        $this->assertInstanceOf('Predis\Command\FactoryInterface', $commands);
+        $this->assertNull($commands->getProcessor());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAppliesPrefixOnDefaultOptionValue()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $options->expects($this->once())
+                ->method('__isset')
+                ->with('prefix')
+                ->will($this->returnValue(true));
+        $options->expects($this->once())
+                ->method('__get')
+                ->with('prefix')
+                ->will($this->returnValue(new KeyPrefixProcessor('prefix:')));
+
+        $commands = $option->getDefault($options);
+
+        $this->assertInstanceOf('Predis\Command\FactoryInterface', $commands);
+        $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $commands->getProcessor());
+        $this->assertSame('prefix:', $commands->getProcessor()->getPrefix());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCommandFactoryInstanceAsValue()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $input = new RedisFactory();
+
+        $commands = $option->filter($options, $input);
+
+        $this->assertSame($commands, $input);
+        $this->assertNull($commands->getProcessor());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsDictionaryOfCommandsAsValue()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $input = array(
+            'FOO' => 'Predis\Command\RawCommand',
+            'BAR' => 'Predis\Command\RawCommand',
+        );
+
+        $commands = $option->filter($options, $input);
+
+        $this->assertInstanceOf('Predis\Command\FactoryInterface', $commands);
+        $this->assertSame('Predis\Command\RawCommand', $commands->getCommandClass('FOO'));
+        $this->assertSame('Predis\Command\RawCommand', $commands->getCommandClass('BAR'));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableReturningCommandFactoryInstance()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $commands = $this->getMock('Predis\Command\FactoryInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($commands));
+
+        $this->assertSame($commands, $option->filter($options, $callable));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableReturningDictionaryOfCommandsAsValue()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $dictionary = array(
+            'FOO' => 'Predis\Command\RawCommand',
+            'BAR' => 'Predis\Command\RawCommand',
+        );
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($dictionary));
+
+        $commands = $option->filter($options, $callable);
+
+        $this->assertInstanceOf('Predis\Command\FactoryInterface', $commands);
+        $this->assertSame('Predis\Command\RawCommand', $commands->getCommandClass('FOO'));
+        $this->assertSame('Predis\Command\RawCommand', $commands->getCommandClass('BAR'));
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Commands expects a valid command factory
+     */
+    public function testThrowsExceptionOnInvalidTypeReturnedByCallable()
+    {
+        $option = new Commands();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue(new \stdClass()));
+
+        $option->filter($options, $callable);
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Commands expects a valid command factory
+     */
+    public function testThrowsExceptionOnInvalidValue()
+    {
+        $option = new Commands();
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $option->filter($options, new \stdClass());
+    }
+}

+ 32 - 11
tests/Predis/Configuration/ConnectionFactoryOptionTest.php → tests/Predis/Configuration/Option/ConnectionsTest.php

@@ -9,21 +9,22 @@
  * file that was distributed with this source code.
  */
 
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
 
 use PredisTestCase;
 
 /**
  *
  */
-class ConnectionFactoryOptionTest extends PredisTestCase
+class ConnectionsTest extends PredisTestCase
 {
     /**
      * @group disconnected
      */
     public function testDefaultOptionValue()
     {
-        $option = new ConnectionFactoryOption();
+        $option = new Connections();
+
         $options = $this->getMock('Predis\Configuration\OptionsInterface');
 
         $this->assertInstanceOf('Predis\Connection\Factory', $option->getDefault($options));
@@ -44,7 +45,7 @@ class ConnectionFactoryOptionTest extends PredisTestCase
                 ->method('define')
                 ->with($this->matchesRegularExpression('/^tcp|redis$/'), $class);
 
-        $option = $this->getMock('Predis\Configuration\ConnectionFactoryOption', array('getDefault'));
+        $option = $this->getMock('Predis\Configuration\Option\Connections', array('getDefault'));
         $option->expects($this->once())
                ->method('getDefault')
                ->with($options)
@@ -74,7 +75,7 @@ class ConnectionFactoryOptionTest extends PredisTestCase
                 ->with('parameters')
                 ->will($this->returnValue($parameters));
 
-        $option = new ConnectionFactoryOption();
+        $option = new Connections();
         $factory = $option->getDefault($options);
 
         $this->assertSame($parameters, $factory->getDefaultParameters());
@@ -85,22 +86,42 @@ class ConnectionFactoryOptionTest extends PredisTestCase
      */
     public function testAcceptsConnectionFactoryInstance()
     {
+        $option = $this->getMock('Predis\Configuration\Option\Connections', array('getDefault'));
+        $option->expects($this->never())->method('getDefault');
+
         $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $value = $this->getMock('Predis\Connection\FactoryInterface');
+        $factory = $this->getMock('Predis\Connection\FactoryInterface');
 
-        $option = $this->getMock('Predis\Configuration\ConnectionFactoryOption', array('getDefault'));
-        $option->expects($this->never())->method('getDefault');
+        $this->assertSame($factory, $option->filter($options, $factory));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableReturningConnectionFactoryInstance()
+    {
+        $option = new Connections();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $factory = $this->getMock('Predis\Connection\FactoryInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($factory));
 
-        $this->assertSame($value, $option->filter($options, $value));
+        $this->assertSame($factory, $option->filter($options, $callable));
     }
 
     /**
      * @group disconnected
-     * @expectedException InvalidArgumentException
+     * @expectedException \InvalidArgumentException
+     * @expectedException Predis\Configuration\Option\Connections expects a valid command factory
      */
     public function testThrowsExceptionOnInvalidArguments()
     {
-        $option = new ConnectionFactoryOption();
+        $option = new Connections();
         $options = $this->getMock('Predis\Configuration\OptionsInterface');
 
         $option->filter($options, new \stdClass());

+ 5 - 5
tests/Predis/Configuration/ExceptionsOptionTest.php → tests/Predis/Configuration/Option/ExceptionsTest.php

@@ -9,21 +9,21 @@
  * file that was distributed with this source code.
  */
 
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
 
 use PredisTestCase;
 
 /**
  *
  */
-class ExceptionsOptionTest extends PredisTestCase
+class ExceptionsTest extends PredisTestCase
 {
     /**
      * @group disconnected
      */
     public function testDefaultOptionValue()
     {
-        $option = new ExceptionsOption();
+        $option = new Exceptions();
         $options = $this->getMock('Predis\Configuration\OptionsInterface');
 
         $this->assertTrue($option->getDefault($options));
@@ -34,7 +34,7 @@ class ExceptionsOptionTest extends PredisTestCase
      */
     public function testAcceptsDifferentValuesAndFiltersThemAsBooleans()
     {
-        $option = new ExceptionsOption();
+        $option = new Exceptions();
         $options = $this->getMock('Predis\Configuration\OptionsInterface');
 
         $this->assertFalse($option->filter($options, null));
@@ -57,7 +57,7 @@ class ExceptionsOptionTest extends PredisTestCase
      */
     public function testReturnsFalesOnValuesNotParsableAsBooleans()
     {
-        $option = new ExceptionsOption();
+        $option = new Exceptions();
         $options = $this->getMock('Predis\Configuration\OptionsInterface');
 
         $this->assertFalse($option->filter($options, new \stdClass()));

+ 127 - 0
tests/Predis/Configuration/Option/PrefixTest.php

@@ -0,0 +1,127 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use PredisTestCase;
+
+/**
+ *
+ */
+class PrefixTest extends PredisTestCase
+{
+    /**
+     * @group disconnected
+     */
+    public function testDefaultOptionValue()
+    {
+        $option = new Prefix();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $this->assertNull($option->getDefault($options));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsStringAndReturnsCommandProcessor()
+    {
+        $option = new Prefix();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $prefix = $option->filter($options, $value = 'prefix:');
+
+        $this->assertInstanceOf('Predis\Command\Processor\ProcessorInterface', $prefix);
+        $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $prefix);
+        $this->assertSame($value, $prefix->getPrefix());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCommandProcessorInstance()
+    {
+        $option = new Prefix();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $processor = $this->getMock('Predis\Command\Processor\ProcessorInterface');
+
+        $prefix = $option->filter($options, $processor);
+
+        $this->assertSame($processor, $prefix);
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableReturningProcessorInterface()
+    {
+        $option = new Prefix();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $processor = $this->getMock('Predis\Command\Processor\ProcessorInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($processor));
+
+        $prefix = $option->filter($options, $callable);
+
+        $this->assertSame($processor, $prefix);
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableReturningStringPrefix()
+    {
+        $option = new Prefix();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue('pfx:'));
+
+        $prefix = $option->filter($options, $callable);
+
+        $this->assertInstanceOf('Predis\Command\Processor\ProcessorInterface', $prefix);
+        $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $prefix);
+        $this->assertSame('pfx:', $prefix->getPrefix());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsObjectAsPrefixAndCastsToString()
+    {
+        $option = new Prefix();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $input = $this->getMock('stdClass', array('__toString'));
+        $input->expects($this->once())
+              ->method('__toString')
+              ->will($this->returnValue('pfx:'));
+
+        $prefix = $option->filter($options, $input);
+
+        $this->assertInstanceOf('Predis\Command\Processor\ProcessorInterface', $prefix);
+        $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $prefix);
+        $this->assertSame('pfx:', $prefix->getPrefix());
+    }
+}

+ 190 - 0
tests/Predis/Configuration/Option/ReplicationTest.php

@@ -0,0 +1,190 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Configuration\Option;
+
+use PredisTestCase;
+
+/**
+ *
+ */
+class ReplicationTest extends PredisTestCase
+{
+    /**
+     * @group disconnected
+     */
+    public function testDefaultOptionValue()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $this->assertInstanceOf('Closure', $initializer = $option->getDefault($options));
+        $this->assertInstanceOf('Predis\Connection\Replication\MasterSlaveReplication', $initializer($options));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testConfiguresAutomaticDiscoveryWhenAutodiscoveryOptionIsPresent()
+    {
+        $option = new Replication();
+
+        $connectionFactory = $this->getMock('Predis\Connection\FactoryInterface');
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $options->expects($this->at(0))
+                ->method('__get')
+                ->with('autodiscovery')
+                ->will($this->returnValue(true));
+        $options->expects($this->at(1))
+                ->method('__get')
+                ->with('connections')
+                ->will($this->returnValue($connectionFactory));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->getDefault($options));
+        $this->assertInstanceOf('Predis\Connection\Replication\MasterSlaveReplication', $connection = $initializer($options));
+
+        // TODO: I know, I know...
+        $reflection = new \ReflectionProperty($connection, 'autoDiscovery');
+        $reflection->setAccessible(true);
+
+        $this->assertTrue($reflection->getValue($connection));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsCallableAsConnectionInitializer()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, $callable));
+        $this->assertSame($connection, $initializer($parameters = array()));
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Replication expects a valid connection type returned by callable initializer
+     */
+    public function testThrowsExceptionOnInvalidReturnTypeOfConnectionInitializer()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\NodeConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->once())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, $callable));
+
+        $initializer($parameters = array());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsShortNameStringPredis()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, 'predis'));
+        $this->assertInstanceOf('Predis\Connection\Replication\MasterSlaveReplication', $initializer($parameters = array()));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testAcceptsShortNameStringRedis()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $options->expects($this->at(0))
+                ->method('__get')
+                ->with('service')
+                ->will($this->returnValue('mymaster'));
+        $options->expects($this->at(1))
+                ->method('__get')
+                ->with('connections')
+                ->will($this->returnValue(
+                    $this->getMock('Predis\Connection\FactoryInterface')
+                ));
+
+        $parameters = array(
+            $this->getMock('Predis\Connection\NodeConnectionInterface'),
+        );
+
+        $this->assertInstanceOf('Closure', $initializer = $option->filter($options, 'sentinel'));
+        $this->assertInstanceOf('Predis\Connection\Replication\SentinelReplication', $connection = $initializer($parameters));
+
+        $this->assertSame($parameters[0], $connection->getSentinelConnection());
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage String value for the replication option must be either `predis` or `sentinel`
+     */
+    public function testThrowsExceptionOnInvalidShortNameString()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $option->filter($options, 'unknown');
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Replication expects a valid callable
+     */
+    public function testThrowsExceptionOnBooleanValue()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+
+        $option->filter($options, true);
+    }
+
+    /**
+     * @group disconnected
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Predis\Configuration\Option\Replication expects a valid callable
+     */
+    public function testThrowsExceptionOnInstanceOfReplicationInterface()
+    {
+        $option = new Replication();
+
+        $options = $this->getMock('Predis\Configuration\OptionsInterface');
+        $connection = $this->getMock('Predis\Connection\Cluster\ClusterInterface');
+
+        $option->filter($options, $connection);
+    }
+}

+ 30 - 12
tests/Predis/Configuration/OptionsTest.php

@@ -25,12 +25,13 @@ class OptionsTest extends PredisTestCase
     {
         $options = new Options();
 
-        $this->assertInstanceOf('Predis\Connection\FactoryInterface', $options->connections);
-        $this->assertInstanceOf('Predis\Command\FactoryInterface', $options->commands);
-        $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $options->cluster);
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $options->replication);
         $this->assertTrue($options->exceptions);
         $this->assertNull($options->prefix);
+        $this->assertNull($options->aggregate);
+        $this->assertInstanceOf('Closure', $options->cluster);
+        $this->assertInstanceOf('Closure', $options->replication);
+        $this->assertInstanceOf('Predis\Command\FactoryInterface', $options->commands);
+        $this->assertInstanceOf('Predis\Connection\FactoryInterface', $options->connections);
     }
 
     /**
@@ -38,21 +39,38 @@ class OptionsTest extends PredisTestCase
      */
     public function testConstructorWithArrayArgument()
     {
+        $connection = $this->getMock('Predis\Connection\AggregateConnectionInterface');
+
+        $callable = $this->getMock('stdClass', array('__invoke'));
+        $callable->expects($this->any())
+                 ->method('__invoke')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
+                 ->will($this->returnValue($connection));
+
         $options = new Options(array(
             'exceptions' => false,
             'prefix' => 'prefix:',
             'commands' => $this->getMock('Predis\Command\FactoryInterface'),
             'connections' => $this->getMock('Predis\Connection\FactoryInterface'),
-            'cluster' => $this->getMock('Predis\Connection\Cluster\ClusterInterface'),
-            'replication' => $this->getMock('Predis\Connection\Replication\ReplicationInterface'),
+            'cluster' => $callable,
+            'replication' => $callable,
+            'aggregate' => $callable,
         ));
 
         $this->assertInternalType('bool', $options->exceptions);
-        $this->assertInstanceOf('Predis\Command\FactoryInterface', $options->commands);
+
         $this->assertInstanceOf('Predis\Command\Processor\ProcessorInterface', $options->prefix);
+        $this->assertInstanceOf('Predis\Command\FactoryInterface', $options->commands);
         $this->assertInstanceOf('Predis\Connection\FactoryInterface', $options->connections);
-        $this->assertInstanceOf('Predis\Connection\Cluster\ClusterInterface', $options->cluster);
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $options->replication);
+
+        $this->assertInstanceOf('Closure', $initializer = $options->aggregate);
+        $this->assertSame($connection, $initializer($options, array()));
+
+        $this->assertInstanceOf('Closure', $initializer = $options->cluster);
+        $this->assertSame($connection, $initializer($options, array()));
+
+        $this->assertInstanceOf('Closure', $initializer = $options->replication);
+        $this->assertSame($connection, $initializer($options, array()));
     }
 
     /**
@@ -144,7 +162,7 @@ class OptionsTest extends PredisTestCase
         $callable = $this->getMock('stdClass', array('__invoke'));
         $callable->expects($this->once())
                  ->method('__invoke')
-                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'), 'commands')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
                  ->will($this->returnValue($commands));
 
         $options = new Options(array(
@@ -166,7 +184,7 @@ class OptionsTest extends PredisTestCase
         $callable = $this->getMock('stdClass', array('__invoke'));
         $callable->expects($this->once())
                  ->method('__invoke')
-                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'), 'custom')
+                 ->with($this->isInstanceOf('Predis\Configuration\OptionsInterface'))
                  ->will($this->returnValue($custom));
 
         $options = new Options(array(
@@ -190,7 +208,7 @@ class OptionsTest extends PredisTestCase
         }, true, false);
 
         try {
-            $options = new Options(array('prefix' => 'pfx'));
+            $options = new Options(array('custom' => 'value'));
             $pfx = $options->prefix;
         } catch (\Exception $_) {
             spl_autoload_unregister($autoload);

+ 0 - 60
tests/Predis/Configuration/PrefixOptionTest.php

@@ -1,60 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use PredisTestCase;
-
-/**
- *
- */
-class PrefixOptionTest extends PredisTestCase
-{
-    /**
-     * @group disconnected
-     */
-    public function testDefaultOptionValue()
-    {
-        $option = new PrefixOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $this->assertNull($option->getDefault($options));
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAcceptsStringAndReturnsCommandProcessor()
-    {
-        $option = new PrefixOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $return = $option->filter($options, $value = 'prefix:');
-
-        $this->assertInstanceOf('Predis\Command\Processor\ProcessorInterface', $return);
-        $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $return);
-        $this->assertSame($value, $return->getPrefix());
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAcceptsCommandProcessorInstance()
-    {
-        $option = new PrefixOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $processor = $this->getMock('Predis\Command\Processor\ProcessorInterface');
-
-        $return = $option->filter($options, $processor);
-
-        $this->assertSame($processor, $return);
-    }
-}

+ 0 - 95
tests/Predis/Configuration/ReplicationOptionTest.php

@@ -1,95 +0,0 @@
-<?php
-
-/*
- * This file is part of the Predis package.
- *
- * (c) Daniele Alessandri <suppakilla@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Predis\Configuration;
-
-use PredisTestCase;
-
-/**
- *
- */
-class ReplicationOptionTest extends PredisTestCase
-{
-    /**
-     * @group disconnected
-     */
-    public function testDefaultOptionValue()
-    {
-        $option = new ReplicationOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $option->getDefault($options));
-        $this->assertInstanceOf('Predis\Connection\Replication\MasterSlaveReplication', $option->getDefault($options));
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testAcceptsValuesThatCanBeInterpretedAsBooleans()
-    {
-        $option = new ReplicationOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-
-        $this->assertNull($option->filter($options, null));
-
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $option->filter($options, true));
-        $this->assertNull($option->filter($options, false));
-
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $option->filter($options, 1));
-        $this->assertNull($option->filter($options, 0));
-
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $option->filter($options, 'true'));
-        $this->assertNull($option->filter($options, 'false'));
-
-        $this->assertInstanceOf('Predis\Connection\Replication\ReplicationInterface', $option->filter($options, 'on'));
-        $this->assertNull($option->filter($options, 'off'));
-    }
-
-    /**
-     * @group disconnected
-     */
-    public function testConfiguresAutomaticDiscoveryWhenAutodiscoveryOptionIsPresent()
-    {
-        $option = new ReplicationOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $connFactory = $this->getMock('Predis\Connection\FactoryInterface');
-
-        $options->expects($this->at(0))
-                ->method('__get')
-                ->with('autodiscovery')
-                ->will($this->returnValue(true));
-        $options->expects($this->at(1))
-                ->method('__get')
-                ->with('connections')
-                ->will($this->returnValue($connFactory));
-
-        $replication = $option->getDefault($options);
-
-        // TODO: I know, I know...
-        $reflection = new \ReflectionProperty($replication, 'autoDiscovery');
-        $reflection->setAccessible(true);
-
-        $this->assertTrue($reflection->getValue($replication));
-    }
-
-    /**
-     * @group disconnected
-     * @expectedException \InvalidArgumentException
-     */
-    public function testThrowsExceptionOnInvalidInstanceType()
-    {
-        $option = new ReplicationOption();
-        $options = $this->getMock('Predis\Configuration\OptionsInterface');
-        $value = $this->getMock('Predis\Connection\NodeConnectionInterface');
-
-        $option->filter($options, $value);
-    }
-}