mirror of
https://github.com/predis/predis.git
synced 2026-09-04 14:56:47 +00:00
Implement PHP iterator for lists based on the LRANGE command.
This iterator tries to mimic the same behaviour of the cursor-based iterators implemented upon the SCAN family of commands and offering only limited guarantees on the returned elements, but uses LRANGE to fetch items from a list incrementally.
This commit is contained in:
+7
-1
@@ -6,7 +6,13 @@ v0.8.5 (2013-xx-xx)
|
||||
|
||||
- Added `SCAN`, `SSCAN`, `ZSCAN`, `HSCAN` to the server profile for Redis 2.8.
|
||||
|
||||
- Implemented iterator-based abstraction for `SCAN`, `SSCAN`, `ZSCAN`, `HSCAN`.
|
||||
- Implemented PHP iterators for incremental iterations over Redis collections:
|
||||
|
||||
- keyspace (cursor-based iterator using `SCAN`)
|
||||
- sets (cursor-based iterator using `SSCAN`)
|
||||
- sorted sets (cursor-based iterator using `ZSCAN`)
|
||||
- hashes (cursor-based iterator using `HSCAN`)
|
||||
- lists (plain iterator using `LRANGE`)
|
||||
|
||||
- `Predis\Client::pubSubLoop()` should now be used instead of the deprecated
|
||||
`Predis\Client::pubSub()` (which still works like usual to avoid B/C breaks).
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<?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\Collection\Iterator;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Iterator;
|
||||
use Predis\ClientInterface;
|
||||
use Predis\NotSupportedException;
|
||||
|
||||
/**
|
||||
* Abstracts the iteration of items stored in a list by leveraging the LRANGE
|
||||
* command wrapped in a fully-rewindable PHP iterator.
|
||||
*
|
||||
* This iterator tries to emulate the behaviour of cursor-based iterators based
|
||||
* on the SCAN-family of commands introduced in Redis <= 2.8, meaning that due
|
||||
* to its incremental nature with multiple fetches it can only offer limited
|
||||
* guarantees on the returned elements because the collection can change several
|
||||
* times (trimmed, deleted, overwritten) during the iteration process.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
* @link http://redis.io/commands/lrange
|
||||
*/
|
||||
class ListKey implements Iterator
|
||||
{
|
||||
protected $client;
|
||||
protected $count;
|
||||
protected $key;
|
||||
|
||||
protected $valid;
|
||||
protected $fetchmore;
|
||||
protected $elements;
|
||||
protected $position;
|
||||
protected $current;
|
||||
|
||||
/**
|
||||
* @param ClientInterface $client Client connected to Redis.
|
||||
* @param string $key Redis list key.
|
||||
* @param int $count Number of items retrieved on each fetch operation.
|
||||
*/
|
||||
public function __construct(ClientInterface $client, $key, $count = 10)
|
||||
{
|
||||
$this->requiredCommand($client, 'LRANGE');
|
||||
|
||||
if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) {
|
||||
throw new InvalidArgumentException('The $count argument must be a positive integer.');
|
||||
}
|
||||
|
||||
$this->client = $client;
|
||||
$this->key = $key;
|
||||
$this->count = $count;
|
||||
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the client instance supports the specified Redis
|
||||
* command required to fetch elements from the server to perform
|
||||
* the iteration.
|
||||
*
|
||||
* @param ClientInterface Client connected to Redis.
|
||||
* @param string $commandID Command ID.
|
||||
*/
|
||||
protected function requiredCommand(ClientInterface $client, $commandID)
|
||||
{
|
||||
if (!$client->getProfile()->supportsCommand($commandID)) {
|
||||
throw new NotSupportedException("The specified server profile does not support the `$commandID` command.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the inner state of the iterator.
|
||||
*/
|
||||
protected function reset()
|
||||
{
|
||||
$this->valid = true;
|
||||
$this->fetchmore = true;
|
||||
$this->elements = array();
|
||||
$this->position = -1;
|
||||
$this->current = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a new set of elements from the remote collection,
|
||||
* effectively advancing the iteration process.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function executeCommand()
|
||||
{
|
||||
return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the local buffer of elements fetched from the
|
||||
* server during the iteration.
|
||||
*/
|
||||
protected function fetch()
|
||||
{
|
||||
$elements = $this->executeCommand();
|
||||
|
||||
if (count($elements) < $this->count) {
|
||||
$this->fetchmore = false;
|
||||
}
|
||||
|
||||
$this->elements = $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts next values for key() and current().
|
||||
*/
|
||||
protected function extractNext()
|
||||
{
|
||||
$this->position++;
|
||||
$this->current = array_shift($this->elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->reset();
|
||||
$this->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
if (!$this->elements && $this->fetchmore) {
|
||||
$this->fetch();
|
||||
}
|
||||
|
||||
if ($this->elements) {
|
||||
$this->extractNext();
|
||||
} else {
|
||||
$this->valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->valid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?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\Collection\Iterator;
|
||||
|
||||
use \PHPUnit_Framework_TestCase as StandardTestCase;
|
||||
|
||||
use Predis\Client;
|
||||
use Predis\Profile\ServerProfile;
|
||||
|
||||
/**
|
||||
* @group realm-iterators
|
||||
*/
|
||||
class ListKeyTest extends StandardTestCase
|
||||
{
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIterationWithNoResults()
|
||||
{
|
||||
$client = $this->getMock('Predis\Client', array('getProfile', 'lrange'));
|
||||
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
$client->expects($this->once())
|
||||
->method('lrange')
|
||||
->with('key:list', 0, 9)
|
||||
->will($this->returnValue(array()));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list');
|
||||
|
||||
$iterator->rewind();
|
||||
$this->assertFalse($iterator->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIterationOnSingleFetch()
|
||||
{
|
||||
$client = $this->getMock('Predis\Client', array('getProfile', 'lrange'));
|
||||
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
$client->expects($this->once())
|
||||
->method('lrange')
|
||||
->with('key:list', 0, 9)
|
||||
->will($this->returnValue(array('item:1', 'item:2', 'item:3')));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list');
|
||||
|
||||
$iterator->rewind();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:1', $iterator->current());
|
||||
$this->assertSame(0, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:2', $iterator->current());
|
||||
$this->assertSame(1, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:3', $iterator->current());
|
||||
$this->assertSame(2, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertFalse($iterator->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIterationOnMultipleFetches()
|
||||
{
|
||||
$client = $this->getMock('Predis\Client', array('getProfile', 'lrange'));
|
||||
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
$client->expects($this->at(1))
|
||||
->method('lrange')
|
||||
->with('key:list', 0, 9)
|
||||
->will($this->returnValue(array(
|
||||
'item:1', 'item:2', 'item:3', 'item:4', 'item:5', 'item:6', 'item:7', 'item:8', 'item:9', 'item:10'
|
||||
)));
|
||||
$client->expects($this->at(2))
|
||||
->method('lrange')
|
||||
->with('key:list', 10, 19)
|
||||
->will($this->returnValue(array('item:11', 'item:12')));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list');
|
||||
|
||||
for ($i = 1, $iterator->rewind(); $i <= 12; $i++, $iterator->next()) {
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame("item:$i", $iterator->current());
|
||||
$this->assertSame($i - 1, $iterator->key());
|
||||
}
|
||||
|
||||
$this->assertFalse($iterator->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException InvalidArgumentException
|
||||
* @expectedExceptionMessage The $count argument must be a positive integer.
|
||||
*/
|
||||
public function testThrowsExceptionOnConstructorWithNonIntegerCountParameter()
|
||||
{
|
||||
$client = $this->getMock('Predis\ClientInterface');
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list', 'wrong');
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException InvalidArgumentException
|
||||
* @expectedExceptionMessage The $count argument must be a positive integer.
|
||||
*/
|
||||
public function testThrowsExceptionOnConstructorWithNegativeCountParameter()
|
||||
{
|
||||
$client = $this->getMock('Predis\ClientInterface');
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list', 'wrong');
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIterationWithCountParameter()
|
||||
{
|
||||
$client = $this->getMock('Predis\Client', array('getProfile', 'lrange'));
|
||||
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
$client->expects($this->at(1))
|
||||
->method('lrange')
|
||||
->with('key:list', 0, 4)
|
||||
->will($this->returnValue(array('item:1', 'item:2')));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list', 5);
|
||||
|
||||
$iterator->rewind();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:1', $iterator->current());
|
||||
$this->assertSame(0, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:2', $iterator->current());
|
||||
$this->assertSame(1, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertFalse($iterator->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIterationWithCountParameterOnMultipleFetches()
|
||||
{
|
||||
$client = $this->getMock('Predis\Client', array('getProfile', 'lrange'));
|
||||
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
$client->expects($this->at(1))
|
||||
->method('lrange')
|
||||
->with('key:list', 0, 1)
|
||||
->will($this->returnValue(array('item:1', 'item:2')));
|
||||
$client->expects($this->at(2))
|
||||
->method('lrange')
|
||||
->with('key:list', 2, 3)
|
||||
->will($this->returnValue(array('item:3')));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list', 2);
|
||||
|
||||
$iterator->rewind();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:1', $iterator->current());
|
||||
$this->assertSame(0, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:2', $iterator->current());
|
||||
$this->assertSame(1, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:3', $iterator->current());
|
||||
$this->assertSame(2, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertFalse($iterator->valid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIterationRewindable()
|
||||
{
|
||||
$client = $this->getMock('Predis\Client', array('getProfile', 'lrange'));
|
||||
|
||||
$client->expects($this->any())
|
||||
->method('getProfile')
|
||||
->will($this->returnValue(ServerProfile::getDefault()));
|
||||
$client->expects($this->exactly(2))
|
||||
->method('lrange')
|
||||
->with('key:list', 0, 9)
|
||||
->will($this->returnValue(array('item:1', 'item:2')));
|
||||
|
||||
$iterator = new ListKey($client, 'key:list');
|
||||
|
||||
$iterator->rewind();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:1', $iterator->current());
|
||||
$this->assertSame(0, $iterator->key());
|
||||
|
||||
$iterator->rewind();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame('item:1', $iterator->current());
|
||||
$this->assertSame(0, $iterator->key());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertTrue($iterator->valid());
|
||||
$this->assertSame(1, $iterator->key());
|
||||
$this->assertSame('item:2', $iterator->current());
|
||||
|
||||
$iterator->next();
|
||||
$this->assertFalse($iterator->valid());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user