Issue #2461985 by stefan.r: Update Guzzle to latest release

8.0.x
Alex Pott 2015-03-31 19:04:21 +01:00
parent 5271f8f028
commit ed89a08f25
45 changed files with 982 additions and 553 deletions

21
core/composer.lock generated
View File

@ -783,16 +783,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "5.0.3",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "6c72627de1d66832e4270e36e56acdb0d1d8f282"
"reference": "475b29ccd411f2fa8a408e64576418728c032cfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/6c72627de1d66832e4270e36e56acdb0d1d8f282",
"reference": "6c72627de1d66832e4270e36e56acdb0d1d8f282",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/475b29ccd411f2fa8a408e64576418728c032cfa",
"reference": "475b29ccd411f2fa8a408e64576418728c032cfa",
"shasum": ""
},
"require": {
@ -837,20 +837,20 @@
"rest",
"web service"
],
"time": "2014-11-04 07:09:15"
"time": "2015-01-28 01:03:29"
},
{
"name": "guzzlehttp/ringphp",
"version": "1.0.3",
"version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/guzzle/RingPHP.git",
"reference": "e7c28f96c5ac12ab0e63412cfc15989756fcb964"
"reference": "52d868f13570a9a56e5fce6614e0ec75d0f13ac2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/e7c28f96c5ac12ab0e63412cfc15989756fcb964",
"reference": "e7c28f96c5ac12ab0e63412cfc15989756fcb964",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/52d868f13570a9a56e5fce6614e0ec75d0f13ac2",
"reference": "52d868f13570a9a56e5fce6614e0ec75d0f13ac2",
"shasum": ""
},
"require": {
@ -887,7 +887,8 @@
"homepage": "https://github.com/mtdowling"
}
],
"time": "2014-11-04 07:01:14"
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"time": "2015-03-30 01:43:20"
},
{
"name": "guzzlehttp/streams",

View File

@ -1218,66 +1218,6 @@
"templating"
]
},
{
"name": "guzzlehttp/guzzle",
"version": "5.0.3",
"version_normalized": "5.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "6c72627de1d66832e4270e36e56acdb0d1d8f282"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/6c72627de1d66832e4270e36e56acdb0d1d8f282",
"reference": "6c72627de1d66832e4270e36e56acdb0d1d8f282",
"shasum": ""
},
"require": {
"guzzlehttp/ringphp": "~1.0",
"php": ">=5.4.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0"
},
"time": "2014-11-04 07:09:15",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"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": "egulias/email-validator",
"version": "1.2.5",
@ -1382,58 +1322,6 @@
"hhvm"
]
},
{
"name": "guzzlehttp/ringphp",
"version": "1.0.3",
"version_normalized": "1.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/RingPHP.git",
"reference": "e7c28f96c5ac12ab0e63412cfc15989756fcb964"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/e7c28f96c5ac12ab0e63412cfc15989756fcb964",
"reference": "e7c28f96c5ac12ab0e63412cfc15989756fcb964",
"shasum": ""
},
"require": {
"guzzlehttp/streams": "~3.0",
"php": ">=5.4.0",
"react/promise": "~2.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"time": "2014-11-04 07:01:14",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\Ring\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
]
},
{
"name": "easyrdf/easyrdf",
"version": "0.9.0",
@ -3156,5 +3044,118 @@
],
"description": "Symfony BrowserKit Component",
"homepage": "http://symfony.com"
},
{
"name": "guzzlehttp/guzzle",
"version": "5.2.0",
"version_normalized": "5.2.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "475b29ccd411f2fa8a408e64576418728c032cfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/475b29ccd411f2fa8a408e64576418728c032cfa",
"reference": "475b29ccd411f2fa8a408e64576418728c032cfa",
"shasum": ""
},
"require": {
"guzzlehttp/ringphp": "~1.0",
"php": ">=5.4.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0"
},
"time": "2015-01-28 01:03:29",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"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": "guzzlehttp/ringphp",
"version": "1.0.7",
"version_normalized": "1.0.7.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/RingPHP.git",
"reference": "52d868f13570a9a56e5fce6614e0ec75d0f13ac2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/52d868f13570a9a56e5fce6614e0ec75d0f13ac2",
"reference": "52d868f13570a9a56e5fce6614e0ec75d0f13ac2",
"shasum": ""
},
"require": {
"guzzlehttp/streams": "~3.0",
"php": ">=5.4.0",
"react/promise": "~2.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"time": "2015-03-30 01:43:20",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"GuzzleHttp\\Ring\\": "src/"
}
},
"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 API and specification that abstracts away the details of HTTP into a single PHP function."
}
]

View File

