1
0
mirror of https://github.com/guzzle/guzzle.git synced 2025-02-24 18:13:00 +01:00

[Service] Allowing commands to be sent more than once through a client

Allowing cloned commands that have previously been sent to be sent again.
Redesigning the ResourceIterator class so that it accepts a command and iterates over the command until the sendRequest() method returns an empty array.
Adding the name of the parameter that received a type validation error to Inpector error messages
This commit is contained in:
Michael Dowling 2012-05-08 14:28:45 -07:00
parent f7e6aa551a
commit f5a9e9bf82
11 changed files with 251 additions and 182 deletions

View File

@ -7,6 +7,7 @@ use Guzzle\Common\Collection;
use Guzzle\Common\Exception\InvalidArgumentException;
use Guzzle\Common\Exception\BadMethodCallException;
use Guzzle\Http\Client as HttpClient;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Service\Exception\CommandSetException;
use Guzzle\Service\Command\CommandInterface;
use Guzzle\Service\Command\CommandSet;
@ -178,14 +179,23 @@ class Client extends HttpClient implements ClientInterface
public function execute($command)
{
if ($command instanceof CommandInterface) {
$command->setClient($this)->prepare();
$this->dispatch('command.before_send', array(
'command' => $command
));
$command->getRequest()->send();
$request = $command->getRequest();
// Set the state to new if the command was previously executed
if ($request->getState() !== RequestInterface::STATE_NEW) {
$request->setState(RequestInterface::STATE_NEW);
}
$request->send();
$this->dispatch('command.after_send', array(
'command' => $command
));
return $command->getResult();
} else if ($command instanceof CommandSet) {
foreach ($command as $c) {

View File

@ -66,6 +66,15 @@ abstract class AbstractCommand extends Collection implements CommandInterface
$this->init();
}
/**
* Custom clone behavior
*/
public function __clone()
{
$this->request = null;
$this->result = null;
}
/**
* Enables magic methods for setting parameters.
*

View File

@ -269,7 +269,7 @@ class Inspector
if ($validate && $this->typeValidation && $configValue !== null && $argType = $arg->getType()) {
$validation = $this->validateConstraint($argType, $configValue);
if ($validation !== true) {
$errors[] = $validation;
$errors[] = $name . ': ' . $validation;
continue;
}
}

View File

@ -3,6 +3,7 @@
namespace Guzzle\Service;
use Guzzle\Common\AbstractHasDispatcher;
use Guzzle\Service\Command\CommandInterface;
/**
* Iterate over a paginated set of resources that requires subsequent paginated
@ -11,40 +12,35 @@ use Guzzle\Common\AbstractHasDispatcher;
abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterator, \Countable
{
/**
* @var ClientInterface
* @var CommandInterface Command used to send requests
*/
protected $client;
protected $command;
/**
* @var mixed Current iterator value
* @var CommandInterface First sent command
*/
protected $current;
protected $originalCommand;
/**
* @var array
* @var array Currently loaded resources
*/
protected $resourceList;
protected $resources;
/**
* @var int Current index in $resourceList
* @var int Total number of resources that have been retrieved
*/
protected $currentIndex = -1;
/**
* @var int Current number of resources that have been iterated
*/
protected $pos = -1;
/**
* @var string NextToken/Marker for a subsequent request
*/
protected $nextToken;
protected $retrievedCount = 0;
/**
* @var int Total number of resources that have been iterated
*/
protected $iteratedCount = 0;
/**
* @var string NextToken/Marker for a subsequent request
*/
protected $nextToken = false;
/**
* @var int Maximum number of resources to fetch per request
*/
@ -56,7 +52,7 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
protected $limit;
/**
* @var array Initial data passed to the constructor -- used with rewind()
* @var array Initial data passed to the constructor
*/
protected $data = array();
@ -76,43 +72,33 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
/**
* This should only be invoked by a {@see ClientInterface} object.
*
* @param ClientInterface $client Client responsible for sending requests
* @param CommandInterface $command Initial command used for iteration
*
* @param array $data Associative array of additional parameters, including
* any initial data to be iterated.
* @param array $data (optional) Associative array of additional parameters,
* including any initial data to be iterated:
*
* <ul>
* <li>page_size => Max number of results to retrieve per request.</li>
* <li>resources => Initial resources to associate with the iterator.</li>
* <li>next_token => The value used to mark the beginning of a subsequent result set.</li>
* </ul>
* limit: Attempt to limit the maximum number of resources to this amount
* page_size: Attempt to retrieve this number of resources per request
*/
public function __construct(ClientInterface $client, array $data)
public function __construct(CommandInterface $command, array $data = array())
{
$this->client = $client;
// Clone the command to keep track of the originating command for rewind
$this->originalCommand = $command;
// Parse options from the array of options
$this->data = $data;
$this->limit = array_key_exists('limit', $data) ? $data['limit'] : -1;
$this->limit = array_key_exists('limit', $data) ? $data['limit'] : 0;
$this->pageSize = array_key_exists('page_size', $data) ? $data['page_size'] : false;
$this->resourceList = array_key_exists('resources', $data) ? $data['resources'] : array();
$this->nextToken = array_key_exists('next_token', $data) ? $data['next_token'] : false;
$this->retrievedCount = count($this->resourceList);
$this->onLoad();
}
/**
* Get all of the resources as an array (be careful as this could issue a
* large number of requests if no limit is specified)
*
* @param bool $rewind (optional) By default, rewind() will be called
*
* @return array
*/
public function toArray($rewind = true)
public function toArray()
{
if ($rewind) {
$this->rewind();
}
return iterator_to_array($this, false);
}
@ -123,15 +109,23 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
*/
public function current()
{
return $this->current;
return $this->resources ? current($this->resources) : false;
}
/**
* Return the key of the current element.
*
* @return mixed
*/
public function key()
{
return max(0, $this->iteratedCount - 1);
}
/**
* Return the total number of items that have been retrieved thus far.
*
* Implements Countable
*
* @return string Returns the total number of items retrieved.
* @return int
*/
public function count()
{
@ -139,37 +133,16 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
}
/**
* Return the total number of items that have been iterated thus far.
*
* @return string Returns the total number of items iterated.
*/
public function getPosition()
{
return $this->pos;
}
/**
* Return the key of the current element.
*
* @return string Returns the current key.
*/
public function key()
{
// @codeCoverageIgnoreStart
return $this->currentIndex;
// @codeCoverageIgnoreEnd
}
/**
* Rewind the Iterator to the first element.
* Rewind the Iterator to the first element and send the original command
*/
public function rewind()
{
$this->currentIndex = -1;
$this->pos = -1;
$this->resourceList = $this->data['resources'];
$this->nextToken = $this->data['next_token'];
$this->retrievedCount = count($this->resourceList);
// Use the original command
$this->command = clone $this->originalCommand;
$this->iteratedCount = 0;
$this->retrievedCount = 0;
$this->nextToken = false;
$this->resources = null;
$this->next();
}
@ -180,10 +153,8 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
*/
public function valid()
{
return isset($this->resourceList)
&& $this->current
&& ($this->pos < $this->limit || $this->limit == -1)
&& ($this->currentIndex < count($this->resourceList) || $this->nextToken);
return (!$this->resources || $this->current() || $this->nextToken)
&& (!$this->limit || $this->iteratedCount < $this->limit + 1);
}
/**
@ -191,26 +162,39 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
*/
public function next()
{
$this->pos++;
$this->iteratedCount++;
if (!isset($this->resourceList)
|| ++$this->currentIndex >= count($this->resourceList)
&& $this->nextToken
&& ($this->limit == -1 || $this->pos < $this->limit)) {
$this->dispatch('resource_iterator.before_send', array(
'iterator' => $this,
'resources' => $this->resourceList
));
$this->sendRequest();
$this->dispatch('resource_iterator.after_send', array(
'iterator' => $this,
'resources' => $this->resourceList
));
// Check if a new set of resources needs to be retrieved
$sendRequest = false;
if (!$this->resources) {
$sendRequest = true;
} else {
// iterate over the internal array
$current = next($this->resources);
$sendRequest = $current === false && $this->nextToken && (!$this->limit || $this->iteratedCount < $this->limit + 1);
}
$this->current = (array_key_exists($this->currentIndex, $this->resourceList))
? $this->resourceList[$this->currentIndex]
: null;
if ($sendRequest) {
$this->dispatch('resource_iterator.before_send', array(
'iterator' => $this,
'resources' => $this->resources
));
// Get a new command object from the original command
$this->command = clone $this->originalCommand;
// Send a request and retrieve the newly loaded resources
$this->resources = $this->sendRequest() ?: array();
// Add to the number of retrieved resources
$this->retrievedCount += count($this->resources);
// Ensure that we rewind to the beginning of the array
reset($this->resources);
$this->dispatch('resource_iterator.after_send', array(
'iterator' => $this,
'resources' => $this->resources
));
}
}
/**
@ -223,12 +207,6 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
return $this->nextToken;
}
/**
* Send a request to retrieve the next page of results.
* Hook for sublasses to implement.
*/
abstract protected function sendRequest();
/**
* Returns the value that should be specified for the page size for
* a request that will maintain any hard limits, but still honor the
@ -239,21 +217,18 @@ abstract class ResourceIterator extends AbstractHasDispatcher implements \Iterat
*/
protected function calculatePageSize()
{
if ($this->limit == -1) {
return $this->pageSize;
} else if ($this->pos + $this->pageSize > $this->limit) {
return $this->limit - $this->pos;
if ($this->limit && $this->iteratedCount + $this->pageSize > $this->limit) {
return 1 + ($this->limit - $this->iteratedCount);
}
// @codeCoverageIgnoreStart
return $this->pageSize;
// @codeCoverageIgnoreEnd
return (int) $this->pageSize;
}
/**
* Called when the iterator is constructed.
* Send a request to retrieve the next page of results.
* Hook for sublasses to implement.
*
* Hook for sub-classes to implement.
* @return array Returns the newly loaded resources
*/
protected function onLoad() {}
abstract protected function sendRequest();
}

View File

@ -92,8 +92,8 @@ class ClientTest extends \Guzzle\Tests\GuzzleTestCase
{
$client = new Client('http://www.test.com/');
$client->getEventDispatcher()->addSubscriber(new MockPlugin(array(
new \Guzzle\Http\Message\Response(200),
new \Guzzle\Http\Message\Response(200)
new Response(200),
new Response(200)
)));
// Create a command set and a command
@ -143,7 +143,7 @@ class ClientTest extends \Guzzle\Tests\GuzzleTestCase
{
$client = new Client('http://www.test.com/');
$client->getEventDispatcher()->addSubscriber(new MockPlugin(array(
new \Guzzle\Http\Message\Response(200)
new Response(200)
)));
// Create a command set and a command
@ -319,4 +319,23 @@ class ClientTest extends \Guzzle\Tests\GuzzleTestCase
$command = $client->getCommand('other_command');
$this->assertTrue($command->get('command.magic_method_call'));
}
/**
* @covers Guzzle\Service\Client::execute
*/
public function testClientResetsRequestsBeforeExecutingCommands()
{
$this->getServer()->flush();
$this->getServer()->enqueue(array(
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nHi",
"HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nI"
));
$client = new Mock\MockClient($this->getServer()->getUrl());
$command = $client->getCommand('mock_command');
$client->execute($command);
$client->execute($command);
$this->assertEquals('I', $command->getResponse()->getBody(true));
}
}

View File

@ -307,4 +307,22 @@ class CommandTest extends AbstractCommandTest
$command->setTest('foo');
$this->assertEquals('foo', $command->get('test'));
}
/**
* @covers Guzzle\Service\Command\AbstractCommand::__clone
*/
public function testCloneMakesNewRequest()
{
$client = $this->getClient();
$command = new MockCommand(array(
'command.magic_method_call' => true
), $this->getApiCommand());
$command->setClient($client);
$command->prepare();
$this->assertTrue($command->isPrepared());
$command2 = clone $command;
$this->assertFalse($command2->isPrepared());
}
}

View File

@ -199,8 +199,8 @@ EOT;
$this->assertContains("Validation errors: Requires that the username argument be supplied. (API username)", $concat);
$this->assertContains("Requires that the password argument be supplied. (API password)", $concat);
$this->assertContains("Requires that the subdomain argument be supplied. (Unfuddle project subdomain)", $concat);
$this->assertContains("Value must be one of: v1, v2, v3", $concat);
$this->assertContains("Value must be of type object", $concat);
$this->assertContains("api_version: Value must be one of: v1, v2, v3", $concat);
$this->assertContains("class: Value must be of type object", $concat);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Guzzle\Tests\Service\Mock\Command;
/**
* Iterable mock command
*
* @guzzle page_size type="integer"
* @guzzle next_token type="string"
*/
class IterableCommand extends MockCommand
{
/**
* {@inheritdoc}
*/
protected function build()
{
$this->request = $this->client->createRequest('GET');
// Add the next token and page size query string values
$this->request->getQuery()->set('next_token', $this->get('next_token'));
if ($this->get('page_size')) {
$this->request->getQuery()->set('page_size', $this->get('page_size'));
}
}
}

View File

@ -8,13 +8,17 @@ class MockResourceIterator extends ResourceIterator
{
protected function sendRequest()
{
$request = $this->client->createRequest();
$request->getQuery()->set('count', $this->calculatePageSize());
$data = json_decode($request->send()->getBody(true), true);
if ($this->nextToken) {
$this->command->set('next_token', $this->nextToken);
}
$this->command->set('page_size', (int) $this->calculatePageSize());
$this->command->execute();
$data = json_decode($this->command->getResponse()->getBody(true), true);
$this->resourceList = $data['resources'];
$this->nextToken = $data['next_token'];
$this->retrievedCount += count($this->data['resources']);
$this->currentIndex = 0;
return $data['resources'];
}
}

View File

@ -31,10 +31,9 @@ class ResourceIteratorApplyBatchedTest extends \Guzzle\Tests\GuzzleTestCase
"HTTP/1.1 200 OK\r\nContent-Length: 41\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"j\"] }",
));
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock'), array(
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command'), array(
'page_size' => 3,
'resources' => array('a', 'b', 'c'),
'next_token' => 'd'
'limit' => 7
));
$received = array();
@ -46,16 +45,15 @@ class ResourceIteratorApplyBatchedTest extends \Guzzle\Tests\GuzzleTestCase
$requests = $this->getServer()->getReceivedRequests(true);
$this->assertEquals(3, count($requests));
$this->assertEquals(3, $requests[0]->getQuery()->get('count'));
$this->assertEquals(3, $requests[1]->getQuery()->get('count'));
$this->assertEquals(3, $requests[2]->getQuery()->get('count'));
$this->assertEquals(3, $requests[0]->getQuery()->get('page_size'));
$this->assertEquals(3, $requests[1]->getQuery()->get('page_size'));
$this->assertEquals(1, $requests[2]->getQuery()->get('page_size'));
$this->assertEquals(array('a', 'b', 'c'), array_values($received[0]));
$this->assertEquals(array('d', 'e', 'f'), array_values($received[1]));
$this->assertEquals(array('g', 'h', 'i'), array_values($received[2]));
$this->assertEquals(array('j'), array_values($received[3]));
$this->assertEquals(array('d', 'e', 'f'), array_values($received[0]));
$this->assertEquals(array('g', 'h', 'i'), array_values($received[1]));
$this->assertEquals(array('j'), array_values($received[2]));
$this->assertEquals(4, $apply->getBatchCount());
$this->assertEquals(10, $apply->getIteratedCount());
$this->assertEquals(3, $apply->getBatchCount());
$this->assertEquals(7, $apply->getIteratedCount());
}
}

