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

Adding a RingBridge for better separation

This commit is contained in:
Michael Dowling 2014-09-17 14:21:40 -07:00
parent 8fe3124bd9
commit 0337b7848a
6 changed files with 291 additions and 213 deletions

View File

@ -224,29 +224,30 @@ class Client implements ClientInterface
$trans = new Transaction($this, $request);
RequestEvents::emitBefore($trans);
// Return a response if one was set during the before event.
if ($trans->response) {
return $trans->response;
}
// Send the request using the Guzzle ring handler
// Send the request using the Guzzle Ring handler
$adapter = $this->adapter;
$response = $adapter(
RequestEvents::createRingRequest($trans, $this->messageFactory)
RingBridge::prepareRingRequest($trans, $this->messageFactory)
);
if ($trans->response) {
return $trans->response;
} elseif ($response instanceof RingFutureInterface) {
if ($response instanceof RingFutureInterface) {
return $this->createFutureResponse($response, $trans);
} elseif ($trans->response) {
// A response can be set during the "then" ring callback.
return $trans->response;
} elseif (isset($response['error'])) {
// Errors are handled outside of the "then" function for
// synchronous errors.
// Errors are thrown and handled outside of the "then" function
// for synchronous errors.
throw $response['error'];
}
throw new \RuntimeException('No response was associated with the '
. 'transaction! This means the ring adapter did something '
. 'wrong and never completed the transaction.');
// No response, future, or error, so throw an exception.
throw $this->getNoRingResponseException($trans->request);
} catch (RequestException $e) {
throw $e;
@ -262,22 +263,27 @@ class Client implements ClientInterface
) {
// Create a future response that's hooked up to the ring future.
return new FutureResponse(
// Dereference function
function () use ($response, $trans) {
// Only deref if an event listener did not intercept.
if (!$trans->response) {
$result = $response->deref();
// The response may have been set when dereferencing.
if (!$trans->response) {
if (isset($result['error'])) {
throw $response['error'];
} else {
throw new \RuntimeException('Did not return a '
. 'response or error.');
}
}
// Event listeners may have intercepted the transaction during
// the "then" event of the ring request transfer.
if ($trans->response) {
return $trans->response;
}
return $trans->response;
// Dereference the underlying future and block until complete.
$result = $response->deref();
// The transaction response may have been set on the trans
// while dereferencing the response (e.g., MockAdapter).
if ($trans->response) {
return $trans->response;
}
// Throw the appropriate exception, whether one was explicitly
// set, or we need to inform the user of a misbehaving adapter.
throw isset($result['error'])
? $response['error']
: $this->getNoRingResponseException($trans->request);
},
// Cancel function. Just proxy to the underlying future.
function () use ($response) {
return $response->cancel();
}
@ -384,9 +390,7 @@ class Client implements ClientInterface
private function mergeDefaults(&$options)
{
// Merging optimization for when no headers are present
if (!isset($options['headers'])
|| !isset($this->defaults['headers'])
) {
if (!isset($options['headers']) || !isset($this->defaults['headers'])) {
$options = array_replace_recursive($this->defaults, $options);
return null;
}
@ -406,4 +410,14 @@ class Client implements ClientInterface
{
Pool::send($this, $requests, $options);
}
private function getNoRingResponseException(RequestInterface $request)
{
return new RequestException(
'Sending the request did not return a response, exception, or '
. 'populate the transaction with a response. This is most likely '
. 'due to an incorrectly implemented Guzzle Ring adapter.',
$request
);
}
}

View File

@ -1,7 +1,6 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Ring\FutureInterface;
use GuzzleHttp\Transaction;
use GuzzleHttp\Exception\RequestException;
@ -158,102 +157,4 @@ final class RequestEvents
return $options;
}
/**
* Convert a Guzzle Transaction object into a Guzzle Ring request array.
*
* @param Transaction $trans Transaction to convert.
* @param MessageFactoryInterface $messageFactory Factory used to create
* response objects.
*
* @return array Request hash to send via a ring handler.
*/
public static function createRingRequest(
Transaction $trans,
MessageFactoryInterface $messageFactory
) {
$request = $trans->request;
$options = $request->getConfig()->toArray();
$url = $request->getUrl();
$r = [
'scheme' => $request->getScheme(),
'http_method' => $request->getMethod(),
'url' => $url,
'uri' => $request->getPath(),
'headers' => $request->getHeaders(),
'body' => $request->getBody(),
'version' => $request->getProtocolVersion(),
'client' => $options,
'future' => isset($options['future']) ? $options['future'] : null,
// No need to calculate the query string twice.
'query_string' => ($pos = strpos($url, '?')) ? substr($url, $pos + 1) : null,
'then' => function (array $response) use ($trans, $messageFactory) {
self::completeRingResponse($trans, $response, $messageFactory);
},
];
// Emit progress events if any progress listeners are registered.
if ($request->getEmitter()->hasListeners('progress')) {
$emitter = $request->getEmitter();
$r['client']['progress'] = function ($a, $b, $c, $d) use ($trans, $emitter) {
$emitter->emit(
'progress',
new ProgressEvent($trans, $a, $b, $c, $d)
);
};
}
return $r;
}
/**
* Handles the process of processing a response received from a handler.
*
* @param Transaction $trans Owns request and response.
* @param array $res Response array
* @param MessageFactoryInterface $messageFactory Creates response objects.
*/
public static function completeRingResponse(
Transaction $trans,
array $res,
MessageFactoryInterface $messageFactory
) {
if (!empty($res['status'])) {
$options = [];
if (isset($res['version'])) {
$options['protocol_version'] = $res['version'];
}
if (isset($res['reason'])) {
$options['reason_phrase'] = $res['reason'];
}
$trans->response = $messageFactory->createResponse(
$res['status'],
$res['headers'],
isset($res['body']) ? $res['body'] : null,
$options
);
if (isset($res['effective_url'])) {
$trans->response->setEffectiveUrl($res['effective_url']);
}
}
if (!isset($res['error'])) {
RequestEvents::emitComplete($trans);
} else {
RequestEvents::emitError(
$trans,
new RequestException(
$res['error']->getMessage(),
$trans->request,
$trans->response,
$res['error']
),
isset($res['transfer_info']) ? $res['transfer_info'] : []
);
}
}
}

152
src/RingBridge.php Normal file
View File

@ -0,0 +1,152 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Event\ProgressEvent;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Event\RequestEvents;
/**
* Provides the bridge between Guzzle requests and responses and Guzzle Ring.
*/
class RingBridge
{
/**
* Creates a Ring request from a request object.
*
* This function does not hook up the "then" and "progress" events that
* would be required for actually sending a Guzzle request through a
* ring adapter.
*
* @param RequestInterface $request Request to convert.
*
* @return array Converted Guzzle Ring request.
*/
public static function createRingRequest(RequestInterface $request)
{
$options = $request->getConfig()->toArray();
$url = $request->getUrl();
// No need to calculate the query string twice (in URL and query).
$qs = ($pos = strpos($url, '?')) ? substr($url, $pos + 1) : null;
return [
'scheme' => $request->getScheme(),
'http_method' => $request->getMethod(),
'url' => $url,
'uri' => $request->getPath(),
'headers' => $request->getHeaders(),
'body' => $request->getBody(),
'version' => $request->getProtocolVersion(),
'client' => $options,
'query_string' => $qs,
'future' => isset($options['future']) ? $options['future'] : null,
];
}
/**
* Give a ring request array, this function adds the "then" and "progress"
* event callbacks using a transaction and message factory.
*
* @param array $ringRequest Request to update.
* @param Transaction $trans Transaction
* @param MessageFactoryInterface $factory Creates responses.
*
* @return array Returns the new ring response array.
*/
public static function addRingRequestCallbacks(
array $ringRequest,
Transaction $trans,
MessageFactoryInterface $factory
) {
$request = $trans->request;
$ringRequest['then'] = function (array $response) use ($trans, $factory) {
self::completeRingResponse($trans, $response, $factory);
};
// Emit progress events if any progress listeners are registered.
if ($request->getEmitter()->hasListeners('progress')) {
$emitter = $request->getEmitter();
$ringRequest['client']['progress'] = function ($a, $b, $c, $d)
use ($trans, $emitter)
{
$emitter->emit(
'progress',
new ProgressEvent($trans, $a, $b, $c, $d)
);
};
}
return $ringRequest;
}
/**
* Creates a Ring request from a request object AND prepares the callbacks.
*
* @param Transaction $transaction Transaction to update.
* @param MessageFactoryInterface $factory Creates responses.
*
* @return array Converted Guzzle Ring request.
*/
public static function prepareRingRequest(
Transaction $transaction,
MessageFactoryInterface $factory
) {
return self::addRingRequestCallbacks(
self::createRingRequest($transaction->request),
$transaction,
$factory
);
}
/**
* Handles the process of processing a response received from a ring
* handler. The created response is added to the transaction, and any
* necessary events are emitted based on the ring response.
*
* @param Transaction $trans Owns request and response.
* @param array $response Ring response array
* @param MessageFactoryInterface $messageFactory Creates response objects.
*/
public static function completeRingResponse(
Transaction $trans,
array $response,
MessageFactoryInterface $messageFactory
) {
if (!empty($response['status'])) {
$options = [];
if (isset($response['version'])) {
$options['protocol_version'] = $response['version'];
}
if (isset($response['reason'])) {
$options['reason_phrase'] = $response['reason'];
}
$trans->response = $messageFactory->createResponse(
$response['status'],
$response['headers'],
isset($response['body']) ? $response['body'] : null,
$options
);
if (isset($response['effective_url'])) {
$trans->response->setEffectiveUrl($response['effective_url']);
}
}
if (!isset($response['error'])) {
RequestEvents::emitComplete($trans);
} else {
RequestEvents::emitError(
$trans,
new RequestException(
$response['error']->getMessage(),
$trans->request,
$trans->response,
$response['error']
),
isset($response['transfer_info'])
? $response['transfer_info'] : []
);
}
}
}

View File

@ -329,7 +329,7 @@ class ClientTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \GuzzleHttp\Exception\RequestException
* @expectedExceptionMessage No response
* @expectedExceptionMessage incorrectly implemented Guzzle Ring adapter
*/
public function testEnsuresResponseIsPresentAfterSending()
{

View File

@ -2,12 +2,7 @@
namespace GuzzleHttp\Tests\Event;
use GuzzleHttp\Client;
use GuzzleHttp\Event\ProgressEvent;
use GuzzleHttp\Message\FutureResponse;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Ring\Client\MockAdapter;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Tests\Server;
use GuzzleHttp\Transaction;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\ErrorEvent;
@ -225,86 +220,4 @@ class RequestEventsTest extends \PHPUnit_Framework_TestCase
$result = RequestEvents::convertEventArray($in, $events, $add);
$this->assertEquals($out, $result);
}
public function testCreatesRingRequests()
{
$stream = Stream::factory('test');
$request = new Request('GET', 'http://httpbin.org/get?a=b', [
'test' => 'hello'
], $stream);
$request->getConfig()->set('foo', 'bar');
$trans = new Transaction(new Client(), $request);
$factory = new MessageFactory();
$r = RequestEvents::createRingRequest($trans, $factory);
$this->assertEquals('http', $r['scheme']);
$this->assertEquals('GET', $r['http_method']);
$this->assertEquals('http://httpbin.org/get?a=b', $r['url']);
$this->assertEquals('/get', $r['uri']);
$this->assertEquals('a=b', $r['query_string']);
$this->assertEquals([
'Host' => ['httpbin.org'],
'test' => ['hello']
], $r['headers']);
$this->assertSame($stream, $r['body']);
$this->assertEquals(['foo' => 'bar'], $r['client']);
$this->assertTrue(is_callable($r['then']));
}
public function testCreatesRingRequestsWithNullQueryString()
{
$request = new Request('GET', 'http://httpbin.org');
$trans = new Transaction(new Client(), $request);
$factory = new MessageFactory();
$r = RequestEvents::createRingRequest($trans, $factory);
$this->assertNull($r['query_string']);
$this->assertEquals('/', $r['uri']);
$this->assertEquals(['Host' => ['httpbin.org']], $r['headers']);
$this->assertNull($r['body']);
$this->assertEquals([], $r['client']);
}
public function testCallsThenAndAddsProgress()
{
Server::enqueue([new Response(200)]);
$client = new Client(['base_url' => Server::$url]);
$request = $client->createRequest('GET');
$called = false;
$request->getEmitter()->on(
'progress',
function (ProgressEvent $e) use (&$called) {
$called = true;
}
);
$this->assertEquals(200, $client->send($request)->getStatusCode());
$this->assertTrue($called);
}
public function testEmitsErrorEventOnError()
{
$client = new Client(['base_url' => 'http://127.0.0.1:123']);
$request = $client->createRequest('GET');
$request->getConfig()['timeout'] = 0.001;
$request->getConfig()['connect_timeout'] = 0.001;
try {
$client->send($request);
$this->fail('did not throw');
} catch (RequestException $e) {
$this->assertSame($request, $e->getRequest());
$this->assertContains('cURL error 7:', $e->getMessage());
}
}
public function testGetsResponseProtocolVersion()
{
$client = new Client([
'adapter' => new MockAdapter([
'status' => 200,
'headers' => [],
'version' => '1.0'
])
]);
$request = $client->createRequest('GET', 'http://foo.com');
$response = $client->send($request);
$this->assertEquals('1.0', $response->getProtocolVersion());
}
}

98
tests/RingBridgeTest.php Normal file
View File

@ -0,0 +1,98 @@
<?php
namespace GuzzleHttp\Tests;
use GuzzleHttp\Client;
use GuzzleHttp\Event\ProgressEvent;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\RingBridge;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Transaction;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Ring\Client\MockAdapter;
use GuzzleHttp\Exception\RequestException;
class RingBridgeTest extends \PHPUnit_Framework_TestCase
{
public function testCreatesRingRequests()
{
$stream = Stream::factory('test');
$request = new Request('GET', 'http://httpbin.org/get?a=b', [
'test' => 'hello'
], $stream);
$request->getConfig()->set('foo', 'bar');
$trans = new Transaction(new Client(), $request);
$factory = new MessageFactory();
$r = RingBridge::prepareRingRequest($trans, $factory);
$this->assertEquals('http', $r['scheme']);
$this->assertEquals('GET', $r['http_method']);
$this->assertEquals('http://httpbin.org/get?a=b', $r['url']);
$this->assertEquals('/get', $r['uri']);
$this->assertEquals('a=b', $r['query_string']);
$this->assertEquals([
'Host' => ['httpbin.org'],
'test' => ['hello']
], $r['headers']);
$this->assertSame($stream, $r['body']);
$this->assertEquals(['foo' => 'bar'], $r['client']);
$this->assertTrue(is_callable($r['then']));
}
public function testCreatesRingRequestsWithNullQueryString()
{
$request = new Request('GET', 'http://httpbin.org');
$trans = new Transaction(new Client(), $request);
$factory = new MessageFactory();
$r = RingBridge::prepareRingRequest($trans, $factory);
$this->assertNull($r['query_string']);
$this->assertEquals('/', $r['uri']);
$this->assertEquals(['Host' => ['httpbin.org']], $r['headers']);
$this->assertNull($r['body']);
$this->assertEquals([], $r['client']);
}
public function testCallsThenAndAddsProgress()
{
Server::enqueue([new Response(200)]);
$client = new Client(['base_url' => Server::$url]);
$request = $client->createRequest('GET');
$called = false;
$request->getEmitter()->on(
'progress',
function (ProgressEvent $e) use (&$called) {
$called = true;
}
);
$this->assertEquals(200, $client->send($request)->getStatusCode());
$this->assertTrue($called);
}
public function testGetsResponseProtocolVersion()
{
$client = new Client([
'adapter' => new MockAdapter([
'status' => 200,
'headers' => [],
'version' => '1.0'
])
]);
$request = $client->createRequest('GET', 'http://foo.com');
$response = $client->send($request);
$this->assertEquals('1.0', $response->getProtocolVersion());
}
public function testEmitsErrorEventOnError()
{
$client = new Client(['base_url' => 'http://127.0.0.1:123']);
$request = $client->createRequest('GET');
$request->getConfig()['timeout'] = 0.001;
$request->getConfig()['connect_timeout'] = 0.001;
try {
$client->send($request);
$this->fail('did not throw');
} catch (RequestException $e) {
$this->assertSame($request, $e->getRequest());
$this->assertContains('cURL error', $e->getMessage());
}
}
}