Issue #2317845 by sun | Arla: Upgrade Guzzle to version 4.1.7.

8.0.x
Alex Pott 2014-08-11 09:02:51 -05:00
parent 453a4451b7
commit cdf77e49f8
52 changed files with 1050 additions and 400 deletions

27
composer.lock generated
View File

@ -459,21 +459,21 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "4.1.3",
"version": "4.1.7",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "012b2aecbda4e38f119c19580898685851015fa7"
"reference": "448f2c2076cf0fb756230611491c4f7ecb735a29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/012b2aecbda4e38f119c19580898685851015fa7",
"reference": "012b2aecbda4e38f119c19580898685851015fa7",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/448f2c2076cf0fb756230611491c4f7ecb735a29",
"reference": "448f2c2076cf0fb756230611491c4f7ecb735a29",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/streams": "~1.3",
"guzzlehttp/streams": "~1.4",
"php": ">=5.4.0"
},
"require-dev": {
@ -487,7 +487,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1.x-dev"
"dev-master": "4.1-dev"
}
},
"autoload": {
@ -520,20 +520,20 @@
"rest",
"web service"
],
"time": "2014-07-16 03:01:02"
"time": "2014-08-08 01:30:43"
},
{
"name": "guzzlehttp/streams",
"version": "1.3.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "d6aaa91cfdbae86355dd2a168a3ca536755898a2"
"reference": "fb0d1ee29987c2bdc59867bffaade6fc88c2675f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/d6aaa91cfdbae86355dd2a168a3ca536755898a2",
"reference": "d6aaa91cfdbae86355dd2a168a3ca536755898a2",
"url": "https://api.github.com/repos/guzzle/streams/zipball/fb0d1ee29987c2bdc59867bffaade6fc88c2675f",
"reference": "fb0d1ee29987c2bdc59867bffaade6fc88c2675f",
"shasum": ""
},
"require": {
@ -545,7 +545,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
"dev-master": "1.5-dev"
}
},
"autoload": {
@ -573,7 +573,7 @@
"Guzzle",
"stream"
],
"time": "2014-07-15 22:02:02"
"time": "2014-08-10 23:57:01"
},
{
"name": "kriswallsmith/assetic",
@ -2448,6 +2448,7 @@
"symfony-cmf/routing": 15,
"phpunit/phpunit-mock-objects": 20
},
"prefer-stable": false,
"platform": {
"php": ">=5.4.2"
},

View File

@ -143,6 +143,8 @@ class ClassLoader
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-0 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
@ -202,10 +204,13 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths) {
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {

View File

@ -23,9 +23,6 @@ class ComposerAutoloaderInitDrupal8
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitDrupal8', 'loadClassLoader'));
$vendorDir = dirname(__DIR__);
$baseDir = dirname(dirname($vendorDir));
$includePaths = require __DIR__ . '/include_paths.php';
array_push($includePaths, get_include_path());
set_include_path(join(PATH_SEPARATOR, $includePaths));

View File

@ -2300,128 +2300,6 @@
"xunit"
]
},
{
"name": "guzzlehttp/streams",
"version": "1.3.0",
"version_normalized": "1.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "d6aaa91cfdbae86355dd2a168a3ca536755898a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/d6aaa91cfdbae86355dd2a168a3ca536755898a2",
"reference": "d6aaa91cfdbae86355dd2a168a3ca536755898a2",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"time": "2014-07-15 22:02:02",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\Stream\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple abstraction over streams of data (Guzzle 4+)",
"homepage": "http://guzzlephp.org/",
"keywords": [
"Guzzle",
"stream"
]
},
{
"name": "guzzlehttp/guzzle",
"version": "4.1.3",
"version_normalized": "4.1.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "012b2aecbda4e38f119c19580898685851015fa7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/012b2aecbda4e38f119c19580898685851015fa7",
"reference": "012b2aecbda4e38f119c19580898685851015fa7",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/streams": "~1.3",
"php": ">=5.4.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"time": "2014-07-16 03:01:02",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
]
},
{
"name": "symfony/serializer",
"version": "v2.5.2",
@ -2512,5 +2390,127 @@
"BSD"
],
"homepage": "http://vfs.bovigo.org/"
},
{
"name": "guzzlehttp/streams",
"version": "1.5.1",
"version_normalized": "1.5.1.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "fb0d1ee29987c2bdc59867bffaade6fc88c2675f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/fb0d1ee29987c2bdc59867bffaade6fc88c2675f",
"reference": "fb0d1ee29987c2bdc59867bffaade6fc88c2675f",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"time": "2014-08-10 23:57:01",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.5-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\Stream\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple abstraction over streams of data (Guzzle 4+)",
"homepage": "http://guzzlephp.org/",
"keywords": [
"Guzzle",
"stream"
]
},
{
"name": "guzzlehttp/guzzle",
"version": "4.1.7",
"version_normalized": "4.1.7.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "448f2c2076cf0fb756230611491c4f7ecb735a29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/448f2c2076cf0fb756230611491c4f7ecb735a29",
"reference": "448f2c2076cf0fb756230611491c4f7ecb735a29",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/streams": "~1.4",
"php": ">=5.4.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"time": "2014-08-08 01:30:43",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
]
}
]

View File

@ -10,7 +10,8 @@ before_script:
- curl --version
- pear config-set php_ini ~/.phpenv/versions/`php -r 'echo phpversion();'`/etc/php.ini || echo 'Error modifying PEAR'
- pecl install uri_template || echo 'Error installing uri_template'
- composer install
- composer self-update
- composer install --no-interaction --prefer-source --dev
- ~/.nvm/nvm.sh install v0.6.14
- ~/.nvm/nvm.sh run v0.6.14

View File

@ -1,6 +1,49 @@
CHANGELOG
=========
4.1.7 (2014-08-07)
------------------
* Fixed an error in the HistoryPlugin that caused the same request and response
to be logged multiple times when an HTTP protocol error occurs.
* Ensuring that cURL does not add a default Content-Type when no Content-Type
has been supplied by the user. This prevents the adapter layer from modifying
the request that is sent over the wire after any listeners may have already
put the request in a desired state (e.g., signed the request).
* Throwing an exception when you attempt to send requests that have the
"stream" set to true in parallel using the MultiAdapter.
* Only calling curl_multi_select when there are active cURL handles. This was
previously changed and caused performance problems on some systems due to PHP
always selecting until the maximum select timeout.
* Fixed a bug where multipart/form-data POST fields were not correctly
aggregated (e.g., values with "&").
4.1.6 (2014-08-03)
------------------
* Added helper methods to make it easier to represent messages as strings,
including getting the start line and getting headers as a string.
4.1.5 (2014-08-02)
------------------
* Automatically retrying cURL "Connection died, retrying a fresh connect"
errors when possible.
* cURL implementation cleanup
* Allowing multiple event subscriber listeners to be registered per event by
passing an array of arrays of listener configuration.
4.1.4 (2014-07-22)
------------------
* Fixed a bug that caused multi-part POST requests with more than one field to
serialize incorrectly.
* Paths can now be set to "0"
* `ResponseInterface::xml` now accepts a `libxml_options` option and added a
missing default argument that was required when parsing XML response bodies.
* A `save_to` stream is now created lazily, which means that files are not
created on disk unless a request succeeds.
4.1.3 (2014-07-15)
------------------

