updated mailgun librarie

This commit is contained in:
2019-05-14 10:47:30 +02:00
parent c97e0f8ba1
commit dc39ddbbea
1588 changed files with 40009 additions and 143222 deletions

View File

@@ -0,0 +1,185 @@
# Change Log
## 1.9.1 - 2019-02-02
### Added
- Updated type hints in doc blocks.
## 1.9.0 - 2019-01-03
### Added
- Support for PSR-18 clients
- Added traits `VersionBridgePlugin` and `VersionBridgeClient` to help plugins and clients to support both
1.x and 2.x version of `php-http/client-common` and `php-http/httplug`.
### Changed
- [RetryPlugin] Renamed the configuration options for the exception retry callback from `decider` to `exception_decider`
and `delay` to `exception_delay`. The old names still work but are deprecated.
## 1.8.2 - 2018-12-14
### Changed
- When multiple cookies exist, a single header with all cookies is sent as per RFC 6265 Section 5.4
- AddPathPlugin will now trim of ending slashes in paths
## 1.8.1 - 2018-10-09
### Fixed
- Reverted change to RetryPlugin so it again waits when retrying to avoid "can only throw objects" error.
## 1.8.0 - 2018-09-21
### Added
- Add an option on ErrorPlugin to only throw exception on response with 5XX status code.
### Changed
- AddPathPlugin no longer add prefix multiple times if a request is restarted - it now only adds the prefix if that request chain has not yet passed through the AddPathPlugin
- RetryPlugin no longer wait for retried requests and use a deferred promise instead
### Fixed
- Decoder plugin will now remove header when there is no more encoding, instead of setting to an empty array
## 1.7.0 - 2017-11-30
### Added
- Symfony 4 support
### Changed
- Strict comparison in DecoderPlugin
## 1.6.0 - 2017-10-16
### Added
- Add HttpClientPool client to leverage load balancing and fallback mechanism [see the documentation](http://docs.php-http.org/en/latest/components/client-common.html) for more details.
- `PluginClientFactory` to create `PluginClient` instances.
- Added new option 'delay' for `RetryPlugin`.
- Added new option 'decider' for `RetryPlugin`.
- Supports more cookie date formats in the Cookie Plugin
### Changed
- The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like:
```php
$plugin = new RetryPlugin(['delay' => function(RequestInterface $request, Exception $e, $retries) {
return 0;
});
```
### Deprecated
- The `debug_plugins` option for `PluginClient` is deprecated and will be removed in 2.0. Use the decorator design pattern instead like in [ProfilePlugin](https://github.com/php-http/HttplugBundle/blob/de33f9c14252f22093a5ec7d84f17535ab31a384/Collector/ProfilePlugin.php).
## 1.5.0 - 2017-03-30
### Added
- `QueryDefaultsPlugin` to add default query parameters.
## 1.4.2 - 2017-03-18
### Deprecated
- `DecoderPlugin` does not longer claim to support `compress` content encoding
### Fixed
- `CookiePlugin` allows main domain cookies to be sent/stored for subdomains
- `DecoderPlugin` uses the right `FilteredStream` to handle `deflate` content encoding
## 1.4.1 - 2017-02-20
### Fixed
- Cast return value of `StreamInterface::getSize` to string in `ContentLengthPlugin`
## 1.4.0 - 2016-11-04
### Added
- Add Path plugin
- Base URI plugin that combines Add Host and Add Path plugins
## 1.3.0 - 2016-10-16
### Changed
- Fix Emulated Trait to use Http based promise which respect the HttpAsyncClient interface
- Require Httplug 1.1 where we use HTTP specific promises.
- RedirectPlugin: use the full URL instead of the URI to properly keep track of redirects
- Add AddPathPlugin for API URLs with base path
- Add BaseUriPlugin that combines AddHostPlugin and AddPathPlugin
## 1.2.1 - 2016-07-26
### Changed
- AddHostPlugin also sets the port if specified
## 1.2.0 - 2016-07-14
### Added
- Suggest separate plugins in composer.json
- Introduced `debug_plugins` option for `PluginClient`
## 1.1.0 - 2016-05-04
### Added
- Add a flexible http client providing both contract, and only emulating what's necessary
- HTTP Client Router: route requests to underlying clients
- Plugin client and core plugins moved here from `php-http/plugins`
### Deprecated
- Extending client classes, they will be made final in version 2.0
## 1.0.0 - 2016-01-27
### Changed
- Remove useless interface in BatchException
## 0.2.0 - 2016-01-12
### Changed
- Updated package files
- Updated HTTPlug to RC1
## 0.1.1 - 2015-12-26
### Added
- Emulated clients
## 0.1.0 - 2015-12-25
### Added
- Batch client from utils
- Methods client from utils
- Emulators and decorators from client-tools

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,55 @@
# HTTP Client Common
[![Latest Version](https://img.shields.io/github/release/php-http/client-common.svg?style=flat-square)](https://github.com/php-http/client-common/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/travis/php-http/client-common.svg?style=flat-square)](https://travis-ci.org/php-http/client-common)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/client-common.svg?style=flat-square)](https://packagist.org/packages/php-http/client-common)
**Common HTTP Client implementations and tools for HTTPlug.**
## Install
Via Composer
``` bash
$ composer require php-http/client-common
```
## Usage
This package provides common tools for HTTP Clients:
- BatchClient to handle sending requests in parallel
- A convenience client with HTTP method names as class methods
- Emulator, decorator layers for sync/async clients
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/components/client-common.html).
## Testing
``` bash
$ composer test
```
## Contributing
Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).
## Security
If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,43 @@
{
"name": "php-http/client-common",
"description": "Common HTTP Client implementations and tools for HTTPlug",
"license": "MIT",
"keywords": ["http", "client", "httplug", "common"],
"homepage": "http://httplug.io",
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": "^5.4 || ^7.0",
"php-http/httplug": "^1.1",
"php-http/message-factory": "^1.0",
"php-http/message": "^1.6",
"symfony/options-resolver": "^2.6 || ^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5 || ^3.4 || ^4.2",
"guzzlehttp/psr7": "^1.4"
},
"suggest": {
"php-http/logger-plugin": "PSR-3 Logger plugin",
"php-http/cache-plugin": "PSR-6 Cache plugin",
"php-http/stopwatch-plugin": "Symfony Stopwatch plugin"
},
"autoload": {
"psr-4": {
"Http\\Client\\Common\\": "src/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
},
"extra": {
"branch-alias": {
"dev-master": "1.9.x-dev"
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Http\Client\Common;
use Http\Client\Exception;
use Http\Client\HttpClient;
use Http\Client\Common\Exception\BatchException;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
/**
* BatchClient allow to sends multiple request and retrieve a Batch Result.
*
* This implementation simply loops over the requests and uses sendRequest with each of them.
*
* @author Joel Wurtz <jwurtz@jolicode.com>
*/
class BatchClient implements HttpClient
{
/**
* @var HttpClient|ClientInterface
*/
private $client;
/**
* @param HttpClient|ClientInterface $client
*/
public function __construct($client)
{
if (!($client instanceof HttpClient) && !($client instanceof ClientInterface)) {
throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface');
}
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
return $this->client->sendRequest($request);
}
/**
* Send several requests.
*
* You may not assume that the requests are executed in a particular order. If the order matters
* for your application, use sendRequest sequentially.
*
* @param RequestInterface[] The requests to send
*
* @return BatchResult Containing one result per request
*
* @throws BatchException If one or more requests fails. The exception gives access to the
* BatchResult with a map of request to result for success, request to
* exception for failures
*/
public function sendRequests(array $requests)
{
$batchResult = new BatchResult();
foreach ($requests as $request) {
try {
$response = $this->sendRequest($request);
$batchResult = $batchResult->addResponse($request, $response);
} catch (Exception $e) {
$batchResult = $batchResult->addException($request, $e);
}
}
if ($batchResult->hasExceptions()) {
throw new BatchException($batchResult);
}
return $batchResult;
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Http\Client\Common;
use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Responses and exceptions returned from parallel request execution.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class BatchResult
{
/**
* @var \SplObjectStorage
*/
private $responses;
/**
* @var \SplObjectStorage
*/
private $exceptions;
public function __construct()
{
$this->responses = new \SplObjectStorage();
$this->exceptions = new \SplObjectStorage();
}
/**
* Checks if there are any successful responses at all.
*
* @return bool
*/
public function hasResponses()
{
return $this->responses->count() > 0;
}
/**
* Returns all successful responses.
*
* @return ResponseInterface[]
*/
public function getResponses()
{
$responses = [];
foreach ($this->responses as $request) {
$responses[] = $this->responses[$request];
}
return $responses;
}
/**
* Checks if there is a successful response for a request.
*
* @param RequestInterface $request
*
* @return bool
*/
public function isSuccessful(RequestInterface $request)
{
return $this->responses->contains($request);
}
/**
* Returns the response for a successful request.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws \UnexpectedValueException If request was not part of the batch or failed
*/
public function getResponseFor(RequestInterface $request)
{
try {
return $this->responses[$request];
} catch (\UnexpectedValueException $e) {
throw new \UnexpectedValueException('Request not found', $e->getCode(), $e);
}
}
/**
* Adds a response in an immutable way.
*
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return BatchResult the new BatchResult with this request-response pair added to it
*/
public function addResponse(RequestInterface $request, ResponseInterface $response)
{
$new = clone $this;
$new->responses->attach($request, $response);
return $new;
}
/**
* Checks if there are any unsuccessful requests at all.
*
* @return bool
*/
public function hasExceptions()
{
return $this->exceptions->count() > 0;
}
/**
* Returns all exceptions for the unsuccessful requests.
*
* @return Exception[]
*/
public function getExceptions()
{
$exceptions = [];
foreach ($this->exceptions as $request) {
$exceptions[] = $this->exceptions[$request];
}
return $exceptions;
}
/**
* Checks if there is an exception for a request, meaning the request failed.
*
* @param RequestInterface $request
*
* @return bool
*/
public function isFailed(RequestInterface $request)
{
return $this->exceptions->contains($request);
}
/**
* Returns the exception for a failed request.
*
* @param RequestInterface $request
*
* @return Exception
*
* @throws \UnexpectedValueException If request was not part of the batch or was successful
*/
public function getExceptionFor(RequestInterface $request)
{
try {
return $this->exceptions[$request];
} catch (\UnexpectedValueException $e) {
throw new \UnexpectedValueException('Request not found', $e->getCode(), $e);
}
}
/**
* Adds an exception in an immutable way.
*
* @param RequestInterface $request
* @param Exception $exception
*
* @return BatchResult the new BatchResult with this request-exception pair added to it
*/
public function addException(RequestInterface $request, Exception $exception)
{
$new = clone $this;
$new->exceptions->attach($request, $exception);
return $new;
}
public function __clone()
{
$this->responses = clone $this->responses;
$this->exceptions = clone $this->exceptions;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Http\Client\Common;
use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\ResponseInterface;
/**
* A deferred allow to return a promise which has not been resolved yet.
*/
class Deferred implements Promise
{
private $value;
private $failure;
private $state;
private $waitCallback;
private $onFulfilledCallbacks;
private $onRejectedCallbacks;
public function __construct(callable $waitCallback)
{
$this->waitCallback = $waitCallback;
$this->state = Promise::PENDING;
$this->onFulfilledCallbacks = [];
$this->onRejectedCallbacks = [];
}
/**
* {@inheritdoc}
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
$deferred = new self($this->waitCallback);
$this->onFulfilledCallbacks[] = function (ResponseInterface $response) use ($onFulfilled, $deferred) {
try {
if (null !== $onFulfilled) {
$response = $onFulfilled($response);
}
$deferred->resolve($response);
} catch (Exception $exception) {
$deferred->reject($exception);
}
};
$this->onRejectedCallbacks[] = function (Exception $exception) use ($onRejected, $deferred) {
try {
if (null !== $onRejected) {
$response = $onRejected($exception);
$deferred->resolve($response);
return;
}
$deferred->reject($exception);
} catch (Exception $newException) {
$deferred->reject($newException);
}
};
return $deferred;
}
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->state;
}
/**
* Resolve this deferred with a Response.
*/
public function resolve(ResponseInterface $response)
{
if (self::PENDING !== $this->state) {
return;
}
$this->value = $response;
$this->state = self::FULFILLED;
foreach ($this->onFulfilledCallbacks as $onFulfilledCallback) {
$onFulfilledCallback($response);
}
}
/**
* Reject this deferred with an Exception.
*/
public function reject(Exception $exception)
{
if (self::PENDING !== $this->state) {
return;
}
$this->failure = $exception;
$this->state = self::REJECTED;
foreach ($this->onRejectedCallbacks as $onRejectedCallback) {
$onRejectedCallback($exception);
}
}
/**
* {@inheritdoc}
*/
public function wait($unwrap = true)
{
if (self::PENDING === $this->state) {
$callback = $this->waitCallback;
$callback();
}
if (!$unwrap) {
return;
}
if (self::FULFILLED === $this->state) {
return $this->value;
}
throw $this->failure;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
/**
* Emulates an async HTTP client.
*
* This should be replaced by an anonymous class in PHP 7.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient
{
use HttpAsyncClientEmulator;
use HttpClientDecorator;
/**
* @param HttpClient|ClientInterface $httpClient
*/
public function __construct($httpClient)
{
if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) {
throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface');
}
$this->httpClient = $httpClient;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
/**
* Emulates an HTTP client.
*
* This should be replaced by an anonymous class in PHP 7.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class EmulatedHttpClient implements HttpClient, HttpAsyncClient
{
use HttpAsyncClientDecorator;
use HttpClientEmulator;
/**
* @param HttpAsyncClient $httpAsyncClient
*/
public function __construct(HttpAsyncClient $httpAsyncClient)
{
$this->httpAsyncClient = $httpAsyncClient;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\TransferException;
use Http\Client\Common\BatchResult;
/**
* This exception is thrown when HttpClient::sendRequests led to at least one failure.
*
* It gives access to a BatchResult with the request-exception and request-response pairs.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class BatchException extends TransferException
{
/**
* @var BatchResult
*/
private $result;
/**
* @param BatchResult $result
*/
public function __construct(BatchResult $result)
{
$this->result = $result;
}
/**
* Returns the BatchResult that contains all responses and exceptions.
*
* @return BatchResult
*/
public function getResult()
{
return $this->result;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\HttpException;
/**
* Thrown when circular redirection is detected.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class CircularRedirectionException extends HttpException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\HttpException;
/**
* Thrown when there is a client error (4xx).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class ClientErrorException extends HttpException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\TransferException;
/**
* Thrown when a http client cannot be chosen in a pool.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class HttpClientNotFoundException extends TransferException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\RequestException;
/**
* Thrown when the Plugin Client detects an endless loop.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class LoopException extends RequestException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\HttpException;
/**
* Redirect location cannot be chosen.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class MultipleRedirectionException extends HttpException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Common\Exception;
use Http\Client\Exception\HttpException;
/**
* Thrown when there is a server error (5xx).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class ServerErrorException extends HttpException
{
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
/**
* A flexible http client, which implements both interface and will emulate
* one contract, the other, or none at all depending on the injected client contract.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class FlexibleHttpClient implements HttpClient, HttpAsyncClient
{
use HttpClientDecorator;
use HttpAsyncClientDecorator;
/**
* @param HttpClient|HttpAsyncClient|ClientInterface $client
*/
public function __construct($client)
{
if (!($client instanceof HttpClient) && !($client instanceof HttpAsyncClient) && !($client instanceof ClientInterface)) {
throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Http\\Client\\HttpAsyncClient');
}
$this->httpClient = $client;
$this->httpAsyncClient = $client;
if (!($this->httpClient instanceof HttpClient) && !($client instanceof ClientInterface)) {
$this->httpClient = new EmulatedHttpClient($this->httpClient);
}
if (!($this->httpAsyncClient instanceof HttpAsyncClient)) {
$this->httpAsyncClient = new EmulatedHttpAsyncClient($this->httpAsyncClient);
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Psr\Http\Message\RequestInterface;
/**
* Decorates an HTTP Async Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait HttpAsyncClientDecorator
{
/**
* @var HttpAsyncClient
*/
protected $httpAsyncClient;
/**
* {@inheritdoc}
*
* @see HttpAsyncClient::sendAsyncRequest
*/
public function sendAsyncRequest(RequestInterface $request)
{
return $this->httpAsyncClient->sendAsyncRequest($request);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Http\Client\Common;
use Http\Client\Exception;
use Http\Client\Promise;
use Psr\Http\Message\RequestInterface;
/**
* Emulates an HTTP Async Client in an HTTP Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait HttpAsyncClientEmulator
{
/**
* {@inheritdoc}
*
* @see HttpClient::sendRequest
*/
abstract public function sendRequest(RequestInterface $request);
/**
* {@inheritdoc}
*
* @see HttpAsyncClient::sendAsyncRequest
*/
public function sendAsyncRequest(RequestInterface $request)
{
try {
return new Promise\HttpFulfilledPromise($this->sendRequest($request));
} catch (Exception $e) {
return new Promise\HttpRejectedPromise($e);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
/**
* Decorates an HTTP Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait HttpClientDecorator
{
/**
* @var HttpClient|ClientInterface
*/
protected $httpClient;
/**
* {@inheritdoc}
*
* @see HttpClient::sendRequest
*/
public function sendRequest(RequestInterface $request)
{
return $this->httpClient->sendRequest($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Http\Client\Common;
use Psr\Http\Message\RequestInterface;
/**
* Emulates an HTTP Client in an HTTP Async Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait HttpClientEmulator
{
/**
* {@inheritdoc}
*
* @see HttpClient::sendRequest
*/
public function sendRequest(RequestInterface $request)
{
$promise = $this->sendAsyncRequest($request);
return $promise->wait();
}
/**
* {@inheritdoc}
*
* @see HttpAsyncClient::sendAsyncRequest
*/
abstract public function sendAsyncRequest(RequestInterface $request);
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Http\Client\Common;
use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
/**
* A http client pool allows to send requests on a pool of different http client using a specific strategy (least used,
* round robin, ...).
*/
abstract class HttpClientPool implements HttpAsyncClient, HttpClient
{
/**
* @var HttpClientPoolItem[]
*/
protected $clientPool = [];
/**
* Add a client to the pool.
*
* @param HttpClient|HttpAsyncClient|HttpClientPoolItem|ClientInterface $client
*/
public function addHttpClient($client)
{
if (!$client instanceof HttpClientPoolItem) {
$client = new HttpClientPoolItem($client);
}
$this->clientPool[] = $client;
}
/**
* Return an http client given a specific strategy.
*
* @throws HttpClientNotFoundException When no http client has been found into the pool
*
* @return HttpClientPoolItem Return a http client that can do both sync or async
*/
abstract protected function chooseHttpClient();
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
return $this->chooseHttpClient()->sendAsyncRequest($request);
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
return $this->chooseHttpClient()->sendRequest($request);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool;
use Http\Client\Common\HttpClientPoolItem;
/**
* LeastUsedClientPool will choose the client with the less current request in the pool.
*
* This strategy is only useful when doing async request
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class LeastUsedClientPool extends HttpClientPool
{
/**
* {@inheritdoc}
*/
protected function chooseHttpClient()
{
$clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) {
return !$clientPoolItem->isDisabled();
});
if (0 === count($clientPool)) {
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
}
usort($clientPool, function (HttpClientPoolItem $clientA, HttpClientPoolItem $clientB) {
if ($clientA->getSendingRequestCount() === $clientB->getSendingRequestCount()) {
return 0;
}
if ($clientA->getSendingRequestCount() < $clientB->getSendingRequestCount()) {
return -1;
}
return 1;
});
return reset($clientPool);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool;
use Http\Client\Common\HttpClientPoolItem;
/**
* RoundRobinClientPool will choose the next client in the pool.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class RandomClientPool extends HttpClientPool
{
/**
* {@inheritdoc}
*/
protected function chooseHttpClient()
{
$clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) {
return !$clientPoolItem->isDisabled();
});
if (0 === count($clientPool)) {
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
}
return $clientPool[array_rand($clientPool)];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool;
/**
* RoundRobinClientPool will choose the next client in the pool.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class RoundRobinClientPool extends HttpClientPool
{
/**
* {@inheritdoc}
*/
protected function chooseHttpClient()
{
$last = current($this->clientPool);
do {
$client = next($this->clientPool);
if (false === $client) {
$client = reset($this->clientPool);
if (false === $client) {
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
}
}
// Case when there is only one and the last one has been disabled
if ($last === $client && $client->isDisabled()) {
throw new HttpClientNotFoundException('Cannot choose a http client as there is no one enabled in the pool');
}
} while ($client->isDisabled());
return $client;
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Http\Client\Exception;
/**
* A HttpClientPoolItem represent a HttpClient inside a Pool.
*
* It is disabled when a request failed and can be reenable after a certain number of seconds
* It also keep tracks of the current number of request the client is currently sending (only usable for async method)
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class HttpClientPoolItem implements HttpClient, HttpAsyncClient
{
/**
* @var int Number of request this client is currently sending
*/
private $sendingRequestCount = 0;
/**
* @var \DateTime|null Time when this client has been disabled or null if enable
*/
private $disabledAt;
/**
* @var int|null Number of seconds after this client is reenable, by default null: never reenable this client
*/
private $reenableAfter;
/**
* @var FlexibleHttpClient A http client responding to async and sync request
*/
private $client;
/**
* @param HttpClient|HttpAsyncClient|ClientInterface $client
* @param null|int $reenableAfter Number of seconds after this client is reenable
*/
public function __construct($client, $reenableAfter = null)
{
$this->client = new FlexibleHttpClient($client);
$this->reenableAfter = $reenableAfter;
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
if ($this->isDisabled()) {
throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request);
}
try {
$this->incrementRequestCount();
$response = $this->client->sendRequest($request);
$this->decrementRequestCount();
} catch (Exception $e) {
$this->disable();
$this->decrementRequestCount();
throw $e;
}
return $response;
}
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
if ($this->isDisabled()) {
throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request);
}
$this->incrementRequestCount();
return $this->client->sendAsyncRequest($request)->then(function ($response) {
$this->decrementRequestCount();
return $response;
}, function ($exception) {
$this->disable();
$this->decrementRequestCount();
throw $exception;
});
}
/**
* Whether this client is disabled or not.
*
* Will also reactivate this client if possible
*
* @internal
*
* @return bool
*/
public function isDisabled()
{
$disabledAt = $this->getDisabledAt();
if (null !== $this->reenableAfter && null !== $disabledAt) {
// Reenable after a certain time
$now = new \DateTime();
if (($now->getTimestamp() - $disabledAt->getTimestamp()) >= $this->reenableAfter) {
$this->enable();
return false;
}
return true;
}
return null !== $disabledAt;
}
/**
* Get current number of request that is send by the underlying http client.
*
* @internal
*
* @return int
*/
public function getSendingRequestCount()
{
return $this->sendingRequestCount;
}
/**
* Return when this client has been disabled or null if it's enabled.
*
* @return \DateTime|null
*/
private function getDisabledAt()
{
return $this->disabledAt;
}
/**
* Increment the request count.
*/
private function incrementRequestCount()
{
++$this->sendingRequestCount;
}
/**
* Decrement the request count.
*/
private function decrementRequestCount()
{
--$this->sendingRequestCount;
}
/**
* Enable the current client.
*/
private function enable()
{
$this->disabledAt = null;
}
/**
* Disable the current client.
*/
private function disable()
{
$this->disabledAt = new \DateTime('now');
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Http\Client\Common;
use Http\Client\Exception\RequestException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Message\RequestMatcher;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
/**
* Route a request to a specific client in the stack based using a RequestMatcher.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class HttpClientRouter implements HttpClient, HttpAsyncClient
{
/**
* @var array
*/
private $clients = [];
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
$client = $this->chooseHttpClient($request);
return $client->sendRequest($request);
}
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
$client = $this->chooseHttpClient($request);
return $client->sendAsyncRequest($request);
}
/**
* Add a client to the router.
*
* @param HttpClient|HttpAsyncClient|ClientInterface $client
* @param RequestMatcher $requestMatcher
*/
public function addClient($client, RequestMatcher $requestMatcher)
{
$this->clients[] = [
'matcher' => $requestMatcher,
'client' => new FlexibleHttpClient($client),
];
}
/**
* Choose an HTTP client given a specific request.
*
* @param RequestInterface $request
*
* @return HttpClient|HttpAsyncClient|ClientInterface
*/
protected function chooseHttpClient(RequestInterface $request)
{
foreach ($this->clients as $client) {
if ($client['matcher']->matches($request)) {
return $client['client'];
}
}
throw new RequestException('No client found for the specified request', $request);
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace Http\Client\Common;
use Http\Client\Exception;
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
/**
* Convenience HTTP client that integrates the MessageFactory in order to send
* requests in the following form:.
*
* $client
* ->get('/foo')
* ->post('/bar')
* ;
*
* The client also exposes the sendRequest methods of the wrapped HttpClient.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
* @author David Buchmann <mail@davidbu.ch>
*/
class HttpMethodsClient implements HttpClient
{
/**
* @var HttpClient|ClientInterface
*/
private $httpClient;
/**
* @var RequestFactory
*/
private $requestFactory;
/**
* @param HttpClient|ClientInterface $httpClient The client to send requests with
* @param RequestFactory $requestFactory The message factory to create requests
*/
public function __construct($httpClient, RequestFactory $requestFactory)
{
if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) {
throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface');
}
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
}
/**
* Sends a GET request.
*
* @param string|UriInterface $uri
* @param array $headers
*
* @throws Exception
*
* @return ResponseInterface
*/
public function get($uri, array $headers = [])
{
return $this->send('GET', $uri, $headers, null);
}
/**
* Sends an HEAD request.
*
* @param string|UriInterface $uri
* @param array $headers
*
* @throws Exception
*
* @return ResponseInterface
*/
public function head($uri, array $headers = [])
{
return $this->send('HEAD', $uri, $headers, null);
}
/**
* Sends a TRACE request.
*
* @param string|UriInterface $uri
* @param array $headers
*
* @throws Exception
*
* @return ResponseInterface
*/
public function trace($uri, array $headers = [])
{
return $this->send('TRACE', $uri, $headers, null);
}
/**
* Sends a POST request.
*
* @param string|UriInterface $uri
* @param array $headers
* @param string|StreamInterface|null $body
*
* @throws Exception
*
* @return ResponseInterface
*/
public function post($uri, array $headers = [], $body = null)
{
return $this->send('POST', $uri, $headers, $body);
}
/**
* Sends a PUT request.
*
* @param string|UriInterface $uri
* @param array $headers
* @param string|StreamInterface|null $body
*
* @throws Exception
*
* @return ResponseInterface
*/
public function put($uri, array $headers = [], $body = null)
{
return $this->send('PUT', $uri, $headers, $body);
}
/**
* Sends a PATCH request.
*
* @param string|UriInterface $uri
* @param array $headers
* @param string|StreamInterface|null $body
*
* @throws Exception
*
* @return ResponseInterface
*/
public function patch($uri, array $headers = [], $body = null)
{
return $this->send('PATCH', $uri, $headers, $body);
}
/**
* Sends a DELETE request.
*
* @param string|UriInterface $uri
* @param array $headers
* @param string|StreamInterface|null $body
*
* @throws Exception
*
* @return ResponseInterface
*/
public function delete($uri, array $headers = [], $body = null)
{
return $this->send('DELETE', $uri, $headers, $body);
}
/**
* Sends an OPTIONS request.
*
* @param string|UriInterface $uri
* @param array $headers
* @param string|StreamInterface|null $body
*
* @throws Exception
*
* @return ResponseInterface
*/
public function options($uri, array $headers = [], $body = null)
{
return $this->send('OPTIONS', $uri, $headers, $body);
}
/**
* Sends a request with any HTTP method.
*
* @param string $method HTTP method to use
* @param string|UriInterface $uri
* @param array $headers
* @param string|StreamInterface|null $body
*
* @throws Exception
*
* @return ResponseInterface
*/
public function send($method, $uri, array $headers = [], $body = null)
{
return $this->sendRequest($this->requestFactory->createRequest(
$method,
$uri,
$headers,
$body
));
}
/**
* Forward to the underlying HttpClient.
*
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
return $this->httpClient->sendRequest($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Http\Client\Common;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
/**
* A plugin is a middleware to transform the request and/or the response.
*
* The plugin can:
* - break the chain and return a response
* - dispatch the request to the next middleware
* - restart the request
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface Plugin
{
/**
* Handle the request and return the response coming from the next callable.
*
* @see http://docs.php-http.org/en/latest/plugins/build-your-own.html
*
* @param RequestInterface $request
* @param callable $next Next middleware in the chain, the request is passed as the first argument
* @param callable $first First middleware in the chain, used to to restart a request
*
* @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception (The same as HttpAsyncClient).
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first);
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Add schema, host and port to a request. Can be set to overwrite the schema and host if desired.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class AddHostPlugin implements Plugin
{
/**
* @var UriInterface
*/
private $host;
/**
* @var bool
*/
private $replace;
/**
* @param UriInterface $host
* @param array $config {
*
* @var bool $replace True will replace all hosts, false will only add host when none is specified.
* }
*/
public function __construct(UriInterface $host, array $config = [])
{
if ('' === $host->getHost()) {
throw new \LogicException('Host can not be empty');
}
$this->host = $host;
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$options = $resolver->resolve($config);
$this->replace = $options['replace'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
if ($this->replace || '' === $request->getUri()->getHost()) {
$uri = $request->getUri()
->withHost($this->host->getHost())
->withScheme($this->host->getScheme())
->withPort($this->host->getPort())
;
$request = $request->withUri($uri);
}
return $next($request);
}
/**
* @param OptionsResolver $resolver
*/
private function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'replace' => false,
]);
$resolver->setAllowedTypes('replace', 'bool');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* Prepend a base path to the request URI. Useful for base API URLs like http://domain.com/api.
*
* @author Sullivan Senechal <soullivaneuh@gmail.com>
*/
final class AddPathPlugin implements Plugin
{
/**
* @var UriInterface
*/
private $uri;
/**
* Stores identifiers of the already altered requests.
*
* @var array
*/
private $alteredRequests = [];
/**
* @param UriInterface $uri
*/
public function __construct(UriInterface $uri)
{
if ('' === $uri->getPath()) {
throw new \LogicException('URI path cannot be empty');
}
if ('/' === substr($uri->getPath(), -1)) {
$uri = $uri->withPath(rtrim($uri->getPath(), '/'));
}
$this->uri = $uri;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$identifier = spl_object_hash((object) $first);
if (!array_key_exists($identifier, $this->alteredRequests)) {
$request = $request->withUri($request->getUri()
->withPath($this->uri->getPath().$request->getUri()->getPath())
);
$this->alteredRequests[$identifier] = $identifier;
}
return $next($request);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Send an authenticated request.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class AuthenticationPlugin implements Plugin
{
/**
* @var Authentication An authentication system
*/
private $authentication;
/**
* @param Authentication $authentication
*/
public function __construct(Authentication $authentication)
{
$this->authentication = $authentication;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$request = $this->authentication->authenticate($request);
return $next($request);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* Combines the AddHostPlugin and AddPathPlugin.
*
* @author Sullivan Senechal <soullivaneuh@gmail.com>
*/
final class BaseUriPlugin implements Plugin
{
/**
* @var AddHostPlugin
*/
private $addHostPlugin;
/**
* @var AddPathPlugin|null
*/
private $addPathPlugin = null;
/**
* @param UriInterface $uri Has to contain a host name and cans have a path.
* @param array $hostConfig Config for AddHostPlugin. @see AddHostPlugin::configureOptions
*/
public function __construct(UriInterface $uri, array $hostConfig = [])
{
$this->addHostPlugin = new AddHostPlugin($uri, $hostConfig);
if (rtrim($uri->getPath(), '/')) {
$this->addPathPlugin = new AddPathPlugin($uri);
}
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$addHostNext = function (RequestInterface $request) use ($next, $first) {
return $this->addHostPlugin->handleRequest($request, $next, $first);
};
if ($this->addPathPlugin) {
return $this->addPathPlugin->handleRequest($request, $addHostNext, $first);
}
return $addHostNext($request);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\Encoding\ChunkStream;
use Psr\Http\Message\RequestInterface;
/**
* Allow to set the correct content length header on the request or to transfer it as a chunk if not possible.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class ContentLengthPlugin implements Plugin
{
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
if (!$request->hasHeader('Content-Length')) {
$stream = $request->getBody();
// Cannot determine the size so we use a chunk stream
if (null === $stream->getSize()) {
$stream = new ChunkStream($stream);
$request = $request->withBody($stream);
$request = $request->withAddedHeader('Transfer-Encoding', 'chunked');
} else {
$request = $request->withHeader('Content-Length', (string) $stream->getSize());
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Allow to set the correct content type header on the request automatically only if it is not set.
*
* @author Karim Pinchon <karim.pinchon@gmail.com>
*/
final class ContentTypePlugin implements Plugin
{
/**
* Allow to disable the content type detection when stream is too large (as it can consume a lot of resource).
*
* @var bool
*
* true skip the content type detection
* false detect the content type (default value)
*/
protected $skipDetection;
/**
* Determine the size stream limit for which the detection as to be skipped (default to 16Mb).
*
* @var int
*/
protected $sizeLimit;
/**
* @param array $config {
*
* @var bool $skip_detection True skip detection if stream size is bigger than $size_limit.
* @var int $size_limit size stream limit for which the detection as to be skipped.
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'skip_detection' => false,
'size_limit' => 16000000,
]);
$resolver->setAllowedTypes('skip_detection', 'bool');
$resolver->setAllowedTypes('size_limit', 'int');
$options = $resolver->resolve($config);
$this->skipDetection = $options['skip_detection'];
$this->sizeLimit = $options['size_limit'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
if (!$request->hasHeader('Content-Type')) {
$stream = $request->getBody();
$streamSize = $stream->getSize();
if (!$stream->isSeekable()) {
return $next($request);
}
if (0 === $streamSize) {
return $next($request);
}
if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) {
return $next($request);
}
if ($this->isJson($stream)) {
$request = $request->withHeader('Content-Type', 'application/json');
return $next($request);
}
if ($this->isXml($stream)) {
$request = $request->withHeader('Content-Type', 'application/xml');
return $next($request);
}
}
return $next($request);
}
/**
* @param $stream StreamInterface
*
* @return bool
*/
private function isJson($stream)
{
$stream->rewind();
json_decode($stream->getContents());
return JSON_ERROR_NONE === json_last_error();
}
/**
* @param $stream StreamInterface
*
* @return \SimpleXMLElement|false
*/
private function isXml($stream)
{
$stream->rewind();
$previousValue = libxml_use_internal_errors(true);
$isXml = simplexml_load_string($stream->getContents());
libxml_use_internal_errors($previousValue);
return $isXml;
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Client\Exception\TransferException;
use Http\Message\Cookie;
use Http\Message\CookieJar;
use Http\Message\CookieUtil;
use Http\Message\Exception\UnexpectedValueException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Handle request cookies.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class CookiePlugin implements Plugin
{
/**
* Cookie storage.
*
* @var CookieJar
*/
private $cookieJar;
/**
* @param CookieJar $cookieJar
*/
public function __construct(CookieJar $cookieJar)
{
$this->cookieJar = $cookieJar;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$cookies = [];
foreach ($this->cookieJar->getCookies() as $cookie) {
if ($cookie->isExpired()) {
continue;
}
if (!$cookie->matchDomain($request->getUri()->getHost())) {
continue;
}
if (!$cookie->matchPath($request->getUri()->getPath())) {
continue;
}
if ($cookie->isSecure() && ('https' !== $request->getUri()->getScheme())) {
continue;
}
$cookies[] = sprintf('%s=%s', $cookie->getName(), $cookie->getValue());
}
if (!empty($cookies)) {
$request = $request->withAddedHeader('Cookie', implode('; ', array_unique($cookies)));
}
return $next($request)->then(function (ResponseInterface $response) use ($request) {
if ($response->hasHeader('Set-Cookie')) {
$setCookies = $response->getHeader('Set-Cookie');
foreach ($setCookies as $setCookie) {
$cookie = $this->createCookie($request, $setCookie);
// Cookie invalid do not use it
if (null === $cookie) {
continue;
}
// Restrict setting cookie from another domain
if (!preg_match("/\.{$cookie->getDomain()}$/", '.'.$request->getUri()->getHost())) {
continue;
}
$this->cookieJar->addCookie($cookie);
}
}
return $response;
});
}
/**
* Creates a cookie from a string.
*
* @param RequestInterface $request
* @param $setCookie
*
* @return Cookie|null
*
* @throws TransferException
*/
private function createCookie(RequestInterface $request, $setCookie)
{
$parts = array_map('trim', explode(';', $setCookie));
if (empty($parts) || !strpos($parts[0], '=')) {
return;
}
list($name, $cookieValue) = $this->createValueKey(array_shift($parts));
$maxAge = null;
$expires = null;
$domain = $request->getUri()->getHost();
$path = $request->getUri()->getPath();
$secure = false;
$httpOnly = false;
// Add the cookie pieces into the parsed data array
foreach ($parts as $part) {
list($key, $value) = $this->createValueKey($part);
switch (strtolower($key)) {
case 'expires':
try {
$expires = CookieUtil::parseDate($value);
} catch (UnexpectedValueException $e) {
throw new TransferException(
sprintf(
'Cookie header `%s` expires value `%s` could not be converted to date',
$name,
$value
),
0,
$e
);
}
break;
case 'max-age':
$maxAge = (int) $value;
break;
case 'domain':
$domain = $value;
break;
case 'path':
$path = $value;
break;
case 'secure':
$secure = true;
break;
case 'httponly':
$httpOnly = true;
break;
}
}
return new Cookie($name, $cookieValue, $maxAge, $domain, $path, $secure, $httpOnly, $expires);
}
/**
* Separates key/value pair from cookie.
*
* @param $part
*
* @return array
*/
private function createValueKey($part)
{
$parts = explode('=', $part, 2);
$key = trim($parts[0]);
$value = isset($parts[1]) ? trim($parts[1]) : true;
return [$key, $value];
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\Encoding;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Allow to decode response body with a chunk, deflate, compress or gzip encoding.
*
* If zlib is not installed, only chunked encoding can be handled.
*
* If Content-Encoding is not disabled, the plugin will add an Accept-Encoding header for the encoding methods it supports.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class DecoderPlugin implements Plugin
{
/**
* @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true).
*
* If set to false only the Transfer-Encoding header will be used
*/
private $useContentEncoding;
/**
* @param array $config {
*
* @var bool $use_content_encoding Whether this plugin should look at the Content-Encoding header first or only at the Transfer-Encoding (defaults to true).
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'use_content_encoding' => true,
]);
$resolver->setAllowedTypes('use_content_encoding', 'bool');
$options = $resolver->resolve($config);
$this->useContentEncoding = $options['use_content_encoding'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$encodings = extension_loaded('zlib') ? ['gzip', 'deflate'] : ['identity'];
if ($this->useContentEncoding) {
$request = $request->withHeader('Accept-Encoding', $encodings);
}
$encodings[] = 'chunked';
$request = $request->withHeader('TE', $encodings);
return $next($request)->then(function (ResponseInterface $response) {
return $this->decodeResponse($response);
});
}
/**
* Decode a response body given its Transfer-Encoding or Content-Encoding value.
*
* @param ResponseInterface $response Response to decode
*
* @return ResponseInterface New response decoded
*/
private function decodeResponse(ResponseInterface $response)
{
$response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response);
if ($this->useContentEncoding) {
$response = $this->decodeOnEncodingHeader('Content-Encoding', $response);
}
return $response;
}
/**
* Decode a response on a specific header (content encoding or transfer encoding mainly).
*
* @param string $headerName Name of the header
* @param ResponseInterface $response Response
*
* @return ResponseInterface A new instance of the response decoded
*/
private function decodeOnEncodingHeader($headerName, ResponseInterface $response)
{
if ($response->hasHeader($headerName)) {
$encodings = $response->getHeader($headerName);
$newEncodings = [];
while ($encoding = array_pop($encodings)) {
$stream = $this->decorateStream($encoding, $response->getBody());
if (false === $stream) {
array_unshift($newEncodings, $encoding);
continue;
}
$response = $response->withBody($stream);
}
if (\count($newEncodings) > 0) {
$response = $response->withHeader($headerName, $newEncodings);
} else {
$response = $response->withoutHeader($headerName);
}
}
return $response;
}
/**
* Decorate a stream given an encoding.
*
* @param string $encoding
* @param StreamInterface $stream
*
* @return StreamInterface|false A new stream interface or false if encoding is not supported
*/
private function decorateStream($encoding, StreamInterface $stream)
{
if ('chunked' === strtolower($encoding)) {
return new Encoding\DechunkStream($stream);
}
if ('deflate' === strtolower($encoding)) {
return new Encoding\DecompressStream($stream);
}
if ('gzip' === strtolower($encoding)) {
return new Encoding\GzipDecodeStream($stream);
}
return false;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Exception\ClientErrorException;
use Http\Client\Common\Exception\ServerErrorException;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Throw exception when the response of a request is not acceptable.
*
* Status codes 400-499 lead to a ClientErrorException, status 500-599 to a ServerErrorException.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class ErrorPlugin implements Plugin
{
/**
* @var bool Whether this plugin should only throw 5XX Exceptions (default to false).
*
* If set to true 4XX Responses code will never throw an exception
*/
private $onlyServerException;
/**
* @param array $config {
*
* @var bool only_server_exception Whether this plugin should only throw 5XX Exceptions (default to false).
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'only_server_exception' => false,
]);
$resolver->setAllowedTypes('only_server_exception', 'bool');
$options = $resolver->resolve($config);
$this->onlyServerException = $options['only_server_exception'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$promise = $next($request);
return $promise->then(function (ResponseInterface $response) use ($request) {
return $this->transformResponseToException($request, $response);
});
}
/**
* Transform response to an error if possible.
*
* @param RequestInterface $request Request of the call
* @param ResponseInterface $response Response of the call
*
* @throws ClientErrorException If response status code is a 4xx
* @throws ServerErrorException If response status code is a 5xx
*
* @return ResponseInterface If status code is not in 4xx or 5xx return response
*/
protected function transformResponseToException(RequestInterface $request, ResponseInterface $response)
{
if (!$this->onlyServerException && $response->getStatusCode() >= 400 && $response->getStatusCode() < 500) {
throw new ClientErrorException($response->getReasonPhrase(), $request, $response);
}
if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) {
throw new ServerErrorException($response->getReasonPhrase(), $request, $response);
}
return $response;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Append headers to the request.
*
* If the header already exists the value will be appended to the current value.
*
* This only makes sense for headers that can have multiple values like 'Forwarded'
*
* @see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
*
* @author Soufiane Ghzal <sghzal@gmail.com>
*/
final class HeaderAppendPlugin implements Plugin
{
/**
* @var array
*/
private $headers = [];
/**
* @param array $headers Hashmap of header name to header value
*/
public function __construct(array $headers)
{
$this->headers = $headers;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
foreach ($this->headers as $header => $headerValue) {
$request = $request->withAddedHeader($header, $headerValue);
}
return $next($request);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Set header to default value if it does not exist.
*
* If a given header already exists the value wont be replaced and the request wont be changed.
*
* @author Soufiane Ghzal <sghzal@gmail.com>
*/
final class HeaderDefaultsPlugin implements Plugin
{
/**
* @var array
*/
private $headers = [];
/**
* @param array $headers Hashmap of header name to header value
*/
public function __construct(array $headers)
{
$this->headers = $headers;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
foreach ($this->headers as $header => $headerValue) {
if (!$request->hasHeader($header)) {
$request = $request->withHeader($header, $headerValue);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Removes headers from the request.
*
* @author Soufiane Ghzal <sghzal@gmail.com>
*/
final class HeaderRemovePlugin implements Plugin
{
/**
* @var array
*/
private $headers = [];
/**
* @param array $headers List of header names to remove from the request
*/
public function __construct(array $headers)
{
$this->headers = $headers;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
foreach ($this->headers as $header) {
if ($request->hasHeader($header)) {
$request = $request->withoutHeader($header);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Set headers on the request.
*
* If the header does not exist it wil be set, if the header already exists it will be replaced.
*
* @author Soufiane Ghzal <sghzal@gmail.com>
*/
final class HeaderSetPlugin implements Plugin
{
/**
* @var array
*/
private $headers = [];
/**
* @param array $headers Hashmap of header name to header value
*/
public function __construct(array $headers)
{
$this->headers = $headers;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
foreach ($this->headers as $header => $headerValue) {
$request = $request->withHeader($header, $headerValue);
}
return $next($request);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Record HTTP calls.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class HistoryPlugin implements Plugin
{
/**
* Journal use to store request / responses / exception.
*
* @var Journal
*/
private $journal;
/**
* @param Journal $journal
*/
public function __construct(Journal $journal)
{
$this->journal = $journal;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$journal = $this->journal;
return $next($request)->then(function (ResponseInterface $response) use ($request, $journal) {
$journal->addSuccess($request, $response);
return $response;
}, function (Exception $exception) use ($request, $journal) {
$journal->addFailure($request, $exception);
throw $exception;
});
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Records history of HTTP calls.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface Journal
{
/**
* Record a successful call.
*
* @param RequestInterface $request Request use to make the call
* @param ResponseInterface $response Response returned by the call
*/
public function addSuccess(RequestInterface $request, ResponseInterface $response);
/**
* Record a failed call.
*
* @param RequestInterface $request Request use to make the call
* @param Exception $exception Exception returned by the call
*/
public function addFailure(RequestInterface $request, Exception $exception);
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* Set query to default value if it does not exist.
*
* If a given query parameter already exists the value wont be replaced and the request wont be changed.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class QueryDefaultsPlugin implements Plugin
{
/**
* @var array
*/
private $queryParams = [];
/**
* @param array $queryParams Hashmap of query name to query value. Names and values must not be url encoded as
* this plugin will encode them
*/
public function __construct(array $queryParams)
{
$this->queryParams = $queryParams;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$uri = $request->getUri();
parse_str($uri->getQuery(), $query);
$query += $this->queryParams;
$request = $request->withUri(
$uri->withQuery(http_build_query($query))
);
return $next($request);
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Exception\CircularRedirectionException;
use Http\Client\Common\Exception\MultipleRedirectionException;
use Http\Client\Common\Plugin;
use Http\Client\Exception\HttpException;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Follow redirections.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class RedirectPlugin implements Plugin
{
/**
* Rule on how to redirect, change method for the new request.
*
* @var array
*/
protected $redirectCodes = [
300 => [
'switch' => [
'unless' => ['GET', 'HEAD'],
'to' => 'GET',
],
'multiple' => true,
'permanent' => false,
],
301 => [
'switch' => [
'unless' => ['GET', 'HEAD'],
'to' => 'GET',
],
'multiple' => false,
'permanent' => true,
],
302 => [
'switch' => [
'unless' => ['GET', 'HEAD'],
'to' => 'GET',
],
'multiple' => false,
'permanent' => false,
],
303 => [
'switch' => [
'unless' => ['GET', 'HEAD'],
'to' => 'GET',
],
'multiple' => false,
'permanent' => false,
],
307 => [
'switch' => false,
'multiple' => false,
'permanent' => false,
],
308 => [
'switch' => false,
'multiple' => false,
'permanent' => true,
],
];
/**
* Determine how header should be preserved from old request.
*
* @var bool|array
*
* true will keep all previous headers (default value)
* false will ditch all previous headers
* string[] will keep only headers with the specified names
*/
protected $preserveHeader;
/**
* Store all previous redirect from 301 / 308 status code.
*
* @var array
*/
protected $redirectStorage = [];
/**
* Whether the location header must be directly used for a multiple redirection status code (300).
*
* @var bool
*/
protected $useDefaultForMultiple;
/**
* @var array
*/
protected $circularDetection = [];
/**
* @param array $config {
*
* @var bool|string[] $preserve_header True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep
* @var bool $use_default_for_multiple Whether the location header must be directly used for a multiple redirection status code (300).
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'preserve_header' => true,
'use_default_for_multiple' => true,
]);
$resolver->setAllowedTypes('preserve_header', ['bool', 'array']);
$resolver->setAllowedTypes('use_default_for_multiple', 'bool');
$resolver->setNormalizer('preserve_header', function (OptionsResolver $resolver, $value) {
if (is_bool($value) && false === $value) {
return [];
}
return $value;
});
$options = $resolver->resolve($config);
$this->preserveHeader = $options['preserve_header'];
$this->useDefaultForMultiple = $options['use_default_for_multiple'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
// Check in storage
if (array_key_exists((string) $request->getUri(), $this->redirectStorage)) {
$uri = $this->redirectStorage[(string) $request->getUri()]['uri'];
$statusCode = $this->redirectStorage[(string) $request->getUri()]['status'];
$redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode);
return $first($redirectRequest);
}
return $next($request)->then(function (ResponseInterface $response) use ($request, $first) {
$statusCode = $response->getStatusCode();
if (!array_key_exists($statusCode, $this->redirectCodes)) {
return $response;
}
$uri = $this->createUri($response, $request);
$redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode);
$chainIdentifier = spl_object_hash((object) $first);
if (!array_key_exists($chainIdentifier, $this->circularDetection)) {
$this->circularDetection[$chainIdentifier] = [];
}
$this->circularDetection[$chainIdentifier][] = (string) $request->getUri();
if (in_array((string) $redirectRequest->getUri(), $this->circularDetection[$chainIdentifier])) {
throw new CircularRedirectionException('Circular redirection detected', $request, $response);
}
if ($this->redirectCodes[$statusCode]['permanent']) {
$this->redirectStorage[(string) $request->getUri()] = [
'uri' => $uri,
'status' => $statusCode,
];
}
// Call redirect request in synchrone
$redirectPromise = $first($redirectRequest);
return $redirectPromise->wait();
});
}
/**
* Builds the redirect request.
*
* @param RequestInterface $request Original request
* @param UriInterface $uri New uri
* @param int $statusCode Status code from the redirect response
*
* @return MessageInterface|RequestInterface
*/
protected function buildRedirectRequest(RequestInterface $request, UriInterface $uri, $statusCode)
{
$request = $request->withUri($uri);
if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($request->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) {
$request = $request->withMethod($this->redirectCodes[$statusCode]['switch']['to']);
}
if (is_array($this->preserveHeader)) {
$headers = array_keys($request->getHeaders());
foreach ($headers as $name) {
if (!in_array($name, $this->preserveHeader)) {
$request = $request->withoutHeader($name);
}
}
}
return $request;
}
/**
* Creates a new Uri from the old request and the location header.
*
* @param ResponseInterface $response The redirect response
* @param RequestInterface $request The original request
*
* @throws HttpException If location header is not usable (missing or incorrect)
* @throws MultipleRedirectionException If a 300 status code is received and default location cannot be resolved (doesn't use the location header or not present)
*
* @return UriInterface
*/
private function createUri(ResponseInterface $response, RequestInterface $request)
{
if ($this->redirectCodes[$response->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$response->hasHeader('Location'))) {
throw new MultipleRedirectionException('Cannot choose a redirection', $request, $response);
}
if (!$response->hasHeader('Location')) {
throw new HttpException('Redirect status code, but no location header present in the response', $request, $response);
}
$location = $response->getHeaderLine('Location');
$parsedLocation = parse_url($location);
if (false === $parsedLocation) {
throw new HttpException(sprintf('Location %s could not be parsed', $location), $request, $response);
}
$uri = $request->getUri();
if (array_key_exists('scheme', $parsedLocation)) {
$uri = $uri->withScheme($parsedLocation['scheme']);
}
if (array_key_exists('host', $parsedLocation)) {
$uri = $uri->withHost($parsedLocation['host']);
}
if (array_key_exists('port', $parsedLocation)) {
$uri = $uri->withPort($parsedLocation['port']);
}
if (array_key_exists('path', $parsedLocation)) {
$uri = $uri->withPath($parsedLocation['path']);
}
if (array_key_exists('query', $parsedLocation)) {
$uri = $uri->withQuery($parsedLocation['query']);
} else {
$uri = $uri->withQuery('');
}
if (array_key_exists('fragment', $parsedLocation)) {
$uri = $uri->withFragment($parsedLocation['fragment']);
} else {
$uri = $uri->withFragment('');
}
return $uri;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\RequestMatcher;
use Psr\Http\Message\RequestInterface;
/**
* Apply a delegated plugin based on a request match.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class RequestMatcherPlugin implements Plugin
{
/**
* @var RequestMatcher
*/
private $requestMatcher;
/**
* @var Plugin
*/
private $delegatedPlugin;
/**
* @param RequestMatcher $requestMatcher
* @param Plugin $delegatedPlugin
*/
public function __construct(RequestMatcher $requestMatcher, Plugin $delegatedPlugin)
{
$this->requestMatcher = $requestMatcher;
$this->delegatedPlugin = $delegatedPlugin;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
if ($this->requestMatcher->matches($request)) {
return $this->delegatedPlugin->handleRequest($request, $next, $first);
}
return $next($request);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Retry the request if an exception is thrown.
*
* By default will retry only one time.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class RetryPlugin implements Plugin
{
/**
* Number of retry before sending an exception.
*
* @var int
*/
private $retry;
/**
* @var callable
*/
private $exceptionDelay;
/**
* @var callable
*/
private $exceptionDecider;
/**
* Store the retry counter for each request.
*
* @var array
*/
private $retryStorage = [];
/**
* @param array $config {
*
* @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up.
* @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried.
* @var callable $exception_delay A callback that gets a request, an exception and the number of retries and returns how many microseconds we should wait before trying again.
* }
*/
public function __construct(array $config = [])
{
if (array_key_exists('decider', $config)) {
if (array_key_exists('exception_decider', $config)) {
throw new \InvalidArgumentException('Do not set both the old "decider" and new "exception_decider" options');
}
trigger_error('The "decider" option has been deprecated in favour of "exception_decider"', E_USER_DEPRECATED);
$config['exception_decider'] = $config['decider'];
unset($config['decider']);
}
if (array_key_exists('delay', $config)) {
if (array_key_exists('exception_delay', $config)) {
throw new \InvalidArgumentException('Do not set both the old "delay" and new "exception_delay" options');
}
trigger_error('The "delay" option has been deprecated in favour of "exception_delay"', E_USER_DEPRECATED);
$config['exception_delay'] = $config['delay'];
unset($config['delay']);
}
$resolver = new OptionsResolver();
$resolver->setDefaults([
'retries' => 1,
'exception_decider' => function (RequestInterface $request, Exception $e) {
return true;
},
'exception_delay' => __CLASS__.'::defaultDelay',
]);
$resolver->setAllowedTypes('retries', 'int');
$resolver->setAllowedTypes('exception_decider', 'callable');
$resolver->setAllowedTypes('exception_delay', 'callable');
$options = $resolver->resolve($config);
$this->retry = $options['retries'];
$this->exceptionDecider = $options['exception_decider'];
$this->exceptionDelay = $options['exception_delay'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$chainIdentifier = spl_object_hash((object) $first);
return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) {
if (array_key_exists($chainIdentifier, $this->retryStorage)) {
unset($this->retryStorage[$chainIdentifier]);
}
return $response;
}, function (Exception $exception) use ($request, $next, $first, $chainIdentifier) {
if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
$this->retryStorage[$chainIdentifier] = 0;
}
if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
unset($this->retryStorage[$chainIdentifier]);
throw $exception;
}
if (!call_user_func($this->exceptionDecider, $request, $exception)) {
throw $exception;
}
$time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]);
usleep($time);
// Retry in synchrone
++$this->retryStorage[$chainIdentifier];
$promise = $this->handleRequest($request, $next, $first);
return $promise->wait();
});
}
/**
* @param RequestInterface $request
* @param Exception $e
* @param int $retries The number of retries we made before. First time this get called it will be 0.
*
* @return int
*/
public static function defaultDelay(RequestInterface $request, Exception $e, $retries)
{
return pow(2, $retries) * 500000;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
/**
* A plugin that helps you migrate from php-http/client-common 1.x to 2.x. This
* will also help you to support PHP5 at the same time you support 2.x.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
trait VersionBridgePlugin
{
abstract protected function doHandleRequest(RequestInterface $request, callable $next, callable $first);
public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
return $this->doHandleRequest($request, $next, $first);
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Http\Client\Common;
use Http\Client\Common\Exception\LoopException;
use Http\Client\Exception as HttplugException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Client\Promise\HttpFulfilledPromise;
use Http\Client\Promise\HttpRejectedPromise;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* The client managing plugins and providing a decorator around HTTP Clients.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class PluginClient implements HttpClient, HttpAsyncClient
{
/**
* An HTTP async client.
*
* @var HttpAsyncClient
*/
private $client;
/**
* The plugin chain.
*
* @var Plugin[]
*/
private $plugins;
/**
* A list of options.
*
* @var array
*/
private $options;
/**
* @param HttpClient|HttpAsyncClient|ClientInterface $client
* @param Plugin[] $plugins
* @param array $options {
*
* @var int $max_restarts
* @var Plugin[] $debug_plugins an array of plugins that are injected between each normal plugin
* }
*
* @throws \RuntimeException if client is not an instance of HttpClient or HttpAsyncClient
*/
public function __construct($client, array $plugins = [], array $options = [])
{
if ($client instanceof HttpAsyncClient) {
$this->client = $client;
} elseif ($client instanceof HttpClient || $client instanceof ClientInterface) {
$this->client = new EmulatedHttpAsyncClient($client);
} else {
throw new \RuntimeException('Client must be an instance of Http\\Client\\HttpClient or Http\\Client\\HttpAsyncClient');
}
$this->plugins = $plugins;
$this->options = $this->configure($options);
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request)
{
// If we don't have an http client, use the async call
if (!($this->client instanceof HttpClient)) {
return $this->sendAsyncRequest($request)->wait();
}
// Else we want to use the synchronous call of the underlying client, and not the async one in the case
// we have both an async and sync call
$pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
try {
return new HttpFulfilledPromise($this->client->sendRequest($request));
} catch (HttplugException $exception) {
return new HttpRejectedPromise($exception);
}
});
return $pluginChain($request)->wait();
}
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
$pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) {
return $this->client->sendAsyncRequest($request);
});
return $pluginChain($request);
}
/**
* Configure the plugin client.
*
* @param array $options
*
* @return array
*/
private function configure(array $options = [])
{
if (isset($options['debug_plugins'])) {
@trigger_error('The "debug_plugins" option is deprecated since 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
}
$resolver = new OptionsResolver();
$resolver->setDefaults([
'max_restarts' => 10,
'debug_plugins' => [],
]);
$resolver
->setAllowedTypes('debug_plugins', 'array')
->setAllowedValues('debug_plugins', function (array $plugins) {
foreach ($plugins as $plugin) {
// Make sure each object passed with the `debug_plugins` is an instance of Plugin.
if (!$plugin instanceof Plugin) {
return false;
}
}
return true;
});
return $resolver->resolve($options);
}
/**
* Create the plugin chain.
*
* @param Plugin[] $pluginList A list of plugins
* @param callable $clientCallable Callable making the HTTP call
*
* @return callable
*/
private function createPluginChain($pluginList, callable $clientCallable)
{
$firstCallable = $lastCallable = $clientCallable;
/*
* Inject debug plugins between each plugin.
*/
$pluginListWithDebug = $this->options['debug_plugins'];
foreach ($pluginList as $plugin) {
$pluginListWithDebug[] = $plugin;
$pluginListWithDebug = array_merge($pluginListWithDebug, $this->options['debug_plugins']);
}
while ($plugin = array_pop($pluginListWithDebug)) {
$lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable, &$firstCallable) {
return $plugin->handleRequest($request, $lastCallable, $firstCallable);
};
$firstCallable = $lastCallable;
}
$firstCalls = 0;
$firstCallable = function (RequestInterface $request) use ($lastCallable, &$firstCalls) {
if ($firstCalls > $this->options['max_restarts']) {
throw new LoopException('Too many restarts in plugin client', $request);
}
++$firstCalls;
return $lastCallable($request);
};
return $firstCallable;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
/**
* Factory to create PluginClient instances. Using this factory instead of calling PluginClient constructor will enable
* the Symfony profiling without any configuration.
*
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class PluginClientFactory
{
/**
* @var callable
*/
private static $factory;
/**
* Set the factory to use.
* The callable to provide must have the same arguments and return type as PluginClientFactory::createClient.
* This is used by the HTTPlugBundle to provide a better Symfony integration.
* Unlike the createClient method, this one is static to allow zero configuration profiling by hooking into early
* application execution.
*
* @internal
*
* @param callable $factory
*/
public static function setFactory(callable $factory)
{
static::$factory = $factory;
}
/**
* @param HttpClient|HttpAsyncClient|ClientInterface $client
* @param Plugin[] $plugins
* @param array $options {
*
* @var string $client_name to give client a name which may be used when displaying client information like in
* the HTTPlugBundle profiler.
* }
*
* @see PluginClient constructor for PluginClient specific $options.
*
* @return PluginClient
*/
public function createClient($client, array $plugins = [], array $options = [])
{
if (static::$factory) {
$factory = static::$factory;
return $factory($client, $plugins, $options);
}
unset($options['client_name']);
return new PluginClient($client, $plugins, $options);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Http\Client\Common;
use Psr\Http\Message\RequestInterface;
/**
* A client that helps you migrate from php-http/httplug 1.x to 2.x. This
* will also help you to support PHP5 at the same time you support 2.x.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
trait VersionBridgeClient
{
abstract protected function doSendRequest(RequestInterface $request);
public function sendRequest(RequestInterface $request)
{
return $this->doSendRequest($request);
}
}