diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 2b5ba40d..41e81255 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -161,6 +161,7 @@ use Predis\Command\CommandInterface; * @method $this geopos($key, array $members) * @method $this geodist($key, $member1, $member2, $unit = null) * @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null) + * @method $this georadiusbymember($key, $member, $radius, $unit, array $options = null) * * @author Daniele Alessandri */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 3db635da..a7410e99 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -169,6 +169,7 @@ use Predis\Profile\ProfileInterface; * @method array geopos($key, array $members) * @method string geodist($key, $member1, $member2, $unit = null) * @method array georadius($key, $longitude, $latitude, $radius, $unit, array $options = null) + * @method array georadiusbymember($key, $member, $radius, $unit, array $options = null) * * @author Daniele Alessandri */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index 4f826e52..8d8ae8e8 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -172,6 +172,7 @@ abstract class ClusterStrategy implements StrategyInterface 'GEOPOS' => $getKeyFromFirstArgument, 'GEODIST' => $getKeyFromFirstArgument, 'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'), + 'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'), ); } @@ -302,7 +303,7 @@ abstract class ClusterStrategy implements StrategyInterface } /** - * Extracts the key from GEORADIUS command. + * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands. * * @param CommandInterface $command Command instance. * @@ -312,11 +313,12 @@ abstract class ClusterStrategy implements StrategyInterface { $arguments = $command->getArguments(); $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; - if ($argc > 5) { + if ($argc > $startIndex) { $keys = array($arguments[0]); - for ($i = 5; $i < $argc; $i++) { + for ($i = $startIndex; $i < $argc; $i++) { $argument = strtoupper($arguments[$i]); if ($argument === 'STORE' || $argument === 'STOREDIST') { $keys[] = $arguments[++$i]; diff --git a/src/Command/GeospatialGeoRadiusByMember.php b/src/Command/GeospatialGeoRadiusByMember.php new file mode 100644 index 00000000..abfff7b4 --- /dev/null +++ b/src/Command/GeospatialGeoRadiusByMember.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/georadiusbymember + * + * @author Daniele Alessandri + */ +class GeospatialGeoRadiusByMember extends GeospatialGeoRadius +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEORADIUSBYMEMBER'; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index 53c0a0f0..a7f2d446 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -165,6 +165,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'GEOPOS' => 'static::first', 'GEODIST' => 'static::first', 'GEORADIUS' => 'static::georadius', + 'GEORADIUSBYMEMBER' => 'static::georadius', ); } @@ -429,9 +430,10 @@ class KeyPrefixProcessor implements ProcessorInterface { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; - if (($count = count($arguments)) > 5) { - for ($i = 5; $i < $count; ++$i) { + if (($count = count($arguments)) > $startIndex) { + for ($i = $startIndex; $i < $count; ++$i) { switch (strtoupper($arguments[$i])) { case 'STORE': case 'STOREDIST': diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index c55d68a5..7de79573 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -275,6 +275,7 @@ class RedisVersion320 extends RedisProfile 'GEOPOS' => 'Predis\Command\GeospatialGeoPos', 'GEODIST' => 'Predis\Command\GeospatialGeoDist', 'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius', + 'GEORADIUSBYMEMBER' => 'Predis\Command\GeospatialGeoRadiusByMember', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index b4337c37..fd6abac8 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -141,9 +141,10 @@ class ReplicationStrategy { $arguments = $command->getArguments(); $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; - if ($argc > 5) { - for ($i = 5; $i < $argc; $i++) { + if ($argc > $startIndex) { + for ($i = $startIndex; $i < $argc; $i++) { $argument = strtoupper($arguments[$i]); if ($argument === 'STORE' || $argument === 'STOREDIST') { return false; @@ -287,6 +288,7 @@ class ReplicationStrategy 'GEOPOS' => true, 'GEODIST' => true, 'GEORADIUS' => array($this, 'isGeoradiusReadOnly'), + 'GEORADIUSBYMEMBER' => array($this, 'isGeoradiusReadOnly'), ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index b0108a30..40e45708 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -169,6 +169,23 @@ class PredisStrategyTest extends PredisTestCase $this->assertNotNull($strategy->getSlot($command), $commandID); } + /** + * @group disconnected + */ + public function testKeysForGeoradiusByMemberCommand() + { + $strategy = $this->getClusterStrategy(); + $profile = Profile\Factory::getDevelopment(); + + $commandID = 'GEORADIUSBYMEMBER'; + + $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km')); + $this->assertNotNull($strategy->getSlot($command), $commandID); + + $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3')); + $this->assertNotNull($strategy->getSlot($command), $commandID); + } + /** * @group disconnected */ @@ -401,6 +418,7 @@ class PredisStrategyTest extends PredisTestCase 'GEOPOS' => 'keys-first', 'GEODIST' => 'keys-first', 'GEORADIUS' => 'keys-georadius', + 'GEORADIUSBYMEMBER' => 'keys-georadius', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index f1b856ba..216c519e 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -182,6 +182,23 @@ class RedisStrategyTest extends PredisTestCase $this->assertNotNull($strategy->getSlot($command), $commandID); } + /** + * @group disconnected + */ + public function testKeysForGeoradiusByMemberCommand() + { + $strategy = $this->getClusterStrategy(); + $profile = Profile\Factory::getDevelopment(); + + $commandID = 'GEORADIUSBYMEMBER'; + + $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km')); + $this->assertNotNull($strategy->getSlot($command), $commandID); + + $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3')); + $this->assertNotNull($strategy->getSlot($command), $commandID); + } + /** * @group disconnected */ @@ -411,6 +428,7 @@ class RedisStrategyTest extends PredisTestCase 'GEOPOS' => 'keys-first', 'GEODIST' => 'keys-first', 'GEORADIUS' => 'keys-georadius', + 'GEORADIUSBYMEMBER' => 'keys-georadius', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php b/tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php new file mode 100644 index 00000000..b5523495 --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @group commands + * @group realm-geospatial + */ +class GeospatialGeoRadiusByMemberTest extends PredisCommandTestCase +{ + /** + * {@inheritdoc} + */ + protected function getExpectedCommand() + { + return 'Predis\Command\GeospatialGeoRadiusByMember'; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedId() + { + return 'GEORADIUSBYMEMBER'; + } + + /** + * @group disconnected + */ + public function testFilterArguments() + { + $arguments = array( + 'Sicily', 'Agrigento', 100, 'km', + 'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist' + ); + + $expected = array( + 'Sicily', 'Agrigento', 100, 'km', + 'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist' + ); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterArgumentsWithComplexOptions() + { + $arguments = array( + 'Sicily', 'Agrigento', 100, 'km', array( + 'store' => 'key:store', + 'storedist' => 'key:storedist', + 'withdist' => true, + 'withcoord' => true, + 'withhash' => true, + 'count' => 1, + 'sort' => 'asc', + ), + ); + + $expected = array( + 'Sicily', 'Agrigento', 100, 'km', + 'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist' + ); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterArgumentsWithSpecificOptionsSetToFalse() + { + $arguments = array( + 'Sicily', 'Agrigento', 100, 'km', array( + 'store' => 'key:store', + 'storedist' => 'key:storedist', + 'withdist' => false, + 'withcoord' => false, + 'withhash' => false, + 'count' => 1, + 'sort' => 'asc', + ), + ); + + $expected = array('Sicily', 'Agrigento', 100, 'km', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponseWithNoOptions() + { + $raw = array( + array('Agrigento', 'Palermo'), + ); + + $expected = array( + array('Agrigento', 'Palermo'), + ); + + $command = $this->getCommand(); + + $this->assertSame($expected, $command->parseResponse($raw)); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + */ + public function testCommandReturnsGeoRadiusInfoWithNoOptions() + { + $redis = $this->getClient(); + + $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania', '13.583333', '37.316667', 'Agrigento'); + $this->assertEquals(array('Agrigento', 'Palermo'), $redis->georadiusbymember('Sicily', 'Agrigento', 100, 'km')); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + */ + public function testCommandReturnsGeoRadiusInfoWithOptions() + { + $redis = $this->getClient(); + + $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania', '13.583333', '37.316667', 'Agrigento'); + $this->assertEquals(array( + array('Agrigento', '0.0000', array('13.5833314061164856', '37.31666804993816555')), + array('Palermo', '90.9778', array('13.361389338970184', '38.115556395496299')), + ), $redis->georadiusbymember('Sicily', 'Agrigento', 100, 'km', 'WITHDIST', 'WITHCOORD')); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + * @expectedException \Predis\Response\ServerException + * @expectedExceptionMessage Operation against a key holding the wrong kind of value + */ + public function testThrowsExceptionOnWrongType() + { + $redis = $this->getClient(); + + $redis->lpush('Sicily', 'Palermo'); + $redis->georadiusbymember('Sicily', 'Agrigento', 200, 'km'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 7de8802b..29a5b0e0 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -897,6 +897,14 @@ class KeyPrefixProcessorTest extends PredisTestCase array('key', '15', '37', '200', 'km', 'WITHDIST', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'), array('prefix:key', '15', '37', '200', 'km', 'WITHDIST', 'STORE', 'prefix:key:store', 'STOREDIST', 'prefix:key:storedist'), ), + array('GEORADIUSBYMEMBER', + array('key', 'member', '100', 'km'), + array('prefix:key', 'member', '100', 'km'), + ), + array('GEORADIUSBYMEMBER', + array('key', 'member', '100', 'km', 'WITHDIST', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'), + array('prefix:key', 'member', '100', 'km', 'WITHDIST', 'STORE', 'prefix:key:store', 'STOREDIST', 'prefix:key:storedist'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 13dd93de..75ffbf58 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -196,6 +196,7 @@ class RedisUnstableTest extends PredisProfileTestCase 155 => 'GEOPOS', 156 => 'GEODIST', 157 => 'GEORADIUS', + 158 => 'GEORADIUSBYMEMBER', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index 28780a1e..d68df913 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -196,6 +196,7 @@ class RedisVersion320Test extends PredisProfileTestCase 155 => 'GEOPOS', 156 => 'GEODIST', 157 => 'GEORADIUS', + 158 => 'GEORADIUSBYMEMBER', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index 8caae3b6..b1237c17 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -166,6 +166,33 @@ class ReplicationStrategyTest extends PredisTestCase ); } + /** + * @group disconnected + */ + public function testGeoradiusByMemberCommand() + { + $profile = Profile\Factory::getDevelopment(); + $strategy = new ReplicationStrategy(); + + $command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km')); + $this->assertTrue( + $strategy->isReadOperation($command), + 'GEORADIUSBYMEMBER is expected to be a read operation.' + ); + + $command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km', 'store', 'key:store')); + $this->assertFalse( + $strategy->isReadOperation($command), + 'GEORADIUSBYMEMBER with STORE is expected to be a write operation.' + ); + + $command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km', 'storedist', 'key:storedist')); + $this->assertFalse( + $strategy->isReadOperation($command), + 'GEORADIUSBYMEMBER with STOREDIST is expected to be a write operation.' + ); + } + /** * @group disconnected * @expectedException \Predis\NotSupportedException @@ -472,6 +499,7 @@ class ReplicationStrategyTest extends PredisTestCase 'GEOPOS' => 'read', 'GEODIST' => 'read', 'GEORADIUS' => 'variable', + 'GEORADIUSBYMEMBER' => 'variable', ); if (isset($type)) {