View File

@ -17,7 +17,7 @@
"require": {
"php": ">=5.4.0",
"ext-json": "*",
"guzzlehttp/streams": "~1.3"
"guzzlehttp/streams": "~1.4"
},
"suggest": {
@ -43,7 +43,7 @@
"extra": {
"branch-alias": {
"dev-master": "4.1.x-dev"
"dev-master": "4.1-dev"
}
}
}

View File

@ -297,7 +297,7 @@ immeditaley and prevent subsequent requests from being sent.
use GuzzleHttp\Event\ErrorEvent;
$client->sendAll($requests, [
'error' => function (ErrorEvent $event) use (&$errors) {
'error' => function (ErrorEvent $event) {
throw $event->getException();
}
]);

View File

@ -184,7 +184,7 @@ priority of the listener (as shown in the ``before`` listener in the example).
.. code-block:: php
use GuzzleHttp\Event\EventEmitterInterface;
use GuzzleHttp\Event\EmitterInterface;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\CompleteEvent;
@ -194,8 +194,11 @@ priority of the listener (as shown in the ``before`` listener in the example).
public function getEvents()
{
return [
'before' => ['onBefore', 100], // Provide name and optional priority
'complete' => ['onComplete']
// Provide name and optional priority
'before' => ['onBefore', 100],
'complete' => ['onComplete'],
// You can pass a list of listeners with different priorities
'error' => [['beforeError', 'first'], ['afterError', 'last]]
];
}

View File

@ -50,7 +50,7 @@ then you'll need to use a ``GuzzleHttp\ClientInterface`` object.
use GuzzleHttp\Client;
$client = new Client();
$response = $client->get('https://github.com/timeline.json');
$response = $client->get('http://httpbin.org/get');
// You can use the same methods you saw in the procedural API
$response = $client->delete('http://httpbin.org/delete');
@ -120,9 +120,9 @@ response.
.. code-block:: php
$response = $client->get('https://github.com/timeline.json');
$response = $client->get('http://httpbin.org/get');
$json = $response->json();
var_dump($json[0]['repository']);
var_dump($json[0]['origin']);
Guzzle internally uses PHP's ``json_decode()`` function to parse responses. If
Guzzle is unable to parse the JSON response body, then a

View File

@ -59,6 +59,16 @@ class BatchContext
throw new AdapterException('No curl handle was found');
}
/**
* Returns true if there are any active requests.
*
* @return bool
*/
public function isActive()
{
return count($this->handles) > 0;
}
/**
* Returns true if there are any remaining pending transactions
*
@ -143,16 +153,15 @@ class BatchContext
}
$handle = $this->handles[$transaction];
$this->handles->detach($transaction);
$info = curl_getinfo($handle);
$code = curl_multi_remove_handle($this->multi, $handle);
if ($code != CURLM_OK) {
curl_close($handle);
if ($code !== CURLM_OK) {
MultiAdapter::throwMultiError($code);
}
$info = curl_getinfo($handle);
curl_close($handle);
unset($this->handles[$transaction]);
return $info;
}
}

View File

@ -91,6 +91,11 @@ class CurlFactory
$this->removeHeader('Accept-Encoding', $options);
}
// cURL sometimes adds a content-type by default. Prevent this.
if (!$request->hasHeader('Content-Type')) {
$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
}
return $options;
}
@ -273,6 +278,18 @@ class CurlFactory
$options[CURLOPT_SSLKEY] = $value;
}
private function add_stream()
{
throw new AdapterException('cURL adapters do not support the "stream"'
. ' request option. This error is typically encountered when trying'
. ' to send requests with the "stream" option set to true in '
. ' parallel. You will either need to send these one at a time or'
. ' implement a custom ParallelAdapterInterface that supports'
. ' sending these types of requests in parallel. This error can'
. ' also occur if the StreamAdapter is not available on your'
. ' system (e.g., allow_url_fopen is disabled in your php.ini).');
}
private function add_save_to(
RequestInterface $request,
RequestMediator $mediator,
@ -280,7 +297,7 @@ class CurlFactory
$value
) {
$mediator->setResponseBody(is_string($value)
? Stream\create(fopen($value, 'w'))
? new Stream\LazyOpenStream($value, 'w')
: Stream\create($value));
}

View File

@ -128,20 +128,25 @@ class MultiAdapter implements AdapterInterface, ParallelAdapterInterface
$multi = $context->getMultiHandle();
do {
while (($mrc = curl_multi_exec($multi, $active)) == CURLM_CALL_MULTI_PERFORM);
if ($mrc != CURLM_OK && $mrc != CURLM_CALL_MULTI_PERFORM) {
do {
$mrc = curl_multi_exec($multi, $active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
if ($mrc != CURLM_OK) {
self::throwMultiError($mrc);
}
// Need to check if there are pending transactions before processing
// them so that we don't bail from the loop too early.
$pending = $context->hasPending();
$this->processMessages($context);
if ($active && curl_multi_select($multi, $this->selectTimeout) === -1) {
if ($active &&
curl_multi_select($multi, $this->selectTimeout) === -1
) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
usleep(250);
}
} while ($active || $pending);
} while ($context->isActive() || $active);
$this->releaseMultiHandle($multi);
}
@ -168,7 +173,9 @@ class MultiAdapter implements AdapterInterface, ParallelAdapterInterface
$info = $context->removeTransaction($transaction);
try {
if (!$this->isCurlException($transaction, $curl, $context, $info)) {
if (!$this->isCurlException($transaction, $curl, $context, $info) &&
$this->validateResponseWasSet($transaction, $context)
) {
RequestEvents::emitComplete($transaction, $info);
}
} catch (RequestException $e) {
@ -281,4 +288,75 @@ class MultiAdapter implements AdapterInterface, ParallelAdapterInterface
unset($this->multiHandles[$id], $this->multiOwned[$id]);
}
}
/**
* This function ensures that a response was set on a transaction. If one
* was not set, then the request is retried if possible. This error
* typically means you are sending a payload, curl encountered a
* "Connection died, retrying a fresh connect" error, tried to rewind the
* stream, and then encountered a "necessary data rewind wasn't possible"
* error, causing the request to be sent through curl_multi_info_read()
* without an error status.
*
* @param TransactionInterface $transaction
* @param BatchContext $context
*
* @return bool Returns true if it's OK, and false if it failed.
* @throws \GuzzleHttp\Exception\RequestException If it failed and cannot
* recover.
*/
private function validateResponseWasSet(
TransactionInterface $transaction,
BatchContext $context
) {
if ($transaction->getResponse()) {
return true;
}
$body = $transaction->getRequest()->getBody();
if (!$body) {
// This is weird and should probably never happen.
RequestEvents::emitError(
$transaction,
new RequestException(
'No response was received for a request with no body. This'
. ' could mean that you are saturating your network.',
$transaction->getRequest()
)
);
} elseif (!$body->isSeekable() || !$body->seek(0)) {
// Nothing we can do with this. Sorry!
RequestEvents::emitError(
$transaction,
new RequestException(
'The connection was unexpectedly closed. The request would'
. ' have been retried, but attempting to rewind the'
. ' request body failed. Consider wrapping your request'
. ' body in a CachingStream decorator to work around this'
. ' issue if necessary.',
$transaction->getRequest()
)
);
} else {
$this->retryFailedConnection($transaction, $context);
}
return false;
}
private function retryFailedConnection(
TransactionInterface $transaction,
BatchContext $context
) {
// Add the request back to the batch to retry automatically.
$context->addTransaction(
$transaction,
call_user_func(
$this->curlFactory,
$transaction,
$this->messageFactory
)
);
}
}

View File

@ -72,7 +72,7 @@ class StreamAdapter implements AdapterInterface
if ($saveTo = $request->getConfig()['save_to']) {
// Stream the response into the destination stream
$saveTo = is_string($saveTo)
? Stream\create(fopen($saveTo, 'r+'))
? new Stream\LazyOpenStream($saveTo, 'r+')
: Stream\create($saveTo);
} else {
// Stream into the default temp stream
@ -151,7 +151,7 @@ class StreamAdapter implements AdapterInterface
if (isset($options['http']['proxy'])) {
$message .= "[proxy] {$options['http']['proxy']} ";
}
foreach (error_get_last() as $key => $value) {
foreach ((array) error_get_last() as $key => $value) {
$message .= "[{$key}] {$value} ";
}
throw new RequestException(trim($message), $request);
@ -316,13 +316,13 @@ class StreamAdapter implements AdapterInterface
array $options,
array $params
) {
return $this->createResource(function () use (
return $this->createResource(
function () use ($request, $options, $params) {
return stream_context_create($options, $params);
},
$request,
$options,
$params
) {
return stream_context_create($options, $params);
}, $request, $options);
$options
);
}
private function createStreamResource(
@ -337,16 +337,16 @@ class StreamAdapter implements AdapterInterface
$url = 'compress.zlib://' . $url;
}
return $this->createResource(function () use (
$url,
&$http_response_header,
$context
) {
if (false === strpos($url, 'http')) {
trigger_error("URL is invalid: {$url}", E_USER_WARNING);
return null;
}
return fopen($url, 'r', null, $context);
}, $request, $options);
return $this->createResource(
function () use ($url, &$http_response_header, $context) {
if (false === strpos($url, 'http')) {
trigger_error("URL is invalid: {$url}", E_USER_WARNING);
return null;
}
return fopen($url, 'r', null, $context);
},
$request,
$options
);
}
}