@ -1,5 +1,35 @@
# CHANGELOG
## 5.2.0 - 2015-01-27
* Added `AppliesHeadersInterface` to make applying headers to a request based
on the body more generic and not specific to `PostBodyInterface`.
* Reduced the number of stack frames needed to send requests.
* Nested futures are now resolved in the client rather than the RequestFsm
* Finishing state transitions is now handled in the RequestFsm rather than the
RingBridge.
* Added a guard in the Pool class to not use recursion for request retries.
## 5.1.0 - 2014-12-19
* Pool class no longer uses recursion when a request is intercepted.
* The size of a Pool can now be dynamically adjusted using a callback.
See https://github.com/guzzle/guzzle/pull/943.
* Setting a request option to `null` when creating a request with a client will
ensure that the option is not set. This allows you to overwrite default
request options on a per-request basis.
See https://github.com/guzzle/guzzle/pull/937.
* Added the ability to limit which protocols are allowed for redirects by
specifying a `protocols` array in the `allow_redirects` request option.
* Nested futures due to retries are now resolved when waiting for synchronous
responses. See https://github.com/guzzle/guzzle/pull/947.
* `"0"` is now an allowed URI path. See
https://github.com/guzzle/guzzle/pull/935.
* `Query` no longer typehints on the `$query` argument in the constructor,
allowing for strings and arrays.
* Exceptions thrown in the `end` event are now correctly wrapped with Guzzle
specific exceptions if necessary.
## 5.0.3 - 2014-11-03
This change updates query strings so that they are treated as un-encoded values
@ -71,10 +101,10 @@ The breaking changes in this release are relatively minor. The biggest thing to
look out for is that request and response objects no longer implement fluent
interfaces.
* Removed the fluent interfaces (i.e., ``return $this``) from requests,
responses, ``GuzzleHttp\Collection``, ``GuzzleHttp\Url``,
``GuzzleHttp\Query``, ``GuzzleHttp\Post\PostBody``, and
``GuzzleHttp\Cookie\SetCookie``. This blog post provides a good outline of
* Removed the fluent interfaces (i.e., `return $this`) from requests,
responses, `GuzzleHttp\Collection`, `GuzzleHttp\Url`,
`GuzzleHttp\Query`, `GuzzleHttp\Post\PostBody`, and
`GuzzleHttp\Cookie\SetCookie`. This blog post provides a good outline of
why I did this: http://ocramius.github.io/blog/fluent-interfaces-are-evil/.
This also makes the Guzzle message interfaces compatible with the current
PSR-7 message proposal.
@ -84,7 +114,7 @@ interfaces.
moved to `GuzzleHttp\Utils::jsonDecode`. `GuzzleHttp\get_path` moved to
`GuzzleHttp\Utils::getPath`. `GuzzleHttp\set_path` moved to
`GuzzleHttp\Utils::setPath`. `GuzzleHttp\batch` should now be
`GuzzleHttp\Pool::batch`, which returns a bjectStorage`. Using functions.php
`GuzzleHttp\Pool::batch`, which returns an `objectStorage`. Using functions.php
caused problems for many users: they aren't PSR-4 compliant, require an
explicit include, and needed an if-guard to ensure that the functions are not
declared multiple times.
@ -105,10 +135,10 @@ interfaces.
written to.
* Removed the `asArray` parameter from
`GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header
value as an array, then use the newly added ``getHeaderAsArray()`` method of
``MessageInterface``. This change makes the Guzzle interfaces compatible with
value as an array, then use the newly added `getHeaderAsArray()` method of
`MessageInterface`. This change makes the Guzzle interfaces compatible with
the PSR-7 interfaces.
* ``GuzzleHttp\Message\MessageFactory`` no longer allows subclasses to add
* `GuzzleHttp\Message\MessageFactory` no longer allows subclasses to add
custom request options using double-dispatch (this was an implementation
detail). Instead, you should now provide an associative array to the
constructor which is a mapping of the request option name mapping to a
@ -121,9 +151,9 @@ interfaces.
* `GuzzleHttp\Stream\StreamInterface::getContents()` no longer accepts a
`maxLen` parameter. This update makes the Guzzle streams project
compatible with the current PSR-7 proposal.
* ``GuzzleHttp\Stream\Stream::__construct``,
``GuzzleHttp\Stream\Stream::factory``, and
``GuzzleHttp\Stream\Utils::create`` no longer accept a size in the second
* `GuzzleHttp\Stream\Stream::__construct`,
`GuzzleHttp\Stream\Stream::factory`, and
`GuzzleHttp\Stream\Utils::create` no longer accept a size in the second
argument. They now accept an associative array of options, including the
"size" key and "metadata" key which can be used to provide custom metadata.
@ -359,7 +389,7 @@ interfaces.
* Bug fix: FilterIterator now relies on `\Iterator` instead of `\Traversable`.
* Bug fix: Gracefully handling malformed responses in RequestMediator::writeResponseBody()
* Bug fix: Replaced call to canCache with canCacheRequest in the CallbackCanCacheStrategy of the CachePlugin
* Bug fix: Visiting XML attributes first before visting XML children when serializing requests
* Bug fix: Visiting XML attributes first before visiting XML children when serializing requests
* Bug fix: Properly parsing headers that contain commas contained in quotes
* Bug fix: mimetype guessing based on a filename is now case-insensitive
@ -502,7 +532,7 @@ interfaces.
directly via interfaces
* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist
but are a no-op until removed.
* Most classes that used to require a ``Guzzle\Service\Command\CommandInterface` typehint now request a
* Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a
`Guzzle\Service\Command\ArrayCommandInterface`.
* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response
on a request while the request is still being transferred
@ -517,7 +547,7 @@ interfaces.
## 3.5.0 - 2013-05-13
* Bug: Fixed a regression so that request responses are parsed only once per oncomplete event rather than multiple times
* Bug: Better cleanup of one-time events accross the board (when an event is meant to fire once, it will now remove
* Bug: Better cleanup of one-time events across the board (when an event is meant to fire once, it will now remove
itself from the EventDispatcher)
* Bug: `Guzzle\Log\MessageFormatter` now properly writes "total_time" and "connect_time" values
* Bug: Cloning an EntityEnclosingRequest now clones the EntityBody too
@ -833,15 +863,15 @@ interfaces.
* Bug: Fixed a bug in ExponentialBackoffPlugin that caused fatal errors when retrying an EntityEnclosingRequest that does not have a body
* Bug: Setting the response body of a request to null after completing a request, not when setting the state of a request to new
* Added multiple inheritance to service description commands
* Added an ApiCommandInterface and added ``getParamNames()`` and ``hasParam()``
* Added an ApiCommandInterface and added `getParamNames()` and `hasParam()`
* Removed the default 2mb size cutoff from the Md5ValidatorPlugin so that it now defaults to validating everything
* Changed CurlMulti::perform to pass a smaller timeout to CurlMulti::executeHandles
## 2.8.2 - 2012-07-24
* Bug: Query string values set to 0 are no longer dropped from the query string
* Bug: A Collection object is no longer created each time a call is made to ``Guzzle\Service\Command\AbstractCommand::getRequestHeaders()``
* Bug: ``+`` is now treated as an encoded space when parsing query strings
* Bug: A Collection object is no longer created each time a call is made to `Guzzle\Service\Command\AbstractCommand::getRequestHeaders()`
* Bug: `+` is now treated as an encoded space when parsing query strings
* QueryString and Collection performance improvements
* Allowing dot notation for class paths in filters attribute of a service descriptions
@ -858,7 +888,7 @@ interfaces.
* Changed setEncodeValues(bool) and setEncodeFields(bool) to useUrlEncoding(bool)
* Changed the aggregation functions of QueryString to be static methods
* Can now use fromString() with querystrings that have a leading ?
* cURL configuration values can be specified in service descriptions using ``curl.`` prefixed parameters
* cURL configuration values can be specified in service descriptions using `curl.` prefixed parameters
* Content-Length is set to 0 before emitting the request.before_send event when sending an empty request body
* Cookies are no longer URL decoded by default
* Bug: URI template variables set to null are no longer expanded

View File

@ -503,7 +503,7 @@ allow developers to more easily extend and decorate stream behavior.
## Metadata streams
`GuzzleHttp\Stream\MetadataStreamInterface` has been added to denote streams
that contain additonal metadata accessible via `getMetadata()`.
that contain additional metadata accessible via `getMetadata()`.
`GuzzleHttp\Stream\StreamInterface::getMetadata` and
`GuzzleHttp\Stream\StreamInterface::setMetadata` have been removed.

View File

@ -13,7 +13,7 @@ foreach (['README.md', 'LICENSE'] as $file) {
// Copy each dependency to the staging directory. Copy *.php and *.pem files.
$packager->recursiveCopy('src', 'GuzzleHttp', ['php']);
$packager->recursiveCopy('vendor/react/promise/src', '');
$packager->recursiveCopy('vendor/react/promise/src', 'React/Promise');
$packager->recursiveCopy('vendor/guzzlehttp/ringphp/src', 'GuzzleHttp/Ring');
$packager->recursiveCopy('vendor/guzzlehttp/streams/src', 'GuzzleHttp/Stream');
$packager->createAutoloader(['React/Promise/functions.php']);

View File

@ -173,6 +173,12 @@ response has completed.
If an exception occurred while transferring the future response, then the
exception encountered will be thrown when dereferencing.
.. note::
It depends on the RingPHP handler used by a client, but you typically need
to use the same RingPHP handler in order to utilize asynchronous requests
across multiple clients.
Asynchronous Error Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -524,7 +530,7 @@ json
.. code-block:: php
$request = $client->createRequest('/put', ['json' => ['foo' => 'bar']]);
$request = $client->createRequest('PUT', '/put', ['json' => ['foo' => 'bar']]);
echo $request->getHeader('Content-Type');
// application/json
echo $request->getBody();
@ -701,7 +707,15 @@ allow_redirects
:Types:
- bool
- array
:Default: ``['max' => 5, 'strict' => false, 'referer' => true]``
:Default:
::
[
'max' => 5,
'strict' => false,
'referer' => true,
'protocols' => ['http', 'https']
]
Set to ``false`` to disable redirects.
@ -721,19 +735,22 @@ number of 5 redirects.
// 200
Pass an associative array containing the 'max' key to specify the maximum
number of redirects, optionally provide a 'strict' key value to specify
whether or not to use strict RFC compliant redirects (meaning redirect POST
requests with POST requests vs. doing what most browsers do which is redirect
POST requests with GET requests), and optionally provide a 'referer' key to
specify whether or not the "Referer" header should be added when redirecting.
number of redirects, provide a 'strict' key value to specify whether or not to
use strict RFC compliant redirects (meaning redirect POST requests with POST
requests vs. doing what most browsers do which is redirect POST requests with
GET requests), provide a 'referer' key to specify whether or not the "Referer"
header should be added when redirecting, and provide a 'protocols' array that
specifies which protocols are supported for redirects (defaults to
``['http', 'https']``).
.. code-block:: php
$res = $client->get('/redirect/3', [
'allow_redirects' => [
'max' => 10,
'strict' => true,
'referer' => true
'max' => 10, // allow at most 10 redirects.
'strict' => true, // use "strict" RFC compliant redirects.
'referer' => true, // add a Referer header
'protocols' => ['https'] // only allow https URLs
]
]);
echo $res->getStatusCode();

View File

@ -11,12 +11,12 @@ Event Emitters
==============
Clients, requests, and any other class that implements the
``GuzzleHttp\Common\HasEmitterInterface`` interface have a
``GuzzleHttp\Common\EventEmitter`` object. You can add event *listeners* and
``GuzzleHttp\Event\HasEmitterInterface`` interface have a
``GuzzleHttp\Event\Emitter`` object. You can add event *listeners* and
event *subscribers* to an event *emitter*.
emitter
An object that implements ``GuzzleHttp\Common\EventEmitterInterface``. This
An object that implements ``GuzzleHttp\Event\EmitterInterface``. This
object emits named events to event listeners. You may register event
listeners on subscribers on an emitter.
@ -58,7 +58,7 @@ propagation
Getting an EventEmitter
-----------------------
You can get the event emitter of ``GuzzleHttp\Common\HasEmitterInterface``
You can get the event emitter of ``GuzzleHttp\Event\HasEmitterInterface``
object using the the ``getEmitter()`` method. Here's an example of getting a
client object's event emitter.
@ -95,7 +95,7 @@ event is triggered, and optionally provide a priority.
});
When a listener is triggered, it is passed an event that implements the
``GuzzleHttp\Common\EventInterface`` interface, the name of the event, and the
``GuzzleHttp\Event\EventInterface`` interface, the name of the event, and the
event emitter itself. The above example could more verbosely be written as
follows:
@ -134,7 +134,7 @@ state. This technique is used in Guzzle extensively when intercepting error
events with responses.
You can stop the propagation of an event using the ``stopPropagation()`` method
of a ``GuzzleHttp\Common\EventInterface`` object:
of a ``GuzzleHttp\Event\EventInterface`` object:
.. code-block:: php
@ -168,7 +168,7 @@ Event Subscribers
-----------------
Event subscribers are classes that implement the
``GuzzleHttp\Common\EventSubscriberInterface`` object. They are used to register
``GuzzleHttp\Event\SubscriberInterface`` object. They are used to register
one or more event listeners to methods of the class. Event subscribers tell
event emitters exactly which events to listen to and what method to invoke on
the class when the event is triggered by called the ``getEvents()`` method of
@ -213,6 +213,18 @@ priority of the listener (as shown in the ``before`` listener in the example).
}
}
To register the listeners the subscriber needs to be attached to the emitter:
.. code-block:: php
$client = new GuzzleHttp\Client();
$emitter = $client->getEmitter();
$subscriber = new SimpleSubscriber();
$emitter->attach($subscriber);
//to remove the listeners
$emitter->detach($subscriber);
.. note::
You can specify event priorities using integers or ``"first"`` and
@ -315,7 +327,7 @@ a ``GuzzleHttp\Event\BeforeEvent``.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Common\EmitterInterface;
use GuzzleHttp\Event\EmitterInterface;
use GuzzleHttp\Event\BeforeEvent;
$client = new Client(['base_url' => 'http://httpbin.org']);
@ -480,7 +492,7 @@ end
---
The ``end`` event is a terminal event, emitted once per request, that provides
access to the repsonse that was received or the exception that was encountered.
access to the response that was received or the exception that was encountered.
The event emitted is a ``GuzzleHttp\Event\EndEvent``.
This event can be intercepted, but keep in mind that the ``complete`` event

View File

@ -64,7 +64,7 @@ an HTTP response into a more meaningful model object.
- `Guzzle Command <https://github.com/guzzle/command>`_: Provides the building
blocks for service description abstraction.
- `Guzzle Services <https://github.com/guzzle/guzzle-services>`_: Provides an
implementation of "Guzzle Command" that utlizes Guzzle's service description
implementation of "Guzzle Command" that utilizes Guzzle's service description
format.
Does Guzzle require cURL?
@ -91,7 +91,7 @@ Can Guzzle send asynchronous requests?
Yes. Pass the ``future`` true request option to a request to send it
asynchronously. Guzzle will then return a ``GuzzleHttp\Message\FutureResponse``
object that can be used synchronously by accessing the response object like a
normal response, and it can be used asynchronoulsy using a promise that is
normal response, and it can be used asynchronously using a promise that is
notified when the response is resolved with a real response or rejected with an
exception.