View File

@ -24,39 +24,15 @@ class ResourceIteratorTest extends \Guzzle\Tests\GuzzleTestCase
public function testConstructorConfiguresDefaults()
{
$ri = $this->getMockForAbstractClass('Guzzle\\Service\\ResourceIterator', array(
$this->getServiceBuilder()->get('mock'),
$this->getServiceBuilder()->get('mock')->getCommand('iterable_command'),
array(
'limit' => 10,
'page_size' => 3,
'resources' => array('a', 'b', 'c'),
'next_token' => 'd'
'page_size' => 3
)
), 'MockIterator');
$this->assertEquals('d', $ri->getNextToken());
$this->assertEquals(array('a', 'b', 'c'), $ri->toArray());
$ri->rewind();
$this->assertEquals('a', $ri->current());
$ri->next();
$this->assertEquals('b', $ri->current());
$ri->next();
$this->assertEquals('c', $ri->current());
// It ran out
$ri->next();
$this->assertEquals('', $ri->current());
$this->assertEquals(3, count($ri));
$this->assertEquals(3, $ri->getPosition());
// Rewind works?
$ri->rewind();
$this->assertEquals('a', $ri->current());
$ri->next();
$this->assertEquals('b', $ri->current());
$ri->next();
$this->assertEquals('c', $ri->current());
$this->assertEquals(false, $ri->getNextToken());
$this->assertEquals(false, $ri->current());
}
/**
@ -64,25 +40,30 @@ class ResourceIteratorTest extends \Guzzle\Tests\GuzzleTestCase
*/
public function testSendsRequestsForNextSetOfResources()
{
// Queue up an array of responses for iterating
$this->getServer()->flush();
$this->getServer()->enqueue(array(
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\", \"i\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 41\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"j\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 41\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"j\"] }"
));
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock'), array(
'page_size' => 3,
'resources' => array('a', 'b', 'c'),
'next_token' => 'd'
// Create a new resource iterator using the IteraableCommand mock
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command'), array(
'page_size' => 3
));
$this->assertEquals(array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), $ri->toArray());
// Ensure that no requests have been sent yet
$this->assertEquals(0, count($this->getServer()->getReceivedRequests(false)));
//$this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i', 'j'), $ri->toArray());
$ri->toArray();
$requests = $this->getServer()->getReceivedRequests(true);
$this->assertEquals(3, count($requests));
$this->assertEquals(3, $requests[0]->getQuery()->get('count'));
$this->assertEquals(3, $requests[1]->getQuery()->get('count'));
$this->assertEquals(3, $requests[2]->getQuery()->get('count'));
$this->assertEquals(3, $requests[0]->getQuery()->get('page_size'));
$this->assertEquals(3, $requests[1]->getQuery()->get('page_size'));
$this->assertEquals(3, $requests[2]->getQuery()->get('page_size'));
// Reset and resend
$this->getServer()->flush();
@ -91,12 +72,13 @@ class ResourceIteratorTest extends \Guzzle\Tests\GuzzleTestCase
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\", \"i\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 41\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"j\"] }",
));
$d = array();
reset($ri);
foreach ($ri as $data) {
$d[] = $data;
}
$this->assertEquals(array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), $d);
$this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i', 'j'), $d);
}
/**
@ -108,20 +90,47 @@ class ResourceIteratorTest extends \Guzzle\Tests\GuzzleTestCase
$this->getServer()->enqueue(array(
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\", \"i\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"g\", \"h\"] }"
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"j\", \"resources\": [\"j\", \"k\"] }"
));
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock'), array(
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command'), array(
'page_size' => 3,
'limit' => 8,
'resources' => array('a', 'b', 'c'),
'next_token' => 'd'
'limit' => 7
));
$this->assertEquals(array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), $ri->toArray());
$this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i', 'j'), $ri->toArray());
$requests = $this->getServer()->getReceivedRequests(true);
$this->assertEquals(2, count($requests));
$this->assertEquals(3, $requests[0]->getQuery()->get('count'));
$this->assertEquals(2, $requests[1]->getQuery()->get('count'));
$this->assertEquals(3, count($requests));
$this->assertEquals(3, $requests[0]->getQuery()->get('page_size'));
$this->assertEquals(3, $requests[1]->getQuery()->get('page_size'));
$this->assertEquals(1, $requests[2]->getQuery()->get('page_size'));
}
/**
* @covers Guzzle\Service\ResourceIterator
*/
public function testUseAsArray()
{
$this->getServer()->flush();
$this->getServer()->enqueue(array(
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"g\", \"resources\": [\"d\", \"e\", \"f\"] }",
"HTTP/1.1 200 OK\r\nContent-Length: 52\r\n\r\n{ \"next_token\": \"\", \"resources\": [\"g\", \"h\", \"i\"] }"
));
$ri = new MockResourceIterator($this->getServiceBuilder()->get('mock')->getCommand('iterable_command'));
// Ensure that the key is never < 0
$this->assertEquals(0, $ri->key());
$this->assertEquals(0, count($ri));
// Ensure that the iterator can be used as KVP array
$data = array();
foreach ($ri as $key => $value) {
$data[$key] = $value;
}
// Ensure that the iterate is countable
$this->assertEquals(6, count($ri));
$this->assertEquals(array('d', 'e', 'f', 'g', 'h', 'i'), $data);
}
}