View File

@ -13,7 +13,7 @@ use GuzzleHttp\Exception\AdapterException;
*/
interface ClientInterface extends HasEmitterInterface
{
const VERSION = '4.1.3';
const VERSION = '4.1.7';
/**
* Create and return a new {@see RequestInterface} object.

View File

@ -114,12 +114,22 @@ class Emitter implements EmitterInterface
public function attach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listener) {
$this->on(
$eventName,
array($subscriber, $listener[0]),
isset($listener[1]) ? $listener[1] : 0
);
foreach ($subscriber->getEvents() as $eventName => $listeners) {
if (is_array($listeners[0])) {
foreach ($listeners as $listener) {
$this->on(
$eventName,
[$subscriber, $listener[0]],
isset($listener[1]) ? $listener[1] : 0
);
}
} else {
$this->on(
$eventName,
[$subscriber, $listeners[0]],
isset($listeners[1]) ? $listeners[1] : 0
);
}
}
}

View File

@ -17,13 +17,17 @@ interface SubscriberInterface
*
* The returned array keys MUST map to an event name. Each array value
* MUST be an array in which the first element is the name of a function
* on the EventSubscriber. The second element in the array is optional, and
* if specified, designates the event priority.
* on the EventSubscriber OR an array of arrays in the aforementioned
* format. The second element in the array is optional, and if specified,
* designates the event priority.
*
* For example:
* For example, the following are all valid:
*
* - ['eventName' => ['methodName']]
* - ['eventName' => ['methodName', $priority]]
* - ['eventName' => [['methodName'], ['otherMethod']]
* - ['eventName' => [['methodName'], ['otherMethod', $priority]]
* - ['eventName' => [['methodName', $priority], ['otherMethod', $priority]]
*
* @return array
*/

View File

@ -1,5 +1,4 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Stream\StreamInterface;
@ -20,12 +19,8 @@ abstract class AbstractMessage implements MessageInterface
public function __toString()
{
$result = $this->getStartLine();
foreach ($this->getHeaders() as $name => $values) {
$result .= "\r\n{$name}: " . implode(', ', $values);
}
return $result . "\r\n\r\n" . $this->body;
return static::getStartLineAndHeaders($this)
. "\r\n\r\n" . $this->getBody();
}
public function getProtocolVersion()
@ -214,11 +209,57 @@ abstract class AbstractMessage implements MessageInterface
}
/**
* Returns the start line of a message.
* Gets the start-line and headers of a message as a string
*
* @param MessageInterface $message
*
* @return string
*/
abstract protected function getStartLine();
public static function getStartLineAndHeaders(MessageInterface $message)
{
return static::getStartLine($message)
. self::getHeadersAsString($message);
}
/**
* Gets the headers of a message as a string
*
* @param MessageInterface $message
*
* @return string
*/
public static function getHeadersAsString(MessageInterface $message)
{
$result = '';
foreach ($message->getHeaders() as $name => $values) {
$result .= "\r\n{$name}: " . implode(', ', $values);
}
return $result;
}
/**
* Gets the start line of a message
*
* @param MessageInterface $message
*
* @return string
* @throws \InvalidArgumentException
*/
public static function getStartLine(MessageInterface $message)
{
if ($message instanceof RequestInterface) {
return trim($message->getMethod() . ' '
. $message->getResource())
. ' HTTP/' . $message->getProtocolVersion();
} elseif ($message instanceof ResponseInterface) {
return 'HTTP/' . $message->getProtocolVersion() . ' '
. $message->getStatusCode() . ' '
. $message->getReasonPhrase();
} else {
throw new \InvalidArgumentException('Unknown message type');
}
}
/**
* Accepts and modifies the options provided to the message in the

View File

@ -1,5 +1,4 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Event\HasEmitterTrait;
@ -179,12 +178,6 @@ class Request extends AbstractMessage implements RequestInterface
}
}
protected function getStartLine()
{
return trim($this->method . ' ' . $this->getResource())
. ' HTTP/' . $this->getProtocolVersion();
}
/**
* Adds a subscriber that ensures a request's body is prepared before
* sending.

View File

@ -150,7 +150,8 @@ class Response extends AbstractMessage implements ResponseInterface
// Allow XML to be retrieved even if there is no response body
$xml = new \SimpleXMLElement(
(string) $this->getBody() ?: '<root />',
LIBXML_NONET,
isset($config['libxml_options']) ? $config['libxml_options'] : LIBXML_NONET,
false,
isset($config['ns']) ? $config['ns'] : '',
isset($config['ns_is_prefix']) ? $config['ns_is_prefix'] : false
);
@ -193,10 +194,4 @@ class Response extends AbstractMessage implements ResponseInterface
$this->reasonPhrase = $options['reason_phrase'];
}
}
protected function getStartLine()
{
return 'HTTP/' . $this->getProtocolVersion()
. " {$this->statusCode} {$this->reasonPhrase}";
}
}

View File

@ -77,6 +77,9 @@ interface ResponseInterface extends MessageInterface
* - ns: Set to a string to represent the namespace prefix or URI
* - ns_is_prefix: Set to true to specify that the NS is a prefix rather
* than a URI (defaults to false).
* - libxml_options: Bitwise OR of the libxml option constants
* listed at http://php.net/manual/en/libxml.constants.php
* (defaults to LIBXML_NONET)
*
* @return \SimpleXMLElement
* @throws \RuntimeException if the response body is not in XML format

View File

@ -9,13 +9,13 @@ use GuzzleHttp\Stream;
*/
class MultipartBody implements Stream\StreamInterface
{
/** @var Stream\StreamInterface */
private $stream;
use Stream\StreamDecoratorTrait;
private $boundary;
/**
* @param array $fields Associative array of field names to values where
* each value is a string.
* each value is a string or array of strings.
* @param array $files Associative array of PostFileInterface objects
* @param string $boundary You can optionally provide a specific boundary
* @throws \InvalidArgumentException
@ -26,17 +26,7 @@ class MultipartBody implements Stream\StreamInterface
$boundary = null
) {
$this->boundary = $boundary ?: uniqid();
$this->createStream($fields, $files);
}
public function __toString()
{
return (string) $this->stream;
}
public function getContents($maxLength = -1)
{
return $this->stream->getContents($maxLength);
$this->stream = $this->createStream($fields, $files);
}
/**
@ -49,63 +39,11 @@ class MultipartBody implements Stream\StreamInterface
return $this->boundary;
}
public function close()
{
$this->stream->close();
$this->detach();
}
public function detach()
{
$this->stream->detach();
$this->size = 0;
}
public function eof()
{
return $this->stream->eof();
}
public function tell()
{
return $this->stream->tell();
}
public function isReadable()
{
return true;
}
public function isWritable()
{
return false;
}
public function isSeekable()
{
return $this->stream->isSeekable();
}
public function getSize()
{
return $this->stream->getSize();
}
public function read($length)
{
return $this->stream->read($length);
}
public function seek($offset, $whence = SEEK_SET)
{
return $this->stream->seek($offset, $whence);
}
public function write($string)
{
return false;
}
/**
* Get the string needed to transfer a POST field
*/
@ -137,12 +75,14 @@ class MultipartBody implements Stream\StreamInterface
*/
private function createStream(array $fields, array $files)
{
$this->stream = new Stream\AppendStream();
$stream = new Stream\AppendStream();
foreach ($fields as $name => $field) {
$this->stream->addStream(
Stream\create($this->getFieldString($name, $field))
);
foreach ($fields as $name => $fieldValues) {
foreach ((array) $fieldValues as $value) {
$stream->addStream(
Stream\create($this->getFieldString($name, $value))
);
}
}
foreach ($files as $file) {
@ -152,14 +92,16 @@ class MultipartBody implements Stream\StreamInterface
. 'implement PostFieldInterface');
}
$this->stream->addStream(
$stream->addStream(
Stream\create($this->getFileHeaders($file))
);
$this->stream->addStream($file->getContent());
$this->stream->addStream(Stream\create("\r\n"));
$stream->addStream($file->getContent());
$stream->addStream(Stream\create("\r\n"));
}
// Add the trailing boundary
$this->stream->addStream(Stream\create("--{$this->boundary}--"));
$stream->addStream(Stream\create("--{$this->boundary}--"));
return $stream;
}
}