View File

@ -351,7 +351,7 @@ method of a request.
$request = $client->createRequest('GET', '/');
$config = $request->getConfig();
The config object is a ``GuzzleHttp\Common\Collection`` object that acts like
The config object is a ``GuzzleHttp\Collection`` object that acts like
an associative array. You can grab values from the collection using array like
access. You can also modify and remove values using array like access.
@ -393,7 +393,7 @@ allow customization through request configuration options.
Event Emitter
-------------
Request objects implement ``GuzzleHttp\Common\HasEmitterInterface``, so they
Request objects implement ``GuzzleHttp\Event\HasEmitterInterface``, so they
have a method called ``getEmitter()`` that can be used to get an event emitter
used by the request. Any listener or subscriber attached to a request will only
be triggered for the lifecycle events of a specific request. Conversely, adding

View File

@ -177,26 +177,14 @@ class Client implements ClientInterface
public function createRequest($method, $url = null, array $options = [])
{
$headers = $this->mergeDefaults($options);
$options = $this->mergeDefaults($options);
// Use a clone of the client's emitter
$options['config']['emitter'] = clone $this->getEmitter();
$url = $url || (is_string($url) && strlen($url))
? $this->buildUrl($url)
: (string) $this->baseUrl;
$request = $this->messageFactory->createRequest(
$method,
$url ? (string) $this->buildUrl($url) : (string) $this->baseUrl,
$options
);
// Merge in default headers
if ($headers) {
foreach ($headers as $key => $value) {
if (!$request->hasHeader($key)) {
$request->setHeader($key, $value);
}
}
}
return $request;
return $this->messageFactory->createRequest($method, $url, $options);
}
public function get($url = null, $options = [])
@ -236,30 +224,31 @@ class Client implements ClientInterface
public function send(RequestInterface $request)
{
$trans = new Transaction($this, $request);
$isFuture = $request->getConfig()->get('future');
$trans = new Transaction($this, $request, $isFuture);
$fn = $this->fsm;
// Ensure a future response is returned if one was requested.
if ($request->getConfig()->get('future')) {
try {
$fn($trans);
try {
$fn($trans);
if ($isFuture) {
// Turn the normal response into a future if needed.
return $trans->response instanceof FutureInterface
? $trans->response
: new FutureResponse(new FulfilledPromise($trans->response));
} catch (RequestException $e) {
// Wrap the exception in a promise if the user asked for a future.
}
// Resolve deep futures if this is not a future
// transaction. This accounts for things like retries
// that do not have an immediate side-effect.
while ($trans->response instanceof FutureInterface) {
$trans->response = $trans->response->wait();
}
return $trans->response;
} catch (\Exception $e) {
if ($isFuture) {
// Wrap the exception in a promise
return new FutureResponse(new RejectedPromise($e));
}
} else {
try {
$fn($trans);
return $trans->response instanceof FutureInterface
? $trans->response->wait()
: $trans->response;
} catch (\Exception $e) {
throw RequestException::wrapException($trans->request, $e);
}
throw RequestException::wrapException($trans->request, $e);
}
}
@ -292,9 +281,10 @@ class Client implements ClientInterface
/**
* Expand a URI template and inherit from the base URL if it's relative
*
* @param string|array $url URL or URI template to expand
*
* @param string|array $url URL or an array of the URI template to expand
* followed by a hash of template varnames.
* @return string
* @throws \InvalidArgumentException
*/
private function buildUrl($url)
{
@ -305,6 +295,11 @@ class Client implements ClientInterface
: (string) $this->baseUrl->combine($url);
}
if (!isset($url[1])) {
throw new \InvalidArgumentException('You must provide a hash of '
. 'varname options in the second element of a URL array.');
}
// Absolute URL
if (strpos($url[0], '://')) {
return Utils::uriTemplate($url[0], $url[1]);
@ -320,7 +315,12 @@ class Client implements ClientInterface
{
if (!isset($config['base_url'])) {
$this->baseUrl = new Url('', '');
} elseif (is_array($config['base_url'])) {
} elseif (!is_array($config['base_url'])) {
$this->baseUrl = Url::fromString($config['base_url']);
} elseif (count($config['base_url']) < 2) {
throw new \InvalidArgumentException('You must provide a hash of '
. 'varname options in the second element of a base_url array.');
} else {
$this->baseUrl = Url::fromString(
Utils::uriTemplate(
$config['base_url'][0],
@ -328,8 +328,6 @@ class Client implements ClientInterface
)
);
$config['base_url'] = (string) $this->baseUrl;
} else {
$this->baseUrl = Url::fromString($config['base_url']);
}
}
@ -356,27 +354,42 @@ class Client implements ClientInterface
}
/**
* Merges default options into the array passed by reference and returns
* an array of headers that need to be merged in after the request is
* created.
* Merges default options into the array passed by reference.
*
* @param array $options Options to modify by reference
*
* @return array|null
* @return array
*/
private function mergeDefaults(&$options)
private function mergeDefaults($options)
{
// Merging optimization for when no headers are present
if (!isset($options['headers']) || !isset($this->defaults['headers'])) {
$options = array_replace_recursive($this->defaults, $options);
return null;
$defaults = $this->defaults;
// Case-insensitively merge in default headers if both defaults and
// options have headers specified.
if (!empty($defaults['headers']) && !empty($options['headers'])) {
// Create a set of lowercased keys that are present.
$lkeys = [];
foreach (array_keys($options['headers']) as $k) {
$lkeys[strtolower($k)] = true;
}
// Merge in lowercase default keys when not present in above set.
foreach ($defaults['headers'] as $key => $value) {
if (!isset($lkeys[strtolower($key)])) {
$options['headers'][$key] = $value;
}
}
// No longer need to merge in headers.
unset($defaults['headers']);
}
$defaults = $this->defaults;
unset($defaults['headers']);
$options = array_replace_recursive($defaults, $options);
$result = array_replace_recursive($defaults, $options);
foreach ($options as $k => $v) {
if ($v === null) {
unset($result[$k]);
}
}
return $this->defaults['headers'];
return $result;
}
/**
@ -385,6 +398,6 @@ class Client implements ClientInterface
*/
public function sendAll($requests, array $options = [])
{
(new Pool($this, $requests, $options))->wait();
Pool::send($this, $requests, $options);
}
}

View File

@ -11,7 +11,7 @@ use GuzzleHttp\Message\ResponseInterface;
*/
interface ClientInterface extends HasEmitterInterface
{
const VERSION = '5.0.3';
const VERSION = '5.2.0';
/**
* Create and return a new {@see RequestInterface} object.

View File

@ -41,6 +41,16 @@ abstract class AbstractRequestEvent extends AbstractEvent
return $this->transaction->request;
}
/**
* Get the number of transaction retries.
*
* @return int
*/
public function getRetryCount()
{
return $this->transaction->retries;
}
/**
* @return Transaction
*/

View File

@ -27,9 +27,9 @@ class AbstractRetryableEvent extends AbstractTransferEvent
*/
public function retry($afterDelay = 0)
{
$this->transaction->response = null;
$this->transaction->exception = null;
$this->transaction->state = 'before';
// Setting the transition state to 'retry' will cause the next state
// transition of the transaction to retry the request.
$this->transaction->state = 'retry';
if ($afterDelay) {
$this->transaction->request->getConfig()->set('delay', $afterDelay);

View File

@ -20,11 +20,13 @@ abstract class AbstractTransferEvent extends AbstractRequestEvent
*/
public function getTransferInfo($name = null)
{
return !$name
? $this->transaction->transferInfo
: (isset($this->transaction->transferInfo[$name])
? $this->transaction->transferInfo[$name]
: null);
if (!$name) {
return $this->transaction->transferInfo;
}
return isset($this->transaction->transferInfo[$name])
? $this->transaction->transferInfo[$name]
: null;
}
/**

View File

@ -0,0 +1,24 @@
<?php
namespace GuzzleHttp\Message;
/**
* Applies headers to a request.
*
* This interface can be used with Guzzle streams to apply body specific
* headers to a request during the PREPARE_REQUEST priority of the before event
*
* NOTE: a body that implements this interface will prevent a default
* content-type from being added to a request during the before event. If you
* want a default content-type to be added, then it will need to be done
* manually (e.g., using {@see GuzzleHttp\Mimetypes}).
*/
interface AppliesHeadersInterface
{
/**
* Apply headers to a request appropriate for the current state of the
* object.
*
* @param RequestInterface $request Request
*/
public function applyRequestHeaders(RequestInterface $request);
}

View File

@ -40,9 +40,10 @@ class MessageFactory implements MessageFactoryInterface
/** @var array Default allow_redirects request option settings */
private static $defaultRedirect = [
'max' => 5,
'strict' => false,
'referer' => false
'max' => 5,
'strict' => false,
'referer' => false,
'protocols' => ['http', 'https']
];
/**
@ -198,9 +199,8 @@ class MessageFactory implements MessageFactoryInterface
if ($value === true) {
$value = self::$defaultRedirect;
} elseif (!isset($value['max'])) {
throw new Iae('allow_redirects must be true, false, or an '
. 'array that contains the \'max\' key');
} elseif (!is_array($value)) {
throw new Iae('allow_redirects must be true, false, or array');
} else {
// Merge the default settings with the provided settings
$value += self::$defaultRedirect;
@ -227,12 +227,8 @@ class MessageFactory implements MessageFactoryInterface
if (!is_array($value)) {
throw new Iae('header value must be an array');
}
// Do not overwrite existing headers
foreach ($value as $k => $v) {
if (!$request->hasHeader($k)) {
$request->setHeader($k, $v);
}
$request->setHeader($k, $v);
}
break;

View File

@ -1,6 +1,7 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
@ -9,7 +10,9 @@ use GuzzleHttp\Ring\Future\FutureInterface;
use GuzzleHttp\Event\ListenerAttacherTrait;
use GuzzleHttp\Event\EndEvent;
use React\Promise\Deferred;
use React\Promise\FulfilledPromise;
use React\Promise\PromiseInterface;
use React\Promise\RejectedPromise;
/**
* Sends and iterator of requests concurrently using a capped pool size.
@ -50,9 +53,9 @@ class Pool implements FutureInterface
private $isRealized = false;
/**
* The option values for 'before', 'after', and 'error' can be a callable,
* an associative array containing event data, or an array of event data
* arrays. Event data arrays contain the following keys:
* The option values for 'before', 'complete', 'error' and 'end' can be a
* callable, an associative array containing event data, or an array of
* event data arrays. Event data arrays contain the following keys:
*
* - fn: callable to invoke that receives the event
* - priority: Optional event priority (defaults to 0)
@ -61,10 +64,14 @@ class Pool implements FutureInterface
* @param ClientInterface $client Client used to send the requests.
* @param array|\Iterator $requests Requests to send in parallel
* @param array $options Associative array of options
* - pool_size: (int) Maximum number of requests to send concurrently
* - pool_size: (callable|int) Maximum number of requests to send
* concurrently, or a callback that receives
* the current queue size and returns the
* number of new requests to send
* - before: (callable|array) Receives a BeforeEvent
* - after: (callable|array) Receives a CompleteEvent
* - complete: (callable|array) Receives a CompleteEvent
* - error: (callable|array) Receives a ErrorEvent
* - end: (callable|array) Receives an EndEvent
*/
public function __construct(
ClientInterface $client,
@ -140,7 +147,28 @@ class Pool implements FutureInterface
$requests,
array $options = []
) {
(new self($client, $requests, $options))->wait();
$pool = new self($client, $requests, $options);
$pool->wait();
}
private function getPoolSize()
{
return is_callable($this->poolSize)
? call_user_func($this->poolSize, count($this->waitQueue))
: $this->poolSize;
}
/**
* Add as many requests as possible up to the current pool limit.
*/
private function addNextRequests()
{
$limit = max($this->getPoolSize() - count($this->waitQueue), 0);
while ($limit--) {
if (!$this->addNextRequest()) {
break;
}
}
}
public function wait()
@ -150,11 +178,7 @@ class Pool implements FutureInterface
}
// Seed the pool with N number of requests.
for ($i = 0; $i < $this->poolSize; $i++) {
if (!$this->addNextRequest()) {
break;
}
}
$this->addNextRequests();
// Stop if the pool was cancelled while transferring requests.
if ($this->isRealized) {
@ -168,6 +192,7 @@ class Pool implements FutureInterface
} catch (\Exception $e) {
// Eat exceptions because they should be handled asynchronously
}
$this->addNextRequests();
}
// Clean up no longer needed state.
@ -241,6 +266,8 @@ class Pool implements FutureInterface
*/
private function addNextRequest()
{
add_next:
if ($this->isRealized || !$this->iter || !$this->iter->valid()) {
return false;
}
@ -258,23 +285,49 @@ class Pool implements FutureInterface
// Be sure to use "lazy" futures, meaning they do not send right away.
$request->getConfig()->set('future', 'lazy');
$this->attachListeners($request, $this->eventListeners);
$response = $this->client->send($request);
$hash = spl_object_hash($request);
$this->attachListeners($request, $this->eventListeners);
$request->getEmitter()->on('before', [$this, '_trackRetries'], RequestEvents::EARLY);
$response = $this->client->send($request);
$this->waitQueue[$hash] = $response;
$promise = $response->promise();
// Don't recursively call itself for completed or rejected responses.
if ($promise instanceof FulfilledPromise
|| $promise instanceof RejectedPromise
) {
try {
$this->finishResponse($request, $response->wait(), $hash);
} catch (\Exception $e) {
$this->finishResponse($request, $e, $hash);
}
goto add_next;
}
// Use this function for both resolution and rejection.
$fn = function ($value) use ($request, $hash) {
unset($this->waitQueue[$hash]);
$result = $value instanceof ResponseInterface
? ['request' => $request, 'response' => $value, 'error' => null]
: ['request' => $request, 'response' => null, 'error' => $value];
$this->deferred->progress($result);
$this->addNextRequest();
$thenFn = function ($value) use ($request, $hash) {
$this->finishResponse($request, $value, $hash);
if (!$request->getConfig()->get('_pool_retries')) {
$this->addNextRequests();
}
};
$response->then($fn, $fn);
$promise->then($thenFn, $thenFn);
return true;
}
public function _trackRetries(BeforeEvent $e)
{
$e->getRequest()->getConfig()->set('_pool_retries', $e->getRetryCount());
}
private function finishResponse($request, $value, $hash)
{
unset($this->waitQueue[$hash]);
$result = $value instanceof ResponseInterface
? ['request' => $request, 'response' => $value, 'error' => null]
: ['request' => $request, 'response' => null, 'error' => $value];
$this->deferred->progress($result);
}
}

View File

@ -1,22 +1,15 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\AppliesHeadersInterface;
use GuzzleHttp\Stream\StreamInterface;
/**
* Represents a POST body that is sent as either a multipart/form-data stream
* or application/x-www-urlencoded stream.
*/
interface PostBodyInterface extends StreamInterface, \Countable
interface PostBodyInterface extends StreamInterface, \Countable, AppliesHeadersInterface
{
/**
* Apply headers to the request appropriate for the current state of the object
*
* @param RequestInterface $request Request
*/
public function applyRequestHeaders(RequestInterface $request);
/**
* Set a specific field
*

View File

@ -20,30 +20,6 @@ class RequestFsm
private $mf;
private $maxTransitions;
private $states = [
// When a mock intercepts the emitted "before" event, then we
// transition to the "complete" intercept state.
'before' => [
'success' => 'send',
'intercept' => 'complete',
'error' => 'error'
],
// The complete and error events are handled using the "then" of
// the RingPHP request, so we exit the FSM.
'send' => ['error' => 'error'],
'complete' => [
'success' => 'end',
'intercept' => 'before',
'error' => 'error'
],
'error' => [
'success' => 'complete',
'intercept' => 'before',
'error' => 'end'
],
'end' => []
];
public function __construct(
callable $handler,
MessageFactoryInterface $messageFactory,
@ -59,156 +35,119 @@ class RequestFsm
* optionally supplied $finalState is entered.
*
* @param Transaction $trans Transaction being transitioned.
* @param string $finalState The state to stop on. If unspecified,
* runs until a terminal state is found.
*
* @throws \Exception if a terminal state throws an exception.
*/
public function __invoke(Transaction $trans, $finalState = null)
public function __invoke(Transaction $trans)
{
$trans->_transitionCount = 1;
$trans->_transitionCount = 0;
if (!$trans->state) {
$trans->state = 'before';
}
while ($trans->state !== $finalState) {
transition:
if (!isset($this->states[$trans->state])) {
throw new StateException("Invalid state: {$trans->state}");
} elseif (++$trans->_transitionCount > $this->maxTransitions) {
throw new StateException('Too many state transitions were '
. 'encountered ({$trans->_transitionCount}). This likely '
. 'means that a combination of event listeners are in an '
. 'infinite loop.');
}
if (++$trans->_transitionCount > $this->maxTransitions) {
throw new StateException("Too many state transitions were "
. "encountered ({$trans->_transitionCount}). This likely "
. "means that a combination of event listeners are in an "
. "infinite loop.");
}
$state = $this->states[$trans->state];
switch ($trans->state) {
case 'before': goto before;
case 'complete': goto complete;
case 'error': goto error;
case 'retry': goto retry;
case 'send': goto send;
case 'end': goto end;
default: throw new StateException("Invalid state: {$trans->state}");
}
before: {
try {
/** @var callable $fn */
$fn = [$this, $trans->state];
if ($fn($trans)) {
// Handles transitioning to the "intercept" state.
if (isset($state['intercept'])) {
$trans->state = $state['intercept'];
continue;
}
throw new StateException('Invalid intercept state '
. 'transition from ' . $trans->state);
$trans->request->getEmitter()->emit('before', new BeforeEvent($trans));
$trans->state = 'send';
if ((bool) $trans->response) {
$trans->state = 'complete';
}
if (isset($state['success'])) {
// Transition to the success state
$trans->state = $state['success'];
} else {
// Break: this is a terminal state with no transition.
break;
}
} catch (StateException $e) {
// State exceptions are thrown no matter what.
throw $e;
} catch (\Exception $e) {
$trans->state = 'error';
$trans->exception = $e;
// Terminal error states throw the exception.
if (!isset($state['error'])) {
throw $e;
}
goto transition;
}
complete: {
try {
if ($trans->response instanceof FutureInterface) {
// Futures will have their own end events emitted when
// dereferenced.
return;
}
// Transition to the error state.
$trans->state = $state['error'];
$trans->state = 'end';
$trans->response->setEffectiveUrl($trans->request->getUrl());
$trans->request->getEmitter()->emit('complete', new CompleteEvent($trans));
} catch (\Exception $e) {
$trans->state = 'error';
$trans->exception = $e;
}
goto transition;
}
}
private function before(Transaction $trans)
{
$trans->request->getEmitter()->emit('before', new BeforeEvent($trans));
// When a response is set during the before event (i.e., a mock), then
// we don't need to send anything. Skip ahead to the complete event
// by returning to to go to the intercept state.
return (bool) $trans->response;
}
private function send(Transaction $trans)
{
$fn = $this->handler;
$trans->response = FutureResponse::proxy(
$fn(RingBridge::prepareRingRequest($trans)),
function ($value) use ($trans) {
RingBridge::completeRingResponse($trans, $value, $this->mf, $this);
return $trans->response;
error: {
try {
// Convert non-request exception to a wrapped exception
$trans->exception = RequestException::wrapException(
$trans->request, $trans->exception
);
$trans->state = 'end';
$trans->request->getEmitter()->emit('error', new ErrorEvent($trans));
// An intercepted request (not retried) transitions to complete
if (!$trans->exception && $trans->state !== 'retry') {
$trans->state = 'complete';
}
} catch (\Exception $e) {
$trans->state = 'end';
$trans->exception = $e;
}
);
}
goto transition;
}
/**
* Emits the error event and ensures that the exception is set and is an
* instance of RequestException. If the error event is not intercepted,
* then the exception is thrown and we transition to the "end" event. This
* event also allows requests to be retried, and when retried, transitions
* to the "before" event. Otherwise, when no retries, and the exception is
* intercepted, transition to the "complete" event.
*/
private function error(Transaction $trans)
{
// Convert non-request exception to a wrapped exception
if (!($trans->exception instanceof RequestException)) {
$trans->exception = RequestException::wrapException(
$trans->request, $trans->exception
retry: {
$trans->retries++;
$trans->response = null;
$trans->exception = null;
$trans->state = 'before';
goto transition;
}
send: {
$fn = $this->handler;
$trans->response = FutureResponse::proxy(
$fn(RingBridge::prepareRingRequest($trans)),
function ($value) use ($trans) {
RingBridge::completeRingResponse($trans, $value, $this->mf, $this);
$this($trans);
return $trans->response;
}
);
}
// Dispatch an event and allow interception
$event = new ErrorEvent($trans);
$trans->request->getEmitter()->emit('error', $event);
if ($trans->exception) {
throw $trans->exception;
}
$trans->exception = null;
// Return true to transition to the 'before' state. False otherwise.
return $trans->state === 'before';
}
/**
* Emits a complete event, and if a request is marked for a retry during
* the complete event, then the "before" state is transitioned to.
*/
private function complete(Transaction $trans)
{
// Futures will have their own end events emitted when dereferenced.
if ($trans->response instanceof FutureInterface) {
return false;
}
$trans->response->setEffectiveUrl($trans->request->getUrl());
$trans->request->getEmitter()->emit('complete', new CompleteEvent($trans));
// Return true to transition to the 'before' state. False otherwise.
return $trans->state === 'before';
}
/**
* Emits the "end" event and throws an exception if one is present.
*/
private function end(Transaction $trans)
{
// Futures will have their own end events emitted when dereferenced,
// but still emit, even for futures, when an exception is present.
if (!$trans->exception && $trans->response instanceof FutureInterface) {
return;
}
$trans->request->getEmitter()->emit('end', new EndEvent($trans));
// Throw exceptions in the terminal event if the exception was not
// handled by an "end" event listener.
if ($trans->exception) {
throw $trans->exception;
end: {
$trans->request->getEmitter()->emit('end', new EndEvent($trans));
// Throw exceptions in the terminal event if the exception
// was not handled by an "end" event listener.
if ($trans->exception) {
if (!($trans->exception instanceof RequestException)) {
$trans->exception = RequestException::wrapException(
$trans->request, $trans->exception
);
}
throw $trans->exception;
}
}
}
}

View File

@ -72,19 +72,17 @@ class RingBridge
/**
* 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.
* handler. The created response is added to the transaction, and the
* transaction stat is set appropriately.
*
* @param Transaction $trans Owns request and response.
* @param array $response Ring response array
* @param MessageFactoryInterface $messageFactory Creates response objects.
* @param callable $fsm Request FSM function.
*/
public static function completeRingResponse(
Transaction $trans,
array $response,
MessageFactoryInterface $messageFactory,
callable $fsm
MessageFactoryInterface $messageFactory
) {
$trans->state = 'complete';
$trans->transferInfo = isset($response['transfer_stats'])
@ -116,9 +114,6 @@ class RingBridge
$trans->state = 'error';
$trans->exception = $response['error'];
}
// Complete the lifecycle of the request.
$fsm($trans);
}
/**
@ -163,7 +158,7 @@ class RingBridge
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 RingPHP handler. If you are simply trying to mock responses,
then it is recommneded to use the GuzzleHttp\Ring\Client\MockHandler.
then it is recommended to use the GuzzleHttp\Ring\Client\MockHandler.
EOT;
return new RequestException($message, $request);
}

View File

@ -4,9 +4,9 @@ namespace GuzzleHttp\Subscriber;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Message\AppliesHeadersInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Mimetypes;
use GuzzleHttp\Post\PostBodyInterface;
use GuzzleHttp\Stream\StreamInterface;
/**
@ -40,8 +40,8 @@ class Prepare implements SubscriberInterface
$this->addContentLength($request, $body);
if ($body instanceof PostBodyInterface) {
// Synchronize the POST body with the request's headers
if ($body instanceof AppliesHeadersInterface) {
// Synchronize the body with the request headers
$body->applyRequestHeaders($request);
} elseif (!$request->hasHeader('Content-Type')) {
$this->addContentType($request, $body);

View File

@ -4,6 +4,7 @@ namespace GuzzleHttp\Subscriber;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\CouldNotRewindStreamException;
use GuzzleHttp\Exception\TooManyRedirectsException;
use GuzzleHttp\Message\RequestInterface;
@ -25,6 +26,9 @@ use GuzzleHttp\Url;
* POST request with a GET request).
* - referer: Set to true to automatically add the "Referer" header when a
* redirect request is sent.
* - protocols: Array of allowed protocols. Defaults to 'http' and 'https'.
* When a redirect attempts to utilize a protocol that is not white listed,
* an exception is thrown.
*/
class Redirect implements SubscriberInterface
{
@ -99,6 +103,7 @@ class Redirect implements SubscriberInterface
ResponseInterface $response
) {
$config = $request->getConfig();
$protocols = $config->getPath('redirect/protocols') ?: ['http', 'https'];
// Use a GET request if this is an entity enclosing request and we are
// not forcing RFC compliance, but rather emulating what all browsers
@ -112,7 +117,7 @@ class Redirect implements SubscriberInterface
}
$previousUrl = $request->getUrl();
$this->setRedirectUrl($request, $response);
$this->setRedirectUrl($request, $response, $protocols);
$this->rewindEntityBody($request);
// Add the Referer header if it is told to do so and only
@ -134,10 +139,12 @@ class Redirect implements SubscriberInterface
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param array $protocols
*/
private function setRedirectUrl(
RequestInterface $request,
ResponseInterface $response
ResponseInterface $response,
array $protocols
) {
$location = $response->getHeader('Location');
$location = Url::fromString($location);
@ -151,6 +158,19 @@ class Redirect implements SubscriberInterface
$location = $originalUrl->combine($location);
}
// Ensure that the redirect URL is allowed based on the protocols.
if (!in_array($location->getScheme(), $protocols)) {
throw new BadResponseException(
sprintf(
'Redirect URL, %s, does not use one of the allowed redirect protocols: %s',
$location,
implode(', ', $protocols)
),
$request,
$response
);
}
$request->setUrl($location);
}
}

View File

@ -56,30 +56,48 @@ class Transaction
public $transferInfo = [];
/**
* The transaction's state.
* The number of transaction retries.
*
* @var int
*/
public $retries = 0;
/**
* The transaction's current state.
*
* @var string
*/
public $state;
/**
* The number of state transitions that this transactions has been through.
* Whether or not this is a future transaction. This value should not be
* changed after the future is constructed.
*
* @var bool
*/
public $future;
/**
* The number of state transitions that this transaction has been through.
*
* @var int
* @internal This is for internal use only. If you modify this, then you
* are asking for trouble.
*/
public $_transitionCount;
public $_transitionCount = 0;
/**
* @param ClientInterface $client Client that is used to send the requests
* @param RequestInterface $request Request to send
* @param bool $future Whether or not this is a future request.
*/
public function __construct(
ClientInterface $client,
RequestInterface $request
RequestInterface $request,
$future = false
) {
$this->client = $client;
$this->request = $request;
$this->_future = $future;
}
}

View File

@ -135,7 +135,7 @@ class Url
$password = null,
$port = null,
$path = null,
Query $query = null,
$query = null,
$fragment = null
) {
$this->scheme = $scheme;

View File

@ -7,10 +7,12 @@ use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Query;
use GuzzleHttp\Ring\Client\MockHandler;
use GuzzleHttp\Ring\Future\FutureArray;
use GuzzleHttp\Subscriber\History;
use GuzzleHttp\Subscriber\Mock;
use GuzzleHttp\Url;
use React\Promise\Deferred;
/**
@ -70,6 +72,14 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('http://foo.com/baz/', $client->getBaseUrl());
}
/**
* @expectedException \InvalidArgumentException
*/
public function testValidatesUriTemplateValue()
{
new Client(['base_url' => ['http://foo.com/']]);
}
/**
* @expectedException \Exception
* @expectedExceptionMessage Foo
@ -243,6 +253,15 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('custom', $request->getHeader('Foo'));
}
public function testCanOverrideDefaultOptionWithNull()
{
$client = new Client(['defaults' => ['proxy' => 'invalid!']]);
$request = $client->createRequest('GET', 'http://foo.com?a=b', [
'proxy' => null
]);
$this->assertFalse($request->getConfig()->hasKey('proxy'));
}
public function testDoesNotOverwriteExistingUA()
{
$client = new Client(['defaults' => [
@ -272,6 +291,15 @@ class ClientTest extends \PHPUnit_Framework_TestCase
);
}
public function testFalsyPathsAreCombinedWithBaseUrl()
{
$client = new Client(['base_url' => 'http://www.foo.com/baz?bam=bar']);
$this->assertEquals(
'http://www.foo.com/0',
$client->createRequest('GET', '0')->getUrl()
);
}
public function testUsesBaseUrlCombinedWithProvidedUrlViaUriTemplate()
{
$client = new Client(['base_url' => 'http://www.foo.com/baz?bam=bar']);
@ -582,4 +610,15 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$this->assertInstanceOf('GuzzleHttp\Message\FutureResponse', $res);
$this->assertEquals(200, $res->getStatusCode());
}
public function testCanUseUrlWithCustomQuery()
{
$client = new Client();
$url = Url::fromString('http://foo.com/bar');
$query = new Query(['baz' => '123%20']);
$query->setEncodingType(false);
$url->setQuery($query);
$r = $client->createRequest('GET', $url);
$this->assertEquals('http://foo.com/bar?baz=123%20', $r->getUrl());
}
}

View File

@ -19,7 +19,7 @@ class AbstractRetryableEventTest extends \PHPUnit_Framework_TestCase
->getMockForAbstractClass();
$e->retry();
$this->assertTrue($e->isPropagationStopped());
$this->assertEquals('before', $t->state);
$this->assertEquals('retry', $t->state);
}
public function testCanRetryAfterDelay()
@ -31,7 +31,7 @@ class AbstractRetryableEventTest extends \PHPUnit_Framework_TestCase
->getMockForAbstractClass();
$e->retry(10);
$this->assertTrue($e->isPropagationStopped());
$this->assertEquals('before', $t->state);
$this->assertEquals('retry', $t->state);
$this->assertEquals(10, $t->request->getConfig()->get('delay'));
}
}

View File

@ -46,4 +46,14 @@ class AbstractTransferEventTest extends \PHPUnit_Framework_TestCase
$this->assertSame($t->response, $e->getResponse());
$this->assertTrue($e->isPropagationStopped());
}
public function testReturnsNumberOfRetries()
{
$t = new Transaction(new Client(), new Request('GET', '/'));
$t->retries = 2;
$e = $this->getMockBuilder('GuzzleHttp\Event\AbstractTransferEvent')
->setConstructorArgs([$t])
->getMockForAbstractClass();
$this->assertEquals(2, $e->getRetryCount());
}
}

View File

@ -3,7 +3,10 @@ namespace GuzzleHttp\Tests;
use GuzzleHttp\Client;
use GuzzleHttp\Event\AbstractTransferEvent;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\EndEvent;
use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Pool;
@ -70,4 +73,51 @@ class IntegrationTest extends \PHPUnit_Framework_TestCase
$this->assertNotEmpty($transfer);
$this->assertArrayHasKey('url', $transfer);
}
public function testNestedFutureResponsesAreResolvedWhenSending()
{
$c = new Client();
$total = 3;
Server::enqueue([
new Response(200),
new Response(201),
new Response(202)
]);
$c->getEmitter()->on(
'complete',
function (CompleteEvent $e) use (&$total) {
if (--$total) {
$e->retry();
}
}
);
$response = $c->get(Server::$url);
$this->assertEquals(202, $response->getStatusCode());
$this->assertEquals('GuzzleHttp\Message\Response', get_class($response));
}
public function testNestedFutureErrorsAreResolvedWhenSending()
{
$c = new Client();
$total = 3;
Server::enqueue([
new Response(500),
new Response(501),
new Response(502)
]);
$c->getEmitter()->on(
'error',
function (ErrorEvent $e) use (&$total) {
if (--$total) {
$e->retry();
}
}
);
try {
$c->get(Server::$url);
$this->fail('Did not throw!');
} catch (RequestException $e) {
$this->assertEquals(502, $e->getResponse()->getStatusCode());
}
}
}

View File

@ -144,7 +144,7 @@ class MessageFactoryTest extends \PHPUnit_Framework_TestCase
*/
public function testValidatesRedirects()
{
(new MessageFactory())->createRequest('GET', '/', ['allow_redirects' => []]);
(new MessageFactory())->createRequest('GET', '/', ['allow_redirects' => 'foo']);
}
public function testCanEnableStrictRedirectsAndSpecifyMax()

View File

@ -154,6 +154,64 @@ class PoolTest extends \PHPUnit_Framework_TestCase
$this->assertSame($responses[3], $result[2]->getResponse());
}
public function testBatchesRequestsWithDynamicPoolSize()
{
$client = new Client(['handler' => function () {
throw new \RuntimeException('No network access');
}]);
$responses = [
new Response(301, ['Location' => 'http://foo.com/bar']),
new Response(200),
new Response(200),
new Response(404)
];
$client->getEmitter()->attach(new Mock($responses));
$requests = [
$client->createRequest('GET', 'http://foo.com/baz'),
$client->createRequest('HEAD', 'http://httpbin.org/get'),
$client->createRequest('PUT', 'http://httpbin.org/put'),
];
$a = $b = $c = $d = 0;
$result = Pool::batch($client, $requests, [
'before' => function (BeforeEvent $e) use (&$a) { $a++; },
'complete' => function (CompleteEvent $e) use (&$b) { $b++; },
'error' => function (ErrorEvent $e) use (&$c) { $c++; },
'end' => function (EndEvent $e) use (&$d) { $d++; },
'pool_size' => function ($queueSize) {
static $options = [1, 2, 1];
static $queued = 0;
$this->assertEquals(
$queued,
$queueSize,
'The number of queued requests should be equal to the sum of pool sizes so far.'
);
$next = array_shift($options);
$queued += $next;
return $next;
}
]);
$this->assertEquals(4, $a);
$this->assertEquals(2, $b);
$this->assertEquals(1, $c);
$this->assertEquals(3, $d);
$this->assertCount(3, $result);
$this->assertInstanceOf('GuzzleHttp\BatchResults', $result);
// The first result is actually the second (redirect) response.
$this->assertSame($responses[1], $result[0]);
// The second result is a 1:1 request:response map
$this->assertSame($responses[2], $result[1]);
// The third entry is the 404 RequestException
$this->assertSame($responses[3], $result[2]->getResponse());
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Each event listener must be a callable or
@ -228,4 +286,34 @@ class PoolTest extends \PHPUnit_Framework_TestCase
Pool::send($client, $requests);
$this->assertCount(1, $history);
}
public function testDoesNotInfinitelyRecurse()
{
$client = new Client(['handler' => function () {
throw new \RuntimeException('No network access');
}]);
$last = null;
$client->getEmitter()->on(
'before',
function (BeforeEvent $e) use (&$last) {
$e->intercept(new Response(200));
if (function_exists('xdebug_get_stack_depth')) {
if ($last) {
$this->assertEquals($last, xdebug_get_stack_depth());
} else {
$last = xdebug_get_stack_depth();
}
}
}
);
$requests = [];
for ($i = 0; $i < 100; $i++) {
$requests[] = $client->createRequest('GET', 'http://foo.com');
}
$pool = new Pool($client, $requests);
$pool->wait();
}
}

View File

@ -130,7 +130,6 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$b->seek(0);
$this->assertEquals('foo=bar&baz=123', $b->read(1000));
$this->assertEquals(15, $b->tell());
$this->assertTrue($b->eof());
}
public function testCanSpecifyQueryAggregator()

View File

@ -5,6 +5,7 @@ use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\Response;
use GuzzleHttp\RequestFsm;
use GuzzleHttp\Ring\Future\CompletedFutureArray;
use GuzzleHttp\Subscriber\Mock;
use GuzzleHttp\Transaction;
use GuzzleHttp\Client;
@ -29,19 +30,23 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
public function testEmitsBeforeEventInTransition()
{
$fsm = new RequestFsm(function () {}, $this->mf);
$fsm = new RequestFsm(function () {
return new CompletedFutureArray(['status' => 200]);
}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$c = false;
$t->request->getEmitter()->on('before', function (BeforeEvent $e) use (&$c) {
$c = true;
});
$fsm($t, 'send');
$fsm($t);
$this->assertTrue($c);
}
public function testEmitsCompleteEventInTransition()
{
$fsm = new RequestFsm(function () {}, $this->mf);
$fsm = new RequestFsm(function () {
return new CompletedFutureArray(['status' => 200]);
}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$t->response = new Response(200);
$t->state = 'complete';
@ -49,13 +54,15 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('complete', function (CompleteEvent $e) use (&$c) {
$c = true;
});
$fsm($t, 'end');
$fsm($t);
$this->assertTrue($c);
}
public function testDoesNotEmitCompleteForFuture()
{
$fsm = new RequestFsm(function () {}, $this->mf);
$fsm = new RequestFsm(function () {
return new CompletedFutureArray(['status' => 200]);
}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$deferred = new Deferred();
$t->response = new FutureResponse($deferred->promise());
@ -64,21 +71,6 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('complete', function (CompleteEvent $e) use (&$c) {
$c = true;
});
$fsm($t, 'end');
$this->assertFalse($c);
}
public function testDoesNotEmitEndForFuture()
{
$fsm = new RequestFsm(function () {}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$deferred = new Deferred();
$t->response = new FutureResponse($deferred->promise());
$t->state = 'end';
$c = false;
$t->request->getEmitter()->on('end', function (EndEvent $e) use (&$c) {
$c = true;
});
$fsm($t);
$this->assertFalse($c);
}
@ -95,7 +87,9 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
public function testTransitionsThroughErrorsInBefore()
{
$fsm = new RequestFsm(function () {}, $this->mf);
$fsm = new RequestFsm(function () {
return new CompletedFutureArray(['status' => 200]);
}, $this->mf);
$client = new Client();
$request = $client->createRequest('GET', 'http://ewfewwef.com');
$t = new Transaction($client, $request);
@ -105,7 +99,7 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
throw new \Exception('foo');
});
try {
$fsm($t, 'send');
$fsm($t);
$this->fail('did not throw');
} catch (RequestException $e) {
$this->assertContains('foo', $t->exception->getMessage());
@ -133,7 +127,9 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
public function testTransitionsThroughErrorInterception()
{
$fsm = new RequestFsm(function () {}, $this->mf);
$fsm = new RequestFsm(function () {
return new CompletedFutureArray(['status' => 404]);
}, $this->mf);
$client = new Client();
$request = $client->createRequest('GET', 'http://ewfewwef.com');
$t = new Transaction($client, $request);
@ -142,9 +138,6 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('error', function (ErrorEvent $e) {
$e->intercept(new Response(200));
});
$fsm($t, 'send');
$t->response = new Response(404);
$t->state = 'complete';
$fsm($t);
$this->assertEquals(200, $t->response->getStatusCode());
$this->assertNull($t->exception);
@ -175,10 +168,12 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
{
$client = new Client([
'fsm' => $fsm = new RequestFsm(
function () {},
new MessageFactory(),
3
)
function () {
return new CompletedFutureArray(['status' => 200]);
},
new MessageFactory(),
3
)
]);
$request = $client->createRequest('GET', 'http://foo.com:123');
$request->getEmitter()->on('before', function () {

View File

@ -107,24 +107,6 @@ class RingBridgeTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', (string) $response->getBody());
}
public function testEmitsCompleteEventOnSuccess()
{
$c = false;
$trans = new Transaction(new Client(), new Request('GET', 'http://f.co'));
$trans->request->getEmitter()->on('complete', function () use (&$c) {
$c = true;
});
$f = new MessageFactory();
$res = ['status' => 200, 'headers' => []];
$fsm = new RequestFsm(function () {}, new MessageFactory());
RingBridge::completeRingResponse($trans, $res, $f, $fsm);
$this->assertInstanceOf(
'GuzzleHttp\Message\ResponseInterface',
$trans->response
);
$this->assertTrue($c);
}
public function testEmitsErrorEventOnError()
{
$client = new Client(['base_url' => 'http://127.0.0.1:123']);

View File

@ -266,4 +266,23 @@ class RedirectTest extends \PHPUnit_Framework_TestCase
$response->getEffectiveUrl()
);
}
/**
* @expectedException \GuzzleHttp\Exception\BadResponseException
* @expectedExceptionMessage Redirect URL, https://foo.com/redirect2, does not use one of the allowed redirect protocols: http
*/
public function testThrowsWhenRedirectingToInvalidUrlProtocol()
{
$mock = new Mock([
"HTTP/1.1 301 Moved Permanently\r\nLocation: /redirect1\r\nContent-Length: 0\r\n\r\n",
"HTTP/1.1 301 Moved Permanently\r\nLocation: https://foo.com/redirect2\r\nContent-Length: 0\r\n\r\n"
]);
$client = new Client();
$client->getEmitter()->attach($mock);
$client->get('http://www.example.com/foo', [
'allow_redirects' => [
'protocols' => ['http']
]
]);
}
}

View File

@ -4,6 +4,7 @@ php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
before_script:

View File

@ -1,5 +1,27 @@
# CHANGELOG
## 1.0.7 - 2015-03-29
* PHP 7 fixes.
## 1.0.6 - 2015-02-26
* Bug fix: futures now extend from React's PromiseInterface to ensure that they
are properly forwarded down the promise chain.
* The multi handle of the CurlMultiHandler is now created lazily.
## 1.0.5 - 2014-12-10
* Adding more error information to PHP stream wrapper exceptions.
* Added digest auth integration test support to test server.
## 1.0.4 - 2014-12-01
* Added support for older versions of cURL that do not have CURLOPT_TIMEOUT_MS.
* Setting debug to `false` does not enable debug output.
* Added a fix to the StreamHandler to return a `FutureArrayInterface` when an
error occurs.
## 1.0.3 - 2014-11-03
* Setting the `header` stream option as a string to be compatible with GAE.

View File

@ -1,5 +1,6 @@
{
"name": "guzzlehttp/ringphp",
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"license": "MIT",
"authors": [
{

View File

@ -15,11 +15,11 @@ without tying your application to a specific implementation.
.. toctree::
:maxdepth: 2
spec
futures
client_middleware
client_handlers
testing
spec
futures
client_middleware
client_handlers
testing
.. code-block:: php

View File

@ -406,12 +406,20 @@ class CurlFactory
case 'timeout':
$options[CURLOPT_TIMEOUT_MS] = $value * 1000;
if (defined('CURLOPT_TIMEOUT_MS')) {
$options[CURLOPT_TIMEOUT_MS] = $value * 1000;
} else {
$options[CURLOPT_TIMEOUT] = $value;
}
break;
case 'connect_timeout':
$options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000;
if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
$options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000;
} else {
$options[CURLOPT_CONNECTTIMEOUT] = $value;
}
break;
case 'proxy':

View File

@ -71,6 +71,7 @@ class CurlHandler
$response = ['transfer_stats' => curl_getinfo($h)];
$response['curl']['error'] = curl_error($h);
$response['curl']['errno'] = curl_errno($h);
$response['transfer_stats'] = array_merge($response['transfer_stats'], $response['curl']);
$this->releaseEasyHandle($h);
return new CompletedFutureArray(

View File

@ -13,13 +13,14 @@ use React\Promise\Deferred;
* When using the CurlMultiHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the "client" key of the request.
*
* @property resource $_mh Internal use only. Lazy loaded multi-handle.
*/
class CurlMultiHandler
{
/** @var callable */
private $factory;
private $selectTimeout;
private $mh;
private $active;
private $handles = [];
private $delays = [];
@ -42,8 +43,9 @@ class CurlMultiHandler
*/
public function __construct(array $options = [])
{
$this->mh = isset($options['mh'])
? $options['mh'] : curl_multi_init();
if (isset($options['mh'])) {
$this->_mh = $options['mh'];
}
$this->factory = isset($options['handle_factory'])
? $options['handle_factory'] : new CurlFactory();
$this->selectTimeout = isset($options['select_timeout'])
@ -52,6 +54,15 @@ class CurlMultiHandler
? $options['max_handles'] : 100;
}
public function __get($name)
{
if ($name === '_mh') {
return $this->_mh = curl_multi_init();
}
throw new \BadMethodCallException();
}
public function __destruct()
{
// Finish any open connections before terminating the script.
@ -59,9 +70,9 @@ class CurlMultiHandler
$this->execute();
}
if ($this->mh) {
curl_multi_close($this->mh);
$this->mh = null;
if (isset($this->_mh)) {
curl_multi_close($this->_mh);
unset($this->_mh);
}
}
@ -106,7 +117,7 @@ class CurlMultiHandler
do {
if ($this->active &&
curl_multi_select($this->mh, $this->selectTimeout) === -1
curl_multi_select($this->_mh, $this->selectTimeout) === -1
) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
@ -119,7 +130,7 @@ class CurlMultiHandler
}
do {
$mrc = curl_multi_exec($this->mh, $this->active);
$mrc = curl_multi_exec($this->_mh, $this->active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
@ -142,13 +153,13 @@ class CurlMultiHandler
if (isset($entry['request']['client']['delay'])) {
$this->delays[$id] = microtime(true) + ($entry['request']['client']['delay'] / 1000);
} elseif (empty($entry['request']['future'])) {
curl_multi_add_handle($this->mh, $entry['handle']);
curl_multi_add_handle($this->_mh, $entry['handle']);
} else {
curl_multi_add_handle($this->mh, $entry['handle']);
curl_multi_add_handle($this->_mh, $entry['handle']);
// "lazy" futures are only sent once the pool has many requests.
if ($entry['request']['future'] !== 'lazy') {
do {
$mrc = curl_multi_exec($this->mh, $this->active);
$mrc = curl_multi_exec($this->_mh, $this->active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
}
@ -159,7 +170,7 @@ class CurlMultiHandler
{
if (isset($this->handles[$id])) {
curl_multi_remove_handle(
$this->mh,
$this->_mh,
$this->handles[$id]['handle']
);
curl_close($this->handles[$id]['handle']);
@ -183,7 +194,7 @@ class CurlMultiHandler
$handle = $this->handles[$id]['handle'];
unset($this->delays[$id], $this->handles[$id]);
curl_multi_remove_handle($this->mh, $handle);
curl_multi_remove_handle($this->_mh, $handle);
curl_close($handle);
return true;
@ -197,7 +208,7 @@ class CurlMultiHandler
if ($currentTime >= $delay) {
unset($this->delays[$id]);
curl_multi_add_handle(
$this->mh,
$this->_mh,
$this->handles[$id]['handle']
);
}
@ -206,7 +217,7 @@ class CurlMultiHandler
private function processMessages()
{
while ($done = curl_multi_info_read($this->mh)) {
while ($done = curl_multi_info_read($this->_mh)) {
$id = (int) $done['handle'];
if (!isset($this->handles[$id])) {

View File

@ -16,6 +16,7 @@ use GuzzleHttp\Stream\Utils;
class StreamHandler
{
private $options;
private $lastHeaders;
public function __construct(array $options = [])
{
@ -30,15 +31,17 @@ class StreamHandler
try {
// Does not support the expect header.
$request = Core::removeHeader($request, 'Expect');
$stream = $this->createStream($url, $request, $headers);
return $this->createResponse($request, $url, $headers, $stream);
$stream = $this->createStream($url, $request);
return $this->createResponse($request, $url, $stream);
} catch (RingException $e) {
return $this->createErrorResponse($url, $e);
}
}
private function createResponse(array $request, $url, array $hdrs, $stream)
private function createResponse(array $request, $url, $stream)
{
$hdrs = $this->lastHeaders;
$this->lastHeaders = null;
$parts = explode(' ', array_shift($hdrs), 3);
$response = [
'status' => $parts[1],
@ -131,13 +134,13 @@ class StreamHandler
$e = new ConnectException($e->getMessage(), 0, $e);
}
return [
return new CompletedFutureArray([
'status' => null,
'body' => null,
'headers' => [],
'effective_url' => $url,
'error' => $e
];
]);
}
/**
@ -150,17 +153,25 @@ class StreamHandler
*/
private function createResource(callable $callback)
{
// Turn off error reporting while we try to initiate the request
$level = error_reporting(0);
$resource = call_user_func($callback);
error_reporting($level);
$errors = null;
set_error_handler(function ($_, $msg, $file, $line) use (&$errors) {
$errors[] = [
'message' => $msg,
'file' => $file,
'line' => $line
];
return true;
});
// If the resource could not be created, then grab the last error and
// throw an exception.
if (!is_resource($resource)) {
$resource = $callback();
restore_error_handler();
if (!$resource) {
$message = 'Error creating resource: ';
foreach ((array) error_get_last() as $key => $value) {
$message .= "[{$key}] {$value} ";
foreach ($errors as $err) {
foreach ($err as $key => $value) {
$message .= "[$key] $value" . PHP_EOL;
}
}
throw new RingException(trim($message));
}
@ -168,11 +179,8 @@ class StreamHandler
return $resource;
}
private function createStream(
$url,
array $request,
&$http_response_header
) {
private function createStream($url, array $request)
{
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
@ -207,8 +215,7 @@ class StreamHandler
$url,
$request,
$options,
$this->createContext($request, $options, $params),
$http_response_header
$this->createContext($request, $options, $params)
);
}
@ -302,7 +309,7 @@ class StreamHandler
private function add_progress(array $request, &$options, $value, &$params)
{
$fn = function ($code, $_, $_, $_, $transferred, $total) use ($value) {
$fn = function ($code, $_1, $_2, $_3, $transferred, $total) use ($value) {
if ($code == STREAM_NOTIFY_PROGRESS) {
$value($total, $transferred, null, null);
}
@ -316,6 +323,10 @@ class StreamHandler
private function add_debug(array $request, &$options, $value, &$params)
{
if ($value === false) {
return;
}
static $map = [
STREAM_NOTIFY_CONNECT => 'CONNECT',
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
@ -382,16 +393,17 @@ class StreamHandler
$url,
array $request,
array $options,
$context,
&$http_response_header
$context
) {
return $this->createResource(
function () use ($url, &$http_response_header, $context) {
function () use ($url, $context) {
if (false === strpos($url, 'http')) {
trigger_error("URL is invalid: {$url}", E_USER_WARNING);
return null;
}
return fopen($url, 'r', null, $context);
$resource = fopen($url, 'r', null, $context);
$this->lastHeaders = $http_response_header;
return $resource;
},
$request,
$options

View File

@ -16,7 +16,7 @@ use React\Promise\PromisorInterface;
* computation has not yet completed when wait() is called, the call to wait()
* will block until the future has completed.
*/
interface FutureInterface extends PromisorInterface
interface FutureInterface extends PromiseInterface, PromisorInterface
{
/**
* Returns the result of the future either from cache or by blocking until
@ -37,20 +37,4 @@ interface FutureInterface extends PromisorInterface
* Cancels the future, if possible.
*/
public function cancel();
/**
* Create and return a promise that invokes the given methods when the
* future has a value, exception, or progress events.
*
* @param callable $onFulfilled Called when the promise is resolved.
* @param callable $onRejected Called when the promise is rejected.
* @param callable $onProgress Called on progress events.
*
* @return PromiseInterface
*/
public function then(
callable $onFulfilled = null,
callable $onRejected = null,
callable $onProgress = null
);
}

View File

@ -41,7 +41,10 @@ class StreamHandlerTest extends \PHPUnit_Framework_TestCase
'headers' => ['host' => ['localhost:123']],
'client' => ['timeout' => 0.01],
]);
$this->assertInstanceOf(
'GuzzleHttp\Ring\Future\CompletedFutureArray',
$result
);
$this->assertNull($result['status']);
$this->assertNull($result['body']);
$this->assertEquals([], $result['headers']);

View File

@ -21,6 +21,15 @@
* <
* < [{'http_method': 'GET', 'uri': '/', 'headers': {}, 'body': 'string'}]
*
* - Attempt access to the secure area
* > GET /secure/by-digest/qop-auth/guzzle-server/requests
* > Host: 127.0.0.1:8125
*
* < HTTP/1.1 401 Unauthorized
* < WWW-Authenticate: Digest realm="Digest Test", qop="auth", nonce="0796e98e1aeef43141fab2a66bf4521a", algorithm="MD5", stale="false"
* <
* < 401 Unauthorized
*
* - Shutdown the server
* > DELETE /guzzle-server
* > Host: 127.0.0.1:8125
@ -44,6 +53,77 @@ var GuzzleServer = function(port, log) {
this.requests = [];
var that = this;
var md5 = function(input) {
var crypto = require('crypto');
var hasher = crypto.createHash('md5');
hasher.update(input);
return hasher.digest('hex');
}
/**
* Node.js HTTP server authentication module.
*
* It is only initialized on demand (by loadAuthentifier). This avoids
* requiring the dependency to http-auth on standard operations, and the
* performance hit at startup.
*/
var auth;
/**
* Provides authentication handlers (Basic, Digest).
*/
var loadAuthentifier = function(type, options) {
var typeId = type;
if (type == 'digest') {
typeId += '.'+(options && options.qop ? options.qop : 'none');
}
if (!loadAuthentifier[typeId]) {
if (!auth) {
try {
auth = require('http-auth');
} catch (e) {
if (e.code == 'MODULE_NOT_FOUND') {
return;
}
}
}
switch (type) {
case 'digest':
var digestParams = {
realm: 'Digest Test',
login: 'me',
password: 'test'
};
if (options && options.qop) {
digestParams.qop = options.qop;
}
loadAuthentifier[typeId] = auth.digest(digestParams, function(username, callback) {
callback(md5(digestParams.login + ':' + digestParams.realm + ':' + digestParams.password));
});
break
}
}
return loadAuthentifier[typeId];
};
var firewallRequest = function(request, req, res, requestHandlerCallback) {
var securedAreaUriParts = request.uri.match(/^\/secure\/by-(digest)(\/qop-([^\/]*))?(\/.*)$/);
if (securedAreaUriParts) {
var authentifier = loadAuthentifier(securedAreaUriParts[1], { qop: securedAreaUriParts[2] });
if (!authentifier) {
res.writeHead(501, 'HTTP authentication not implemented', { 'Content-Length': 0 });
res.end();
return;
}
authentifier.check(req, res, function(req, res) {
req.url = securedAreaUriParts[4];
requestHandlerCallback(request, req, res);
});
} else {
requestHandlerCallback(request, req, res);
}
};
var controlRequest = function(request, req, res) {
if (req.url == '/guzzle-server/perf') {
res.writeHead(200, 'OK', {'Content-Length': 16});
@ -140,7 +220,7 @@ var GuzzleServer = function(port, log) {
// Called when the request completes
req.addListener('end', function() {
receivedRequest(request, req, res);
firewallRequest(request, req, res, receivedRequest);
});
});