View File

@ -1,5 +1,4 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Message\RequestInterface;
@ -253,22 +252,10 @@ class PostBody implements PostBodyInterface
private function createMultipart()
{
// Flatten the nested query string values using the correct aggregator
if (!$this->fields) {
$fields = [];
} else {
$query = (string) (new Query($this->fields))
->setEncodingType(false)
->setAggregator($this->getAggregator());
// Convert the flattened query string back into an array
$fields = [];
foreach (explode('&', $query, 2) as $kvp) {
$parts = explode('=', $kvp, 2);
$fields[$parts[0]] = isset($parts[1]) ? $parts[1] : null;
}
}
return new MultipartBody($fields, $this->files);
return new MultipartBody(
call_user_func($this->getAggregator(), $this->fields),
$this->files
);
}
/**

View File

@ -56,7 +56,11 @@ class History implements SubscriberInterface, \IteratorAggregate, \Countable
public function onError(ErrorEvent $event)
{
$this->add($event->getRequest(), $event->getResponse());
// Only track when no response is present, meaning this didn't ever
// emit a complete event
if (!$event->getResponse()) {
$this->add($event->getRequest());
}
}
/**

View File

@ -555,7 +555,7 @@ class Url
);
}
if (!$parts['path']) {
if (!$parts['path'] && $parts['path'] !== '0') {
// The relative URL has no path, so check if it is just a query
$path = $this->path ?: '';
$query = count($parts['query']) ? $parts['query'] : $this->query;

View File

@ -51,8 +51,10 @@ class BatchContextTest extends \PHPUnit_Framework_TestCase
new Request('GET', 'http://httbin.org')
);
$b->addTransaction($t, $h);
$this->assertTrue($b->isActive());
$this->assertSame($t, $b->findTransaction($h));
$b->removeTransaction($t);
$this->assertFalse($b->isActive());
try {
$this->assertEquals([], $b->findTransaction($h));
$this->fail('Did not throw');

View File

@ -9,6 +9,7 @@ use GuzzleHttp\Adapter\Transaction;
use GuzzleHttp\Client;
use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Event\HeadersEvent;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Event\BeforeEvent;
@ -117,4 +118,22 @@ class CurlAdapterTest extends AbstractCurl
$a->send($transaction);
$this->assertCount(2, $this->readAttribute($a, 'handles'));
}
public function testDoesNotSaveToWhenFailed()
{
Server::flush();
Server::enqueue([
"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n"
]);
$tmp = tempnam('/tmp', 'test_save_to');
unlink($tmp);
$a = new CurlAdapter(new MessageFactory());
$client = new Client(['base_url' => Server::$url, 'adapter' => $a]);
try {
$client->get('/', ['save_to' => $tmp]);
} catch (ServerException $e) {
$this->assertFileNotExists($tmp);
}
}
}

View File

@ -14,6 +14,7 @@ namespace GuzzleHttp\Tests\Adapter\Curl {
use GuzzleHttp\Adapter\Curl\MultiAdapter;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Adapter\Curl\CurlFactory;
@ -308,5 +309,27 @@ namespace GuzzleHttp\Tests\Adapter\Curl {
$event = new BeforeEvent(new Transaction(new Client(), $request));
$request->getEmitter()->emit('before', $event);
}
public function testDoesNotAlwaysAddContentType()
{
Server::flush();
Server::enqueue(["HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"]);
$client = new Client();
$client->put(Server::$url . '/foo', ['body' => 'foo']);
$request = Server::received(true)[0];
$this->assertEquals('', $request->getHeader('Content-Type'));
}
/**
* @expectedException \GuzzleHttp\Exception\AdapterException
*/
public function testThrowsForStreamOption()
{
$request = new Request('GET', Server::$url . 'haha');
$request->getConfig()->set('stream', true);
$t = new Transaction(new Client(), $request);
$f = new CurlFactory();
$f($t, new MessageFactory());
}
}
}

View File

@ -12,6 +12,9 @@ use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\NoSeekStream;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Tests\Server;
/**
@ -193,4 +196,127 @@ class MultiAdapterTest extends AbstractCurl
$this->assertSame($request, $e->getRequest());
}
}
public function testEnsuresResponseWasSetForGet()
{
$client = new Client();
$request = $client->createRequest('GET', Server::$url);
$response = new Response(200, []);
$er = null;
$request->getEmitter()->on(
'error',
function (ErrorEvent $e) use (&$er, $response) {
$er = $e;
}
);
$transaction = $this->getMockBuilder('GuzzleHttp\Adapter\Transaction')
->setMethods(['getResponse', 'setResponse'])
->setConstructorArgs([$client, $request])
->getMock();
$transaction->expects($this->any())->method('setResponse');
$transaction->expects($this->any())
->method('getResponse')
->will($this->returnCallback(function () use ($response) {
$caller = debug_backtrace()[6]['function'];
return $caller == 'addHandle' ||
$caller == 'validateResponseWasSet'
? null
: $response;
}));
$a = new MultiAdapter(new MessageFactory());
Server::flush();
Server::enqueue(["HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"]);
$a->sendAll(new \ArrayIterator([$transaction]), 10);
$this->assertNotNull($er);
$this->assertContains(
'No response was received',
$er->getException()->getMessage()
);
}
private function runConnectionTest(
$queue,
$stream,
$msg,
$statusCode = null
) {
$obj = new \stdClass();
$er = null;
$client = new Client();
$request = $client->createRequest('PUT', Server::$url, [
'body' => $stream
]);
$request->getEmitter()->on(
'error',
function (ErrorEvent $e) use (&$er) {
$er = $e;
}
);
$transaction = $this->getMockBuilder('GuzzleHttp\Adapter\Transaction')
->setMethods(['getResponse', 'setResponse'])
->setConstructorArgs([$client, $request])
->getMock();
$transaction->expects($this->any())
->method('setResponse')
->will($this->returnCallback(function ($r) use (&$obj) {
$obj->res = $r;
}));
$transaction->expects($this->any())
->method('getResponse')
->will($this->returnCallback(function () use ($obj, &$called) {
$caller = debug_backtrace()[6]['function'];
if ($caller == 'addHandle') {
return null;
} elseif ($caller == 'validateResponseWasSet') {
return ++$called == 2 ? $obj->res : null;
} else {
return $obj->res;
}
}));
$a = new MultiAdapter(new MessageFactory());
Server::flush();
Server::enqueue($queue);
$a->sendAll(new \ArrayIterator([$transaction]), 10);
if ($msg) {
$this->assertNotNull($er);
$this->assertContains($msg, $er->getException()->getMessage());
} else {
$this->assertEquals(
$statusCode,
$transaction->getResponse()->getStatusCode()
);
}
}
public function testThrowsWhenTheBodyCannotBeRewound()
{
$this->runConnectionTest(
["HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"],
new NoSeekStream(Stream::factory('foo')),
'attempting to rewind the request body failed'
);
}
public function testRetriesRewindableStreamsWhenClosedConnectionErrors()
{
$this->runConnectionTest(
[
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
"HTTP/1.1 201 OK\r\nContent-Length: 0\r\n\r\n",
],
Stream::factory('foo'),
false,
201
);
}
}

View File

@ -168,6 +168,15 @@ class EmitterTest extends \PHPUnit_Framework_TestCase
$this->assertNotEmpty($this->emitter->listeners(self::postFoo));
}
public function testAddSubscriberWithMultiple()
{
$eventSubscriber = new TestEventSubscriberWithMultiple();
$this->emitter->attach($eventSubscriber);
$listeners = $this->emitter->listeners('pre.foo');
$this->assertNotEmpty($this->emitter->listeners(self::preFoo));
$this->assertCount(2, $listeners);
}
public function testAddSubscriberWithPriorities()
{
$eventSubscriber = new TestEventSubscriber();
@ -343,3 +352,11 @@ class TestEventSubscriberWithPriorities extends TestEventListener implements Sub
];
}
}
class TestEventSubscriberWithMultiple extends TestEventListener implements SubscriberInterface
{
public function getEvents()
{
return ['pre.foo' => [['preFoo', 10],['preFoo', 20]]];
}
}

View File

@ -76,7 +76,7 @@ class RequestExceptionTest extends \PHPUnit_Framework_TestCase
$e->emittedError(true);
$e->emittedError(false);
}
public function testHasStatusCodeAsExceptionCode() {
$e = RequestException::create(new Request('GET', '/'), new Response(442));
$this->assertEquals(442, $e->getCode());

View File

@ -1,9 +1,9 @@
<?php
namespace GuzzleHttp\Tests\Message;
use GuzzleHttp\Message\AbstractMessage;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
/**
@ -13,13 +13,13 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
{
public function testHasProtocolVersion()
{
$m = new Message();
$m = new Request('GET', '/');
$this->assertEquals(1.1, $m->getProtocolVersion());
}
public function testHasHeaders()
{
$m = new Message();
$m = new Request('GET', 'http://foo.com');
$this->assertFalse($m->hasHeader('foo'));
$m->addHeader('foo', 'bar');
$this->assertTrue($m->hasHeader('foo'));
@ -35,7 +35,7 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testHasBody()
{
$m = new Message();
$m = new Request('GET', 'http://foo.com');
$this->assertNull($m->getBody());
$s = Stream::factory('test');
$m->setBody($s);
@ -45,7 +45,7 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testCanRemoveBodyBySettingToNullAndRemovesCommonBodyHeaders()
{
$m = new Message();
$m = new Request('GET', 'http://foo.com');
$m->setBody(Stream::factory('foo'));
$m->setHeader('Content-Length', 3)->setHeader('Transfer-Encoding', 'chunked');
$m->setBody(null);
@ -56,10 +56,10 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testCastsToString()
{
$m = new Message();
$m = new Request('GET', 'http://foo.com');
$m->setHeader('foo', 'bar');
$m->setBody(Stream::factory('baz'));
$this->assertEquals("Foo!\r\nfoo: bar\r\n\r\nbaz", (string) $m);
$this->assertEquals("GET / HTTP/1.1\r\nHost: foo.com\r\nfoo: bar\r\n\r\nbaz", (string) $m);
}
public function parseParamsProvider()
@ -115,12 +115,12 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testParseParams($header, $result)
{
$request = new Request('GET', '/', ['foo' => $header]);
$this->assertEquals($result, Message::parseHeader($request, 'foo'));
$this->assertEquals($result, Request::parseHeader($request, 'foo'));
}
public function testAddsHeadersWhenNotPresent()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->addHeader('foo', 'bar');
$this->assertInternalType('string', $h->getHeader('foo'));
$this->assertEquals('bar', $h->getHeader('foo'));
@ -128,7 +128,7 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testAddsHeadersWhenPresentSameCase()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->addHeader('foo', 'bar')->addHeader('foo', 'baz');
$this->assertEquals('bar, baz', $h->getHeader('foo'));
$this->assertEquals(['bar', 'baz'], $h->getHeader('foo', true));
@ -136,27 +136,28 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testAddsMultipleHeaders()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->addHeaders([
'foo' => ' bar',
'baz' => [' bam ', 'boo']
]);
$this->assertEquals([
'foo' => ['bar'],
'baz' => ['bam', 'boo']
'baz' => ['bam', 'boo'],
'Host' => ['foo.com']
], $h->getHeaders());
}
public function testAddsHeadersWhenPresentDifferentCase()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->addHeader('Foo', 'bar')->addHeader('fOO', 'baz');
$this->assertEquals('bar, baz', $h->getHeader('foo'));
}
public function testAddsHeadersWithArray()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->addHeader('Foo', ['bar', 'baz']);
$this->assertEquals('bar, baz', $h->getHeader('foo'));
}
@ -166,12 +167,12 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
*/
public function testThrowsExceptionWhenInvalidValueProvidedToAddHeader()
{
(new Message())->addHeader('foo', false);
(new Request('GET', 'http://foo.com'))->addHeader('foo', false);
}
public function testGetHeadersReturnsAnArrayOfOverTheWireHeaderValues()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->addHeader('foo', 'bar');
$h->addHeader('Foo', 'baz');
$h->addHeader('boO', 'test');
@ -186,7 +187,7 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testSetHeaderOverwritesExistingValues()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', 'bar');
$this->assertEquals('bar', $h->getHeader('foo'));
$h->setHeader('Foo', 'baz');
@ -196,14 +197,14 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testSetHeaderOverwritesExistingValuesUsingHeaderArray()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', ['bar']);
$this->assertEquals('bar', $h->getHeader('foo'));
}
public function testSetHeaderOverwritesExistingValuesUsingArray()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', ['bar']);
$this->assertEquals('bar', $h->getHeader('foo'));
}
@ -213,12 +214,12 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
*/
public function testThrowsExceptionWhenInvalidValueProvidedToSetHeader()
{
(new Message())->setHeader('foo', false);
(new Request('GET', 'http://foo.com'))->setHeader('foo', false);
}
public function testSetHeadersOverwritesAllHeaders()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', 'bar');
$h->setHeaders(['foo' => 'a', 'boo' => 'b']);
$this->assertEquals(['foo' => ['a'], 'boo' => ['b']], $h->getHeaders());
@ -226,7 +227,7 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testChecksIfCaseInsensitiveHeaderIsPresent()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', 'bar');
$this->assertTrue($h->hasHeader('foo'));
$this->assertTrue($h->hasHeader('Foo'));
@ -236,7 +237,7 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testRemovesHeaders()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', 'bar');
$h->removeHeader('foo');
$this->assertFalse($h->hasHeader('foo'));
@ -247,14 +248,14 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
public function testReturnsCorrectTypeWhenMissing()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$this->assertInternalType('string', $h->getHeader('foo'));
$this->assertInternalType('array', $h->getHeader('foo', true));
}
public function testSetsIntegersAndFloatsAsHeaders()
{
$h = new Message();
$h = new Request('GET', 'http://foo.com');
$h->setHeader('foo', 10);
$h->setHeader('bar', 10.5);
$h->addHeader('foo', 10);
@ -262,12 +263,20 @@ class AbstractMessageTest extends \PHPUnit_Framework_TestCase
$this->assertSame('10, 10', $h->getHeader('foo'));
$this->assertSame('10.5, 10.5', $h->getHeader('bar'));
}
}
class Message extends AbstractMessage
{
protected function getStartLine()
public function testGetsResponseStartLine()
{
return 'Foo!';
$m = new Response(200);
$this->assertEquals('HTTP/1.1 200 OK', Response::getStartLine($m));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testThrowsWhenMessageIsUnknown()
{
$m = $this->getMockBuilder('GuzzleHttp\Message\AbstractMessage')
->getMockForAbstractClass();
AbstractMessage::getStartLine($m);
}
}

View File

@ -36,7 +36,10 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$b->forceMultipartUpload(true);
$m = new Request('POST', '/');
$b->applyRequestHeaders($m);
$this->assertContains('multipart/form-data', (string) $m->getHeader('Content-Type'));
$this->assertContains(
'multipart/form-data',
$m->getHeader('Content-Type')
);
}
public function testApplyingWithFilesAddsMultipartUpload()
@ -49,7 +52,10 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$this->assertSame($p, $b->getFile('foo'));
$m = new Request('POST', '/');
$b->applyRequestHeaders($m);
$this->assertContains('multipart/form-data', (string) $m->getHeader('Content-Type'));
$this->assertContains(
'multipart/form-data',
$m->getHeader('Content-Type')
);
$this->assertTrue($m->hasHeader('Content-Length'));
}
@ -60,7 +66,10 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(['foo' => 'bar'], $b->getFields());
$m = new Request('POST', '/');
$b->applyRequestHeaders($m);
$this->assertContains('application/x-www-form', (string) $m->getHeader('Content-Type'));
$this->assertContains(
'application/x-www-form',
$m->getHeader('Content-Type')
);
$this->assertTrue($m->hasHeader('Content-Length'));
}
@ -72,7 +81,10 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(['foo' => ['bar' => 'baz']], $b->getFields());
$m = new Request('POST', '/');
$b->applyRequestHeaders($m);
$this->assertContains('multipart/form-data', (string) $m->getHeader('Content-Type'));
$this->assertContains(
'multipart/form-data',
$m->getHeader('Content-Type')
);
$this->assertTrue($m->hasHeader('Content-Length'));
$contents = $b->getContents();
$this->assertContains('name="foo[bar]"', $contents);
@ -147,10 +159,20 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
{
$b = new PostBody();
$b->setField('testing', ['baz', 'bar']);
$b->setField('other', 'hi');
$b->setField('third', 'there');
$b->addFile(new PostFile('foo', fopen(__FILE__, 'r')));
$s = (string) $b;
$this->assertContains(file_get_contents(__FILE__), $s);
$this->assertContains('testing=bar', $s);
$this->assertContains(
'Content-Disposition: form-data; name="third"',
$s
);
$this->assertContains(
'Content-Disposition: form-data; name="other"',
$s
);
}
public function testMultipartWithBase64Fields()
@ -158,13 +180,40 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$b = new PostBody();
$b->setField('foo64', '/xA2JhWEqPcgyLRDdir9WSRi/khpb2Lh3ooqv+5VYoc=');
$b->forceMultipartUpload(true);
$this->assertEquals(['foo64' => '/xA2JhWEqPcgyLRDdir9WSRi/khpb2Lh3ooqv+5VYoc='], $b->getFields());
$this->assertEquals(
['foo64' => '/xA2JhWEqPcgyLRDdir9WSRi/khpb2Lh3ooqv+5VYoc='],
$b->getFields()
);
$m = new Request('POST', '/');
$b->applyRequestHeaders($m);
$this->assertContains('multipart/form-data', (string) $m->getHeader('Content-Type'));
$this->assertContains(
'multipart/form-data',
$m->getHeader('Content-Type')
);
$this->assertTrue($m->hasHeader('Content-Length'));
$contents = $b->getContents();
$this->assertContains('name="foo64"', $contents);
$this->assertContains('/xA2JhWEqPcgyLRDdir9WSRi/khpb2Lh3ooqv+5VYoc=', $contents);
$this->assertContains(
'/xA2JhWEqPcgyLRDdir9WSRi/khpb2Lh3ooqv+5VYoc=',
$contents
);
}
public function testMultipartWithAmpersandInValue()
{
$b = new PostBody();
$b->setField('a', 'b&c=d');
$b->forceMultipartUpload(true);
$this->assertEquals(['a' => 'b&c=d'], $b->getFields());
$m = new Request('POST', '/');
$b->applyRequestHeaders($m);
$this->assertContains(
'multipart/form-data',
$m->getHeader('Content-Type')
);
$this->assertTrue($m->hasHeader('Content-Length'));
$contents = $b->getContents();
$this->assertContains('name="a"', $contents);
$this->assertContains('b&c=d', $contents);
}
}

View File

@ -28,6 +28,18 @@ class HistoryTest extends \PHPUnit_Framework_TestCase
$ev = new ErrorEvent($t, $e);
$h = new History(2);
$h->onError($ev);
// Only tracks when no response is present
$this->assertEquals([], $h->getRequests());
}
public function testLogsConnectionErrors()
{
$request = new Request('GET', '/');
$t = new Transaction(new Client(), $request);
$e = new RequestException('foo', $request);
$ev = new ErrorEvent($t, $e);
$h = new History();
$h->onError($ev);
$this->assertEquals([$request], $h->getRequests());
}

View File

@ -162,7 +162,9 @@ class UrlTest extends \PHPUnit_Framework_TestCase
['http://www.example.com/path', 'http://u:a@www.example.com/', 'http://u:a@www.example.com/'],
['/path?q=2', 'http://www.test.com/', 'http://www.test.com/path?q=2'],
['http://api.flickr.com/services/', 'http://www.flickr.com/services/oauth/access_token', 'http://www.flickr.com/services/oauth/access_token'],
['https://www.example.com/path', '//foo.com/abc', 'https://foo.com/abc'],
['https://www.example.com/path', '//foo.com/abc', 'https://foo.com/abc'],
['https://www.example.com/0/', 'relative/foo', 'https://www.example.com/0/relative/foo'],
['', '0', '0'],
// RFC 3986 test cases
[self::RFC3986_BASE, 'g:h', 'g:h'],
[self::RFC3986_BASE, 'g', 'http://a/b/c/g'],

View File

@ -7,6 +7,7 @@ php:
- hhvm
before_script:
- composer install
- composer self-update
- composer install --no-interaction --prefer-source --dev
script: vendor/bin/phpunit

View File

@ -2,6 +2,27 @@
Changelog
=========
1.5.1 (2014-09-10)
------------------
* Stream metadata is grabbed from the underlying stream each time
``getMetadata`` is called rather than returning a value from a cache.
* Properly closing all underlying streams when AppendStream is closed.
* Seek functions no longer throw exceptions.
* LazyOpenStream now correctly returns the underlying stream resource when
detached.
1.5.0 (2014-08-07)
------------------
* Added ``Stream\safe_open`` to open stream resources and throw exceptions
instead of raising errors.
1.4.0 (2014-07-19)
------------------
* Added a LazyOpenStream
1.3.0 (2014-07-15)
------------------

View File

@ -1,5 +1,11 @@
all: clean coverage
release: tag
git push origin --tags
tag:
chag tag --sign --debug CHANGELOG.rst
test:
vendor/bin/phpunit

View File

@ -17,7 +17,7 @@ Simply add the following to the composer.json file at the root of your project:
{
"require": {
"guzzlehttp/streams": "1.*"
"guzzlehttp/streams": "~1.0"
}
}

View File

@ -23,7 +23,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
"dev-master": "1.5-dev"
}
}
}

View File

@ -86,7 +86,6 @@ class AppendStream implements StreamInterface
*/
public function detach()
{
$this->streams = [];
$this->close();
}
@ -132,12 +131,8 @@ class AppendStream implements StreamInterface
*/
public function seek($offset, $whence = SEEK_SET)
{
if (!$this->seekable) {
if (!$this->seekable || $whence !== SEEK_SET) {
return false;
} elseif ($whence !== SEEK_SET) {
throw new \InvalidArgumentException(
'AppendStream only supports SEEK_SET'
);
}
$success = true;
@ -207,6 +202,6 @@ class AppendStream implements StreamInterface
public function write($string)
{
return 0;
return false;
}
}

View File

@ -47,8 +47,7 @@ class CachingStream implements StreamInterface, MetadataStreamInterface
} elseif ($whence == SEEK_CUR) {
$byte = $offset + $this->tell();
} else {
throw new \RuntimeException(__CLASS__ . ' supports only SEEK_SET'
.' and SEEK_CUR seek operations');
return false;
}
// You cannot skip ahead past where you've read from the remote stream

View File

@ -0,0 +1,117 @@
<?php
namespace GuzzleHttp\Stream;
/**
* Lazily reads or writes to a file that is opened only after an IO operation
* take place on the stream.
*/
class LazyOpenStream implements StreamInterface, MetadataStreamInterface
{
/** @var string File to open */
private $filename;
/** @var string $mode */
private $mode;
/** @var MetadataStreamInterface */
private $stream;
/**
* @param string $filename File to lazily open
* @param string $mode fopen mode to use when opening the stream
*/
public function __construct($filename, $mode)
{
$this->filename = $filename;
$this->mode = $mode;
}
public function __toString()
{
try {
return (string) $this->getStream();
} catch (\Exception $e) {
return '';
}
}
private function getStream()
{
if (!$this->stream) {
$this->stream = create(safe_open($this->filename, $this->mode));
}
return $this->stream;
}
public function getContents($maxLength = -1)
{
return copy_to_string($this->getStream(), $maxLength);
}
public function close()
{
if ($this->stream) {
$this->stream->close();
}
}
public function detach()
{
$stream = $this->getStream();
$result = $stream->detach();
$this->close();
return $result;
}
public function tell()
{
return $this->stream ? $this->stream->tell() : 0;
}
public function getSize()
{
return $this->getStream()->getSize();
}
public function eof()
{
return $this->getStream()->eof();
}
public function seek($offset, $whence = SEEK_SET)
{
return $this->getStream()->seek($offset, $whence);
}
public function read($length)
{
return $this->getStream()->read($length);
}
public function isReadable()
{
return $this->getStream()->isReadable();
}
public function isWritable()
{
return $this->getStream()->isWritable();
}
public function isSeekable()
{
return $this->getStream()->isSeekable();
}
public function write($string)
{
return $this->getStream()->write($string);
}
public function getMetadata($key = null)
{
return $this->getStream()->getMetadata($key);
}
}

View File

@ -7,19 +7,12 @@ namespace GuzzleHttp\Stream;
*/
class Stream implements MetadataStreamInterface
{
/** @var resource Stream resource */
private $stream;
/** @var int Size of the stream contents in bytes */
private $size;
/** @var bool */
private $seekable;
private $readable;
private $writable;
/** @var array Stream metadata */
private $meta = [];
private $uri;
/** @var array Hash of readable and writable stream types */
private static $readWriteHash = [
@ -66,10 +59,11 @@ class Stream implements MetadataStreamInterface
$this->size = $size;
$this->stream = $stream;
$this->meta = stream_get_meta_data($this->stream);
$this->seekable = $this->meta['seekable'];
$this->readable = isset(self::$readWriteHash['read'][$this->meta['mode']]);
$this->writable = isset(self::$readWriteHash['write'][$this->meta['mode']]);
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'];
$this->readable = isset(self::$readWriteHash['read'][$meta['mode']]);
$this->writable = isset(self::$readWriteHash['write'][$meta['mode']]);
$this->uri = isset($meta['uri']) ? $meta['uri'] : null;
}
/**
@ -104,14 +98,13 @@ class Stream implements MetadataStreamInterface
fclose($this->stream);
}
$this->meta = [];
$this->stream = null;
$this->detach();
}
public function detach()
{
$result = $this->stream;
$this->stream = $this->size = null;
$this->stream = $this->size = $this->uri = null;
$this->readable = $this->writable = $this->seekable = false;
return $result;
@ -121,13 +114,15 @@ class Stream implements MetadataStreamInterface
{
if ($this->size !== null) {
return $this->size;
} elseif (!$this->stream) {
}
if (!$this->stream) {
return null;
}
// If the stream is a file based stream and local, then use fstat
if (isset($this->meta['uri'])) {
clearstatcache(true, $this->meta['uri']);
// Clear the stat cache if the stream has a URI
if ($this->uri) {
clearstatcache(true, $this->uri);
}
$stats = fstat($this->stream);
@ -207,8 +202,8 @@ class Stream implements MetadataStreamInterface
*/
public function getMetadata($key = null)
{
return !$key
? $this->meta
: (isset($this->meta[$key]) ? $this->meta[$key] : null);
$meta = $this->stream ? stream_get_meta_data($this->stream) : [];
return !$key ? $meta : (isset($meta[$key]) ? $meta[$key] : null);
}
}

View File

@ -54,7 +54,7 @@ trait StreamDecoratorTrait
public function close()
{
return $this->stream->close();
$this->stream->close();
}
public function getMetadata($key = null)
@ -66,9 +66,7 @@ trait StreamDecoratorTrait
public function detach()
{
$this->stream->detach();
return $this;
return $this->stream->detach();
}
public function getSize()

View File

@ -171,4 +171,39 @@ if (!defined('GUZZLE_STREAMS_FUNCTIONS')) {
return $buffer;
}
/**
* Safely opens a PHP stream resource using a filename.
*
* When fopen fails, PHP normally raises a warning. This function adds an
* error handler that checks for errors and throws an exception instead.
*
* @param string $filename File to open
* @param string $mode Mode used to open the file
*
* @return resource
* @throws \RuntimeException if the file cannot be opened
*/
function safe_open($filename, $mode)
{
$ex = null;
set_error_handler(function () use ($filename, $mode, &$ex) {
$ex = new \RuntimeException(sprintf(
'Unable to open %s using mode %s: %s',
$filename,
$mode,
func_get_args()[1]
));
});
$handle = fopen($filename, $mode);
restore_error_handler();
if ($ex) {
/** @var $ex \RuntimeException */
throw $ex;
}
return $handle;
}
}

View File

@ -22,14 +22,10 @@ class AppendStreamTest extends \PHPUnit_Framework_TestCase
$a->addStream($s);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage only supports SEEK_SET
*/
public function testValidatesSeekType()
{
$a = new AppendStream();
$a->seek(100, SEEK_CUR);
$this->assertFalse($a->seek(100, SEEK_CUR));
}
public function testTriesToRewindOnSeek()
@ -92,7 +88,7 @@ class AppendStreamTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($a->isWritable());
$this->assertTrue($a->isSeekable());
$this->assertTrue($a->isReadable());
$this->assertSame(0, $a->write('foo'));
$this->assertFalse($a->write('foo'));
}
public function testDoesNotNeedStreams()

View File

@ -44,13 +44,9 @@ class CachingStreamTest extends \PHPUnit_Framework_TestCase
$this->body->seek(10);
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage supports only SEEK_SET and SEEK_CUR
*/
public function testCannotUseSeekEnd()
{
$this->body->seek(2, SEEK_END);
$this->assertFalse($this->body->seek(2, SEEK_END));
}
public function testRewindUsesSeek()

View File

@ -0,0 +1,61 @@
<?php
namespace GuzzleHttp\Tests\Stream;
use GuzzleHttp\Stream\LazyOpenStream;
class LazyOpenStreamTest extends \PHPUnit_Framework_TestCase
{
private $fname;
public function setup()
{
$this->fname = tempnam('/tmp', 'tfile');
if (file_exists($this->fname)) {
unlink($this->fname);
}
}
public function tearDown()
{
if (file_exists($this->fname)) {
unlink($this->fname);
}
}
public function testOpensLazily()
{
$l = new LazyOpenStream($this->fname, 'w+');
$l->write('foo');
$this->assertInternalType('array', $l->getMetadata());
$this->assertFileExists($this->fname);
$this->assertEquals('foo', file_get_contents($this->fname));
}
public function testProxiesToFile()
{
file_put_contents($this->fname, 'foo');
$l = new LazyOpenStream($this->fname, 'r');
$this->assertEquals('foo', $l->read(4));
$this->assertTrue($l->eof());
$this->assertEquals(3, $l->tell());
$this->assertTrue($l->isReadable());
$this->assertTrue($l->isSeekable());
$this->assertFalse($l->isWritable());
$l->seek(1);
$this->assertEquals('oo', $l->getContents());
$this->assertEquals('foo', (string) $l);
$this->assertEquals(3, $l->getSize());
$this->assertInternalType('array', $l->getMetadata());
$l->close();
}
public function testDetachesUnderlyingStream()
{
file_put_contents($this->fname, 'foo');
$l = new LazyOpenStream($this->fname, 'r');
$r = $l->detach();
$this->assertInternalType('resource', $r);
fclose($r);
}
}

View File

@ -7,6 +7,7 @@ use GuzzleHttp\Stream\NoSeekStream;
/**
* @covers GuzzleHttp\Stream\NoSeekStream
* @covers GuzzleHttp\Stream\StreamDecoratorTrait
*/
class NoSeekStreamTest extends \PHPUnit_Framework_TestCase
{
@ -21,4 +22,12 @@ class NoSeekStreamTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($wrapped->isSeekable());
$this->assertFalse($wrapped->seek(2));
}
public function testHandlesClose()
{
$s = Stream::factory('foo');
$wrapped = new NoSeekStream($s);
$wrapped->close();
$this->assertFalse($wrapped->write('foo'));
}
}

View File

@ -151,6 +151,19 @@ class StreamTest extends \PHPUnit_Framework_TestCase
$stream->close();
}
public function testCloseClearProperties()
{
$handle = fopen('php://temp', 'r+');
$stream = new Stream($handle);
$stream->close();
$this->assertEmpty($stream->getMetadata());
$this->assertFalse($stream->isSeekable());
$this->assertFalse($stream->isReadable());
$this->assertFalse($stream->isWritable());
$this->assertNull($stream->getSize());
}
public function testCreatesWithFactory()
{
$stream = Stream::factory('foo');

View File

@ -116,6 +116,22 @@ class functionsTest extends \PHPUnit_Framework_TestCase
{
Stream\create(new \stdClass());
}
public function testOpensFilesSuccessfully()
{
$r = Stream\safe_open(__FILE__, 'r');
$this->assertInternalType('resource', $r);
fclose($r);
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Unable to open /path/to/does/not/exist using mode r
*/
public function testThrowsExceptionNotWarning()
{
Stream\safe_open('/path/to/does/not/exist', 'r');
}
}
class HasToString