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);
}
}

View File

@@ -0,0 +1,9 @@
<?php
return Symfony\CS\Config\Config::create()
->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
->fixers([])
->finder(
Symfony\CS\Finder\DefaultFinder::create()->in(__DIR__ . '/src')
)
;

View File

@@ -0,0 +1,4 @@
preset: psr2
finder:
path:
- "src"

View File

@@ -0,0 +1,173 @@
# Change Log
## Unreleased
## 1.7.1 - 2018-03-36
### Fixed
- #36: Failure evaluating code: is_resource($handle) (string assertions are deprecated in PHP 7.2)
## 1.7 - 2017-02-09
### Changed
- #30: Make sure we rewind streams
## 1.6.2 - 2017-01-02
### Fixed
- #29: Request not using CURLOPT_POSTFIELDS have content-length set to
### Changed
- Use binary mode to create response body stream.
## 1.6.1 - 2016-11-11
### Fixed
- #27: ErrorPlugin and sendAsyncRequest() incompatibility
## 1.6 - 2016-09-12
### Changed
- `Client::sendRequest` now throws `Http\Client\Exception\NetworkException` on network errors.
- `\UnexpectedValueException` replaced with `Http\Client\Exception\RequestException` in
`Client::sendRequest` and `Client::sendAsyncRequest`
## 1.5.1 - 2016-08-29
### Fixed
- #26: Combining CurlClient with StopwatchPlugin causes Promise onRejected handler to never be
invoked.
## 1.5 - 2016-08-03
### Changed
- Request body can be send with any method except GET, HEAD and TRACE.
- #25: Make discovery a hard dependency.
## 1.4.2 - 2016-06-14
### Added
- #23: "php-http/async-client-implementation" added to "provide" section.
## 1.4.1 - 2016-05-30
### Fixed
- #22: Cannot create the client using `HttpClientDiscovery`.
## 1.4 - 2016-03-30
### Changed
- #20: Minimize memory usage when reading large response body.
## 1.3 - 2016-03-14
### Fixed
- #18: Invalid "Expect" header.
### Removed
- #13: Remove HeaderParser.
## 1.2 - 2016-03-09
### Added
- #16: Make sure discovery can find the curl client
### Fixed
- #15: "Out of memory" sending large files.
## 1.1.0 - 2016-01-29
### Changed
- Switch to php-http/message 1.0.
## 1.0.0 - 2016-01-28
First stable release.
## 0.7.0 - 2016-01-26
### Changed
- Migrate from `php-http/discovery` and `php-http/utils` to `php-http/message`.
## 0.6.0 - 2016-01-12
### Changed
- Root namespace changed from `Http\Curl` to `Http\Client\Curl`.
- Main client class name renamed from `CurlHttpClient` to `Client`.
- Minimum required [php-http/discovery](https://packagist.org/packages/php-http/discovery)
version changed to 0.5.
## 0.5.0 - 2015-12-18
### Changed
- Compatibility with php-http/httplug 1.0 beta
- Switch to php-http/discovery 0.4
## 0.4.0 - 2015-12-16
### Changed
- Switch to php-http/message-factory 1.0
## 0.3.1 - 2015-12-14
### Changed
- Requirements fixed.
## 0.3.0 - 2015-11-24
### Changed
- Use cURL constants as options keys.
## 0.2.0 - 2015-11-17
### Added
- HttpAsyncClient support.
## 0.1.0 - 2015-11-11
### Added
- Initial release

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015 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,44 @@
# Curl client for PHP HTTP
[![Latest Version](https://img.shields.io/github/release/php-http/curl-client.svg?style=flat-square)](https://github.com/php-http/curl-client/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/curl-client.svg?style=flat-square)](https://travis-ci.org/php-http/curl-client)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/curl-client.svg?style=flat-square)](https://packagist.org/packages/php-http/curl-client)
The cURL client use the cURL PHP extension which must be activated in your `php.ini`.
## Install
Via Composer
``` bash
$ composer require php-http/curl-client
```
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/clients/curl-client.html).
## Testing
``` bash
$ composer test
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details.
## 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,50 @@
{
"name": "php-http/curl-client",
"description": "cURL client for PHP-HTTP",
"license": "MIT",
"keywords": ["http", "curl"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Михаил Красильников",
"email": "m.krasilnikov@yandex.ru"
}
],
"prefer-stable": true,
"minimum-stability": "beta",
"config": {
"bin-dir": "vendor/bin"
},
"require": {
"php": "^5.5 || ^7.0",
"ext-curl": "*",
"php-http/httplug": "^1.0",
"php-http/message-factory": "^1.0.2",
"php-http/message": "^1.2",
"php-http/discovery": "^1.0"
},
"require-dev": {
"guzzlehttp/psr7": "^1.0",
"php-http/client-integration-tests": "^0.6",
"phpunit/phpunit": "^4.8.27",
"zendframework/zend-diactoros": "^1.0"
},
"autoload": {
"psr-4": {
"Http\\Client\\Curl\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Http\\Client\\Curl\\Tests\\": "tests/"
}
},
"provide": {
"php-http/client-implementation": "1.0",
"php-http/async-client-implementation": "1.0"
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml"
}
}

View File

@@ -0,0 +1,242 @@
{
"version": "1.0",
"name": "php-http/curl-client",
"bindings": {
"98239b8b-103b-4f47-94c7-4cba49a05a1f": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Client\\Curl\\Client",
"type": "Http\\Client\\HttpAsyncClient"
},
"a6a79968-2aa5-427c-bbe1-a581d9a48321": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Client\\Curl\\Client",
"type": "Http\\Client\\HttpClient"
}
},
"config": {
"bootstrap-file": "vendor/autoload.php"
},
"packages": {
"clue/stream-filter": {
"install-path": "vendor/clue/stream-filter",
"installer": "composer",
"env": "dev"
},
"doctrine/instantiator": {
"install-path": "vendor/doctrine/instantiator",
"installer": "composer",
"env": "dev"
},
"guzzlehttp/psr7": {
"install-path": "vendor/guzzlehttp/psr7",
"installer": "composer",
"env": "dev"
},
"justinrainbow/json-schema": {
"install-path": "vendor/justinrainbow/json-schema",
"installer": "composer",
"env": "dev"
},
"paragonie/random_compat": {
"install-path": "vendor/paragonie/random_compat",
"installer": "composer",
"env": "dev"
},
"php-http/adapter-integration-tests": {
"install-path": "vendor/php-http/adapter-integration-tests",
"installer": "composer",
"env": "dev"
},
"php-http/discovery": {
"install-path": "vendor/php-http/discovery",
"installer": "composer",
"env": "dev"
},
"php-http/httplug": {
"install-path": "vendor/php-http/httplug",
"installer": "composer"
},
"php-http/message": {
"install-path": "vendor/php-http/message",
"installer": "composer",
"env": "dev"
},
"php-http/message-factory": {
"install-path": "vendor/php-http/message-factory",
"installer": "composer"
},
"php-http/promise": {
"install-path": "vendor/php-http/promise",
"installer": "composer"
},
"phpdocumentor/reflection-docblock": {
"install-path": "vendor/phpdocumentor/reflection-docblock",
"installer": "composer",
"env": "dev"
},
"phpspec/prophecy": {
"install-path": "vendor/phpspec/prophecy",
"installer": "composer",
"env": "dev"
},
"phpunit/php-code-coverage": {
"install-path": "vendor/phpunit/php-code-coverage",
"installer": "composer",
"env": "dev"
},
"phpunit/php-file-iterator": {
"install-path": "vendor/phpunit/php-file-iterator",
"installer": "composer",
"env": "dev"
},
"phpunit/php-text-template": {
"install-path": "vendor/phpunit/php-text-template",
"installer": "composer",
"env": "dev"
},
"phpunit/php-timer": {
"install-path": "vendor/phpunit/php-timer",
"installer": "composer",
"env": "dev"
},
"phpunit/php-token-stream": {
"install-path": "vendor/phpunit/php-token-stream",
"installer": "composer",
"env": "dev"
},
"phpunit/phpunit": {
"install-path": "vendor/phpunit/phpunit",
"installer": "composer",
"env": "dev"
},
"phpunit/phpunit-mock-objects": {
"install-path": "vendor/phpunit/phpunit-mock-objects",
"installer": "composer",
"env": "dev"
},
"psr/http-message": {
"install-path": "vendor/psr/http-message",
"installer": "composer"
},
"psr/log": {
"install-path": "vendor/psr/log",
"installer": "composer",
"env": "dev"
},
"puli/composer-plugin": {
"install-path": "vendor/puli/composer-plugin",
"installer": "composer",
"env": "dev"
},
"puli/discovery": {
"install-path": "vendor/puli/discovery",
"installer": "composer",
"env": "dev"
},
"puli/repository": {
"install-path": "vendor/puli/repository",
"installer": "composer",
"env": "dev"
},
"puli/url-generator": {
"install-path": "vendor/puli/url-generator",
"installer": "composer",
"env": "dev"
},
"ramsey/uuid": {
"install-path": "vendor/ramsey/uuid",
"installer": "composer",
"env": "dev"
},
"sebastian/comparator": {
"install-path": "vendor/sebastian/comparator",
"installer": "composer",
"env": "dev"
},
"sebastian/diff": {
"install-path": "vendor/sebastian/diff",
"installer": "composer",
"env": "dev"
},
"sebastian/environment": {
"install-path": "vendor/sebastian/environment",
"installer": "composer",
"env": "dev"
},
"sebastian/exporter": {
"install-path": "vendor/sebastian/exporter",
"installer": "composer",
"env": "dev"
},
"sebastian/global-state": {
"install-path": "vendor/sebastian/global-state",
"installer": "composer",
"env": "dev"
},
"sebastian/recursion-context": {
"install-path": "vendor/sebastian/recursion-context",
"installer": "composer",
"env": "dev"
},
"sebastian/version": {
"install-path": "vendor/sebastian/version",
"installer": "composer",
"env": "dev"
},
"seld/jsonlint": {
"install-path": "vendor/seld/jsonlint",
"installer": "composer",
"env": "dev"
},
"symfony/filesystem": {
"install-path": "vendor/symfony/filesystem",
"installer": "composer",
"env": "dev"
},
"symfony/process": {
"install-path": "vendor/symfony/process",
"installer": "composer",
"env": "dev"
},
"symfony/yaml": {
"install-path": "vendor/symfony/yaml",
"installer": "composer",
"env": "dev"
},
"th3n3rd/cartesian-product": {
"install-path": "vendor/th3n3rd/cartesian-product",
"installer": "composer",
"env": "dev"
},
"webmozart/assert": {
"install-path": "vendor/webmozart/assert",
"installer": "composer",
"env": "dev"
},
"webmozart/expression": {
"install-path": "vendor/webmozart/expression",
"installer": "composer",
"env": "dev"
},
"webmozart/glob": {
"install-path": "vendor/webmozart/glob",
"installer": "composer",
"env": "dev"
},
"webmozart/json": {
"install-path": "vendor/webmozart/json",
"installer": "composer",
"env": "dev"
},
"webmozart/path-util": {
"install-path": "vendor/webmozart/path-util",
"installer": "composer",
"env": "dev"
},
"zendframework/zend-diactoros": {
"install-path": "vendor/zendframework/zend-diactoros",
"installer": "composer",
"env": "dev"
}
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
use Http\Message\MessageFactory;
use Http\Message\StreamFactory;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* PSR-7 compatible cURL based HTTP client.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
* @author Blake Williams <github@shabbyrobe.org>
*
* @api
*
* @since 1.0
*/
class Client implements HttpClient, HttpAsyncClient
{
/**
* cURL options.
*
* @var array
*/
private $options;
/**
* PSR-7 message factory.
*
* @var MessageFactory
*/
private $messageFactory;
/**
* PSR-7 stream factory.
*
* @var StreamFactory
*/
private $streamFactory;
/**
* cURL synchronous requests handle.
*
* @var resource|null
*/
private $handle = null;
/**
* Simultaneous requests runner.
*
* @var MultiRunner|null
*/
private $multiRunner = null;
/**
* Create new client.
*
* @param MessageFactory|null $messageFactory HTTP Message factory
* @param StreamFactory|null $streamFactory HTTP Stream factory
* @param array $options cURL options (see http://php.net/curl_setopt)
*
* @throws \Http\Discovery\Exception\NotFoundException If factory discovery failed
*
* @since 1.0
*/
public function __construct(
MessageFactory $messageFactory = null,
StreamFactory $streamFactory = null,
array $options = []
) {
$this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find();
$this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
$this->options = $options;
}
/**
* Release resources if still active.
*/
public function __destruct()
{
if (is_resource($this->handle)) {
curl_close($this->handle);
}
}
/**
* Sends a PSR-7 request.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws \Http\Client\Exception\NetworkException In case of network problems
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If creating the body stream fails
*
* @since 1.6 \UnexpectedValueException replaced with RequestException
* @since 1.6 Throw NetworkException on network errors
* @since 1.0
*/
public function sendRequest(RequestInterface $request)
{
$responseBuilder = $this->createResponseBuilder();
$options = $this->createCurlOptions($request, $responseBuilder);
if (is_resource($this->handle)) {
curl_reset($this->handle);
} else {
$this->handle = curl_init();
}
curl_setopt_array($this->handle, $options);
curl_exec($this->handle);
$errno = curl_errno($this->handle);
switch ($errno) {
case CURLE_OK:
// All OK, no actions needed.
break;
case CURLE_COULDNT_RESOLVE_PROXY:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_COULDNT_CONNECT:
case CURLE_OPERATION_TIMEOUTED:
case CURLE_SSL_CONNECT_ERROR:
throw new Exception\NetworkException(curl_error($this->handle), $request);
default:
throw new Exception\RequestException(curl_error($this->handle), $request);
}
$response = $responseBuilder->getResponse();
$response->getBody()->seek(0);
return $response;
}
/**
* Sends a PSR-7 request in an asynchronous way.
*
* @param RequestInterface $request
*
* @return Promise
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If creating the body stream fails
*
* @since 1.6 \UnexpectedValueException replaced with RequestException
* @since 1.0
*/
public function sendAsyncRequest(RequestInterface $request)
{
if (!$this->multiRunner instanceof MultiRunner) {
$this->multiRunner = new MultiRunner();
}
$handle = curl_init();
$responseBuilder = $this->createResponseBuilder();
$options = $this->createCurlOptions($request, $responseBuilder);
curl_setopt_array($handle, $options);
$core = new PromiseCore($request, $handle, $responseBuilder);
$promise = new CurlPromise($core, $this->multiRunner);
$this->multiRunner->add($core);
return $promise;
}
/**
* Generates cURL options.
*
* @param RequestInterface $request
* @param ResponseBuilder $responseBuilder
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException if can not read body
*
* @return array
*/
private function createCurlOptions(RequestInterface $request, ResponseBuilder $responseBuilder)
{
$options = $this->options;
$options[CURLOPT_HEADER] = false;
$options[CURLOPT_RETURNTRANSFER] = false;
$options[CURLOPT_FOLLOWLOCATION] = false;
try {
$options[CURLOPT_HTTP_VERSION]
= $this->getProtocolVersion($request->getProtocolVersion());
} catch (\UnexpectedValueException $e) {
throw new Exception\RequestException($e->getMessage(), $request);
}
$options[CURLOPT_URL] = (string) $request->getUri();
$options = $this->addRequestBodyOptions($request, $options);
$options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options);
if ($request->getUri()->getUserInfo()) {
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
}
$options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) {
$str = trim($data);
if ('' !== $str) {
if (strpos(strtolower($str), 'http/') === 0) {
$responseBuilder->setStatus($str)->getResponse();
} else {
$responseBuilder->addHeader($str);
}
}
return strlen($data);
};
$options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) {
return $responseBuilder->getResponse()->getBody()->write($data);
};
return $options;
}
/**
* Return cURL constant for specified HTTP version.
*
* @param string $requestVersion
*
* @throws \UnexpectedValueException if unsupported version requested
*
* @return int
*/
private function getProtocolVersion($requestVersion)
{
switch ($requestVersion) {
case '1.0':
return CURL_HTTP_VERSION_1_0;
case '1.1':
return CURL_HTTP_VERSION_1_1;
case '2.0':
if (defined('CURL_HTTP_VERSION_2_0')) {
return CURL_HTTP_VERSION_2_0;
}
throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support');
}
return CURL_HTTP_VERSION_NONE;
}
/**
* Add request body related cURL options.
*
* @param RequestInterface $request
* @param array $options
*
* @return array
*/
private function addRequestBodyOptions(RequestInterface $request, array $options)
{
/*
* Some HTTP methods cannot have payload:
*
* - GET — cURL will automatically change method to PUT or POST if we set CURLOPT_UPLOAD or
* CURLOPT_POSTFIELDS.
* - HEAD — cURL treats HEAD as GET request with a same restrictions.
* - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
*/
if (!in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE'], true)) {
$body = $request->getBody();
$bodySize = $body->getSize();
if ($bodySize !== 0) {
if ($body->isSeekable()) {
$body->rewind();
}
// Message has non empty body.
if (null === $bodySize || $bodySize > 1024 * 1024) {
// Avoid full loading large or unknown size body into memory
$options[CURLOPT_UPLOAD] = true;
if (null !== $bodySize) {
$options[CURLOPT_INFILESIZE] = $bodySize;
}
$options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return $body->read($length);
};
} else {
// Small body can be loaded into memory
$options[CURLOPT_POSTFIELDS] = (string) $body;
}
}
}
if ($request->getMethod() === 'HEAD') {
// This will set HTTP method to "HEAD".
$options[CURLOPT_NOBODY] = true;
} elseif ($request->getMethod() !== 'GET') {
// GET is a default method. Other methods should be specified explicitly.
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
}
return $options;
}
/**
* Create headers array for CURLOPT_HTTPHEADER.
*
* @param RequestInterface $request
* @param array $options cURL options
*
* @return string[]
*/
private function createHeaders(RequestInterface $request, array $options)
{
$curlHeaders = [];
$headers = $request->getHeaders();
foreach ($headers as $name => $values) {
$header = strtolower($name);
if ('expect' === $header) {
// curl-client does not support "Expect-Continue", so dropping "expect" headers
continue;
}
if ('content-length' === $header) {
if (array_key_exists(CURLOPT_POSTFIELDS, $options)) {
// Small body content length can be calculated here.
$values = [strlen($options[CURLOPT_POSTFIELDS])];
} elseif (!array_key_exists(CURLOPT_READFUNCTION, $options)) {
// Else if there is no body, forcing "Content-length" to 0
$values = [0];
}
}
foreach ($values as $value) {
$curlHeaders[] = $name.': '.$value;
}
}
/*
* curl-client does not support "Expect-Continue", but cURL adds "Expect" header by default.
* We can not suppress it, but we can set it to empty.
*/
$curlHeaders[] = 'Expect:';
return $curlHeaders;
}
/**
* Create new ResponseBuilder instance.
*
* @return ResponseBuilder
*
* @throws \RuntimeException If creating the stream from $body fails
*/
private function createResponseBuilder()
{
try {
$body = $this->streamFactory->createStream(fopen('php://temp', 'w+b'));
} catch (\InvalidArgumentException $e) {
throw new \RuntimeException('Can not create "php://temp" stream.');
}
$response = $this->messageFactory->createResponse(200, null, [], $body);
return new ResponseBuilder($response);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Http\Client\Curl;
use Http\Promise\Promise;
/**
* Promise represents a response that may not be available yet, but will be resolved at some point
* in future. It acts like a proxy to the actual response.
*
* This interface is an extension of the promises/a+ specification https://promisesaplus.com/
* Value is replaced by an object where its class implement a Psr\Http\Message\RequestInterface.
* Reason is replaced by an object where its class implement a Http\Client\Exception.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class CurlPromise implements Promise
{
/**
* Shared promise core.
*
* @var PromiseCore
*/
private $core;
/**
* Requests runner.
*
* @var MultiRunner
*/
private $runner;
/**
* Create new promise.
*
* @param PromiseCore $core Shared promise core
* @param MultiRunner $runner Simultaneous requests runner
*/
public function __construct(PromiseCore $core, MultiRunner $runner)
{
$this->core = $core;
$this->runner = $runner;
}
/**
* Add behavior for when the promise is resolved or rejected.
*
* If you do not care about one of the cases, you can set the corresponding callable to null
* The callback will be called when the response or exception arrived and never more than once.
*
* @param callable $onFulfilled Called when a response will be available
* @param callable $onRejected Called when an error happens.
*
* You must always return the Response in the interface or throw an Exception
*
* @return Promise Always returns a new promise which is resolved with value of the executed
* callback (onFulfilled / onRejected)
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
if ($onFulfilled) {
$this->core->addOnFulfilled($onFulfilled);
}
if ($onRejected) {
$this->core->addOnRejected($onRejected);
}
return new self($this->core, $this->runner);
}
/**
* Get the state of the promise, one of PENDING, FULFILLED or REJECTED.
*
* @return string
*/
public function getState()
{
return $this->core->getState();
}
/**
* Wait for the promise to be fulfilled or rejected.
*
* When this method returns, the request has been resolved and the appropriate callable has terminated.
*
* When called with the unwrap option
*
* @param bool $unwrap Whether to return resolved value / throw reason or not
*
* @return \Psr\Http\Message\ResponseInterface|null Resolved value, null if $unwrap is set to false
*
* @throws \Http\Client\Exception The rejection reason
*/
public function wait($unwrap = true)
{
$this->runner->wait($this->core);
if ($unwrap) {
if ($this->core->getState() === self::REJECTED) {
throw $this->core->getException();
}
return $this->core->getResponse();
}
return null;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception\RequestException;
/**
* Simultaneous requests runner.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class MultiRunner
{
/**
* cURL multi handle.
*
* @var resource|null
*/
private $multiHandle = null;
/**
* Awaiting cores.
*
* @var PromiseCore[]
*/
private $cores = [];
/**
* Release resources if still active.
*/
public function __destruct()
{
if (is_resource($this->multiHandle)) {
curl_multi_close($this->multiHandle);
}
}
/**
* Add promise to runner.
*
* @param PromiseCore $core
*/
public function add(PromiseCore $core)
{
foreach ($this->cores as $existed) {
if ($existed === $core) {
return;
}
}
$this->cores[] = $core;
if (null === $this->multiHandle) {
$this->multiHandle = curl_multi_init();
}
curl_multi_add_handle($this->multiHandle, $core->getHandle());
}
/**
* Remove promise from runner.
*
* @param PromiseCore $core
*/
public function remove(PromiseCore $core)
{
foreach ($this->cores as $index => $existed) {
if ($existed === $core) {
curl_multi_remove_handle($this->multiHandle, $core->getHandle());
unset($this->cores[$index]);
return;
}
}
}
/**
* Wait for request(s) to be completed.
*
* @param PromiseCore|null $targetCore
*/
public function wait(PromiseCore $targetCore = null)
{
do {
$status = curl_multi_exec($this->multiHandle, $active);
$info = curl_multi_info_read($this->multiHandle);
if (false !== $info) {
$core = $this->findCoreByHandle($info['handle']);
if (null === $core) {
// We have no promise for this handle. Drop it.
curl_multi_remove_handle($this->multiHandle, $info['handle']);
continue;
}
if (CURLE_OK === $info['result']) {
$core->fulfill();
} else {
$error = curl_error($core->getHandle());
$core->reject(new RequestException($error, $core->getRequest()));
}
$this->remove($core);
// This is a promise we are waited for. So exiting wait().
if ($core === $targetCore) {
return;
}
}
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
}
/**
* Find core by handle.
*
* @param resource $handle
*
* @return PromiseCore|null
*/
private function findCoreByHandle($handle)
{
foreach ($this->cores as $core) {
if ($core->getHandle() === $handle) {
return $core;
}
}
return null;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Shared promises core.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class PromiseCore
{
/**
* HTTP request.
*
* @var RequestInterface
*/
private $request;
/**
* cURL handle.
*
* @var resource
*/
private $handle;
/**
* Response builder.
*
* @var ResponseBuilder
*/
private $responseBuilder;
/**
* Promise state.
*
* @var string
*/
private $state;
/**
* Exception.
*
* @var Exception|null
*/
private $exception = null;
/**
* Functions to call when a response will be available.
*
* @var callable[]
*/
private $onFulfilled = [];
/**
* Functions to call when an error happens.
*
* @var callable[]
*/
private $onRejected = [];
/**
* Create shared core.
*
* @param RequestInterface $request HTTP request.
* @param resource $handle cURL handle.
* @param ResponseBuilder $responseBuilder Response builder.
*
* @throws \InvalidArgumentException If $handle is not a cURL resource.
*/
public function __construct(
RequestInterface $request,
$handle,
ResponseBuilder $responseBuilder
) {
if (!is_resource($handle)) {
throw new \InvalidArgumentException(
sprintf(
'Parameter $handle expected to be a cURL resource, %s given',
gettype($handle)
)
);
}
if (get_resource_type($handle) !== 'curl') {
throw new \InvalidArgumentException(
sprintf(
'Parameter $handle expected to be a cURL resource, %s resource given',
get_resource_type($handle)
)
);
}
$this->request = $request;
$this->handle = $handle;
$this->responseBuilder = $responseBuilder;
$this->state = Promise::PENDING;
}
/**
* Add on fulfilled callback.
*
* @param callable $callback
*/
public function addOnFulfilled(callable $callback)
{
if ($this->getState() === Promise::PENDING) {
$this->onFulfilled[] = $callback;
} elseif ($this->getState() === Promise::FULFILLED) {
$response = call_user_func($callback, $this->responseBuilder->getResponse());
if ($response instanceof ResponseInterface) {
$this->responseBuilder->setResponse($response);
}
}
}
/**
* Add on rejected callback.
*
* @param callable $callback
*/
public function addOnRejected(callable $callback)
{
if ($this->getState() === Promise::PENDING) {
$this->onRejected[] = $callback;
} elseif ($this->getState() === Promise::REJECTED) {
$this->exception = call_user_func($callback, $this->exception);
}
}
/**
* Return cURL handle.
*
* @return resource
*/
public function getHandle()
{
return $this->handle;
}
/**
* Get the state of the promise, one of PENDING, FULFILLED or REJECTED.
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* Return request.
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
/**
* Return the value of the promise (fulfilled).
*
* @return ResponseInterface Response Object only when the Promise is fulfilled
*/
public function getResponse()
{
return $this->responseBuilder->getResponse();
}
/**
* Get the reason why the promise was rejected.
*
* If the exception is an instance of Http\Client\Exception\HttpException it will contain
* the response object with the status code and the http reason.
*
* @return Exception Exception Object only when the Promise is rejected
*
* @throws \LogicException When the promise is not rejected
*/
public function getException()
{
if (null === $this->exception) {
throw new \LogicException('Promise is not rejected');
}
return $this->exception;
}
/**
* Fulfill promise.
*/
public function fulfill()
{
$this->state = Promise::FULFILLED;
$response = $this->responseBuilder->getResponse();
try {
$response->getBody()->seek(0);
} catch (\RuntimeException $e) {
$exception = new Exception\TransferException($e->getMessage(), $e->getCode(), $e);
$this->reject($exception);
return;
}
while (count($this->onFulfilled) > 0) {
$callback = array_shift($this->onFulfilled);
$response = call_user_func($callback, $response);
}
if ($response instanceof ResponseInterface) {
$this->responseBuilder->setResponse($response);
}
}
/**
* Reject promise.
*
* @param Exception $exception Reject reason
*/
public function reject(Exception $exception)
{
$this->exception = $exception;
$this->state = Promise::REJECTED;
while (count($this->onRejected) > 0) {
$callback = array_shift($this->onRejected);
try {
$exception = call_user_func($callback, $this->exception);
$this->exception = $exception;
} catch (Exception $exception) {
$this->exception = $exception;
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Http\Client\Curl;
use Http\Message\Builder\ResponseBuilder as OriginalResponseBuilder;
use Psr\Http\Message\ResponseInterface;
/**
* Extended response builder.
*/
class ResponseBuilder extends OriginalResponseBuilder
{
/**
* Replace response with a new instance.
*
* @param ResponseInterface $response
*/
public function setResponse(ResponseInterface $response)
{
$this->response = $response;
}
}

View File

@@ -0,0 +1,236 @@
# Change Log
## 1.6.1 - 2019-02-23
### Fixed
- MockClientStrategy also provides the mock client when requesting an async client
## 1.6.0 - 2019-01-23
### Added
- Support for PSR-17 factories
- Support for PSR-18 clients
## 1.5.2 - 2018-12-31
Corrected mistakes in 1.5.1. The different between 1.5.2 and 1.5.0 is that
we removed some PHP 7 code.
https://github.com/php-http/discovery/compare/1.5.0...1.5.2
## 1.5.1 - 2018-12-31
This version added new features by mistake. These are reverted in 1.5.2.
Do not use 1.5.1.
### Fixed
- Removed PHP 7 code
## 1.5.0 - 2018-12-30
### Added
- Support for `nyholm/psr7` version 1.0.
- `ClassDiscovery::safeClassExists` which will help Magento users.
- Support for HTTPlug 2.0
- Support for Buzz 1.0
- Better error message when nothing found by introducing a new exception: `NoCandidateFoundException`.
### Fixed
- Fixed condition evaluation, it should stop after first invalid condition.
## 1.4.0 - 2018-02-06
### Added
- Discovery support for nyholm/psr7
## 1.3.0 - 2017-08-03
### Added
- Discovery support for CakePHP adapter
- Discovery support for Zend adapter
- Discovery support for Artax adapter
## 1.2.1 - 2017-03-02
### Fixed
- Fixed minor issue with `MockClientStrategy`, also added more tests.
## 1.2.0 - 2017-02-12
### Added
- MockClientStrategy class.
## 1.1.1 - 2016-11-27
### Changed
- Made exception messages clearer. `StrategyUnavailableException` is no longer the previous exception to `DiscoveryFailedException`.
- `CommonClassesStrategy` is using `self` instead of `static`. Using `static` makes no sense when `CommonClassesStrategy` is final.
## 1.1.0 - 2016-10-20
### Added
- Discovery support for Slim Framework factories
## 1.0.0 - 2016-07-18
### Added
- Added back `Http\Discovery\NotFoundException` to preserve BC with 0.8 version. You may upgrade from 0.8.x and 0.9.x to 1.0.0 without any BC breaks.
- Added interface `Http\Discovery\Exception` which is implemented by all our exceptions
### Changed
- Puli strategy renamed to Puli Beta strategy to prevent incompatibility with a future Puli stable
### Deprecated
- For BC reasons, the old `Http\Discovery\NotFoundException` (extending the new exception) will be thrown until version 2.0
## 0.9.1 - 2016-06-28
### Changed
- Dropping PHP 5.4 support because we use the ::class constant.
## 0.9.0 - 2016-06-25
### Added
- Discovery strategies to find classes
### Changed
- [Puli](http://puli.io) made optional
- Improved exceptions
- **[BC] `NotFoundException` moved to `Http\Discovery\Exception\NotFoundException`**
## 0.8.0 - 2016-02-11
### Changed
- Puli composer plugin must be installed separately
## 0.7.0 - 2016-01-15
### Added
- Temporary puli.phar (Beta 10) executable
### Changed
- Updated HTTPlug dependencies
- Updated Puli dependencies
- Local configuration to make tests passing
### Removed
- Puli CLI dependency
## 0.6.4 - 2016-01-07
### Fixed
- Puli [not working](https://twitter.com/PuliPHP/status/685132540588507137) with the latest json-schema
## 0.6.3 - 2016-01-04
### Changed
- Adjust Puli dependencies
## 0.6.2 - 2016-01-04
### Changed
- Make Puli CLI a requirement
## 0.6.1 - 2016-01-03
### Changed
- More flexible Puli requirement
## 0.6.0 - 2015-12-30
### Changed
- Use [Puli](http://puli.io) for discovery
- Improved exception messages
## 0.5.0 - 2015-12-25
### Changed
- Updated message factory dependency (php-http/message)
## 0.4.0 - 2015-12-17
### Added
- Array condition evaluation in the Class Discovery
### Removed
- Message factories (moved to php-http/utils)
## 0.3.0 - 2015-11-18
### Added
- HTTP Async Client Discovery
- Stream factories
### Changed
- Discoveries and Factories are final
- Message and Uri factories have the type in their names
- Diactoros Message factory uses Stream factory internally
### Fixed
- Improved docblocks for API documentation generation
## 0.2.0 - 2015-10-31
### Changed
- Renamed AdapterDiscovery to ClientDiscovery
## 0.1.1 - 2015-06-13
### Fixed
- Bad HTTP Adapter class name for Guzzle 5
## 0.1.0 - 2015-06-12
### Added
- Initial release

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,46 @@
# HTTPlug Discovery
[![Latest Version](https://img.shields.io/github/release/php-http/discovery.svg?style=flat-square)](https://github.com/php-http/discovery/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/discovery/master.svg?style=flat-square)](https://travis-ci.org/php-http/discovery)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/discovery.svg?style=flat-square)](https://packagist.org/packages/php-http/discovery)
**Finds installed HTTPlug implementations and PSR-7 message factories.**
## Install
Via Composer
``` bash
$ composer require php-http/discovery
```
## Documentation
Please see the [official documentation](http://php-http.readthedocs.org/en/latest/discovery.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,50 @@
{
"name": "php-http/discovery",
"description": "Finds installed HTTPlug implementations and PSR-7 message factories",
"license": "MIT",
"keywords": ["http", "discovery", "client", "adapter", "message", "factory", "psr7"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": "^5.5 || ^7.0"
},
"require-dev": {
"php-http/httplug": "^1.0 || ^2.0",
"php-http/message-factory": "^1.0",
"puli/composer-plugin": "1.0.0-beta10",
"phpspec/phpspec": "^2.4"
},
"suggest": {
"puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details.",
"php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories"
},
"autoload": {
"psr-4": {
"Http\\Discovery\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"spec\\Http\\Discovery\\": "spec/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
},
"extra": {
"branch-alias": {
"dev-master": "1.5-dev"
}
},
"conflict": {
"nyholm/psr7": "<1.0"
},
"prefer-stable": true,
"minimum-stability": "beta"
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\ClassInstantiationFailedException;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Discovery\Exception\NoCandidateFoundException;
use Http\Discovery\Exception\StrategyUnavailableException;
/**
* Registry that based find results on class existence.
*
* @author David de Boer <david@ddeboer.nl>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
abstract class ClassDiscovery
{
/**
* A list of strategies to find classes.
*
* @var array
*/
private static $strategies = [
Strategy\PuliBetaStrategy::class,
Strategy\CommonClassesStrategy::class,
Strategy\CommonPsr17ClassesStrategy::class,
];
/**
* Discovery cache to make the second time we use discovery faster.
*
* @var array
*/
private static $cache = [];
/**
* Finds a class.
*
* @param string $type
*
* @return string|\Closure
*
* @throws DiscoveryFailedException
*/
protected static function findOneByType($type)
{
// Look in the cache
if (null !== ($class = self::getFromCache($type))) {
return $class;
}
$exceptions = [];
foreach (self::$strategies as $strategy) {
try {
$candidates = call_user_func($strategy.'::getCandidates', $type);
} catch (StrategyUnavailableException $e) {
$exceptions[] = $e;
continue;
}
foreach ($candidates as $candidate) {
if (isset($candidate['condition'])) {
if (!self::evaluateCondition($candidate['condition'])) {
continue;
}
}
// save the result for later use
self::storeInCache($type, $candidate);
return $candidate['class'];
}
$exceptions[] = new NoCandidateFoundException($strategy, $candidates);
}
throw DiscoveryFailedException::create($exceptions);
}
/**
* Get a value from cache.
*
* @param string $type
*
* @return string|null
*/
private static function getFromCache($type)
{
if (!isset(self::$cache[$type])) {
return;
}
$candidate = self::$cache[$type];
if (isset($candidate['condition'])) {
if (!self::evaluateCondition($candidate['condition'])) {
return;
}
}
return $candidate['class'];
}
/**
* Store a value in cache.
*
* @param string $type
* @param string $class
*/
private static function storeInCache($type, $class)
{
self::$cache[$type] = $class;
}
/**
* Set new strategies and clear the cache.
*
* @param array $strategies string array of fully qualified class name to a DiscoveryStrategy
*/
public static function setStrategies(array $strategies)
{
self::$strategies = $strategies;
self::clearCache();
}
/**
* Append a strategy at the end of the strategy queue.
*
* @param string $strategy Fully qualified class name to a DiscoveryStrategy
*/
public static function appendStrategy($strategy)
{
self::$strategies[] = $strategy;
self::clearCache();
}
/**
* Prepend a strategy at the beginning of the strategy queue.
*
* @param string $strategy Fully qualified class name to a DiscoveryStrategy
*/
public static function prependStrategy($strategy)
{
array_unshift(self::$strategies, $strategy);
self::clearCache();
}
/**
* Clear the cache.
*/
public static function clearCache()
{
self::$cache = [];
}
/**
* Evaluates conditions to boolean.
*
* @param mixed $condition
*
* @return bool
*/
protected static function evaluateCondition($condition)
{
if (is_string($condition)) {
// Should be extended for functions, extensions???
return self::safeClassExists($condition);
}
if (is_callable($condition)) {
return (bool) $condition();
}
if (is_bool($condition)) {
return $condition;
}
if (is_array($condition)) {
foreach ($condition as $c) {
if (false === static::evaluateCondition($c)) {
// Immediately stop execution if the condition is false
return false;
}
}
return true;
}
return false;
}
/**
* Get an instance of the $class.
*
* @param string|\Closure $class A FQCN of a class or a closure that instantiate the class.
*
* @return object
*
* @throws ClassInstantiationFailedException
*/
protected static function instantiateClass($class)
{
try {
if (is_string($class)) {
return new $class();
}
if (is_callable($class)) {
return $class();
}
} catch (\Exception $e) {
throw new ClassInstantiationFailedException('Unexpected exception when instantiating class.', 0, $e);
}
throw new ClassInstantiationFailedException('Could not instantiate class because parameter is neither a callable nor a string');
}
/**
* We want to do a "safe" version of PHP's "class_exists" because Magento has a bug
* (or they call it a "feature"). Magento is throwing an exception if you do class_exists()
* on a class that ends with "Factory" and if that file does not exits.
*
* This function will catch all potential exceptions and make sure it returns a boolean.
*
* @param string $class
* @param bool $autoload
*
* @return bool
*/
public static function safeClassExists($class)
{
try {
return class_exists($class);
} catch (\Exception $e) {
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Discovery;
/**
* An interface implemented by all discovery related exceptions.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface Exception
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* Thrown when a class fails to instantiate.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ClassInstantiationFailedException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* Thrown when all discovery strategies fails to find a resource.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class DiscoveryFailedException extends \Exception implements Exception
{
/**
* @var \Exception[]
*/
private $exceptions;
/**
* @param string $message
* @param \Exception[] $exceptions
*/
public function __construct($message, array $exceptions = [])
{
$this->exceptions = $exceptions;
parent::__construct($message);
}
/**
* @param \Exception[] $exceptions
*/
public static function create($exceptions)
{
$message = 'Could not find resource using any discovery strategy. Find more information at http://docs.php-http.org/en/latest/discovery.html#common-errors';
foreach ($exceptions as $e) {
$message .= "\n - ".$e->getMessage();
}
$message .= "\n\n";
return new self($message, $exceptions);
}
/**
* @return \Exception[]
*/
public function getExceptions()
{
return $this->exceptions;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* When we have used a strategy but no candidates provided by that strategy could be used.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class NoCandidateFoundException extends \Exception implements Exception
{
/**
* @param string $strategy
* @param array $candidates
*/
public function __construct($strategy, array $candidates)
{
$classes = array_map(
function ($a) {
return $a['class'];
},
$candidates
);
$message = sprintf(
'No valid candidate found using strategy "%s". We tested the following candidates: %s.',
$strategy,
implode(', ', array_map([$this, 'stringify'], $classes))
);
parent::__construct($message);
}
private function stringify($mixed)
{
if (is_string($mixed)) {
return $mixed;
}
if (is_array($mixed) && 2 === count($mixed)) {
return sprintf('%s::%s', $this->stringify($mixed[0]), $mixed[1]);
}
return is_object($mixed) ? get_class($mixed) : gettype($mixed);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* Thrown when a discovery does not find any matches.
*
* @final do NOT extend this class, not final for BC reasons
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
/*final */class NotFoundException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Discovery\Exception;
/**
* Thrown when we can't use Puli for discovery.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class PuliUnavailableException extends StrategyUnavailableException
{
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* This exception is thrown when we cannot use a discovery strategy. This is *not* thrown when
* the discovery fails to find a class.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class StrategyUnavailableException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Http\Discovery;
use Http\Client\HttpAsyncClient;
use Http\Discovery\Exception\DiscoveryFailedException;
/**
* Finds an HTTP Asynchronous Client.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class HttpAsyncClientDiscovery extends ClassDiscovery
{
/**
* Finds an HTTP Async Client.
*
* @return HttpAsyncClient
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$asyncClient = static::findOneByType(HttpAsyncClient::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException(
'No HTTPlug async clients found. Make sure to install a package providing "php-http/async-client-implementation". Example: "php-http/guzzle6-adapter".',
0,
$e
);
}
return static::instantiateClass($asyncClient);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Http\Discovery;
use Http\Client\HttpClient;
use Http\Discovery\Exception\DiscoveryFailedException;
/**
* Finds an HTTP Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class HttpClientDiscovery extends ClassDiscovery
{
/**
* Finds an HTTP Client.
*
* @return HttpClient
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$client = static::findOneByType(HttpClient::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException(
'No HTTPlug clients found. Make sure to install a package providing "php-http/client-implementation". Example: "php-http/guzzle6-adapter".',
0,
$e
);
}
return static::instantiateClass($client);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Message\MessageFactory;
/**
* Finds a Message Factory.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
*/
final class MessageFactoryDiscovery extends ClassDiscovery
{
/**
* Finds a Message Factory.
*
* @return MessageFactory
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$messageFactory = static::findOneByType(MessageFactory::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException(
'No message factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.',
0,
$e
);
}
return static::instantiateClass($messageFactory);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Discovery;
/**
* Thrown when a discovery does not find any matches.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since since version 1.0, and will be removed in 2.0. Use {@link \Http\Discovery\Exception\NotFoundException} instead.
*/
final class NotFoundException extends \Http\Discovery\Exception\NotFoundException
{
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
/**
* Finds PSR-17 factories.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class Psr17FactoryDiscovery extends ClassDiscovery
{
private static function createException($type, Exception $e)
{
return new \Http\Discovery\Exception\NotFoundException(
'No PSR-17 '.$type.' found. Install a package from this list: https://packagist.org/providers/psr/http-factory-implementation',
0,
$e
);
}
/**
* @return RequestFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findRequestFactory()
{
try {
$messageFactory = static::findOneByType(RequestFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('request factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return ResponseFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findResponseFactory()
{
try {
$messageFactory = static::findOneByType(ResponseFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('response factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return ServerRequestFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findServerRequestFactory()
{
try {
$messageFactory = static::findOneByType(ServerRequestFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('server request factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return StreamFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findStreamFactory()
{
try {
$messageFactory = static::findOneByType(StreamFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('stream factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return UploadedFileFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findUploadedFileFactory()
{
try {
$messageFactory = static::findOneByType(UploadedFileFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('uploaded file factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return UriFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findUrlFactory()
{
try {
$messageFactory = static::findOneByType(UriFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('url factory', $e);
}
return static::instantiateClass($messageFactory);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Psr\Http\Client\ClientInterface;
/**
* Finds a PSR-18 HTTP Client.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class Psr18ClientDiscovery extends ClassDiscovery
{
/**
* Finds a PSR-18 HTTP Client.
*
* @return ClientInterface
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$client = static::findOneByType(ClientInterface::class);
} catch (DiscoveryFailedException $e) {
throw new \Http\Discovery\Exception\NotFoundException(
'No PSR-18 clients found. Make sure to install a package providing "psr/http-client-implementation". Example: "php-http/guzzle6-adapter".',
0,
$e
);
}
return static::instantiateClass($client);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Http\Discovery\Strategy;
use GuzzleHttp\Psr7\Request as GuzzleRequest;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\MessageFactory;
use Http\Message\MessageFactory\GuzzleMessageFactory;
use Http\Message\StreamFactory;
use Http\Message\StreamFactory\GuzzleStreamFactory;
use Http\Message\UriFactory;
use Http\Message\UriFactory\GuzzleUriFactory;
use Http\Message\MessageFactory\DiactorosMessageFactory;
use Http\Message\StreamFactory\DiactorosStreamFactory;
use Http\Message\UriFactory\DiactorosUriFactory;
use Psr\Http\Client\ClientInterface as Psr18Client;
use Zend\Diactoros\Request as DiactorosRequest;
use Http\Message\MessageFactory\SlimMessageFactory;
use Http\Message\StreamFactory\SlimStreamFactory;
use Http\Message\UriFactory\SlimUriFactory;
use Slim\Http\Request as SlimRequest;
use Http\Adapter\Guzzle6\Client as Guzzle6;
use Http\Adapter\Guzzle5\Client as Guzzle5;
use Http\Client\Curl\Client as Curl;
use Http\Client\Socket\Client as Socket;
use Http\Adapter\React\Client as React;
use Http\Adapter\Buzz\Client as Buzz;
use Http\Adapter\Cake\Client as Cake;
use Http\Adapter\Zend\Client as Zend;
use Http\Adapter\Artax\Client as Artax;
use Nyholm\Psr7\Factory\HttplugFactory as NyholmHttplugFactory;
/**
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class CommonClassesStrategy implements DiscoveryStrategy
{
/**
* @var array
*/
private static $classes = [
MessageFactory::class => [
['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]],
['class' => GuzzleMessageFactory::class, 'condition' => [GuzzleRequest::class, GuzzleMessageFactory::class]],
['class' => DiactorosMessageFactory::class, 'condition' => [DiactorosRequest::class, DiactorosMessageFactory::class]],
['class' => SlimMessageFactory::class, 'condition' => [SlimRequest::class, SlimMessageFactory::class]],
],
StreamFactory::class => [
['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]],
['class' => GuzzleStreamFactory::class, 'condition' => [GuzzleRequest::class, GuzzleStreamFactory::class]],
['class' => DiactorosStreamFactory::class, 'condition' => [DiactorosRequest::class, DiactorosStreamFactory::class]],
['class' => SlimStreamFactory::class, 'condition' => [SlimRequest::class, SlimStreamFactory::class]],
],
UriFactory::class => [
['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]],
['class' => GuzzleUriFactory::class, 'condition' => [GuzzleRequest::class, GuzzleUriFactory::class]],
['class' => DiactorosUriFactory::class, 'condition' => [DiactorosRequest::class, DiactorosUriFactory::class]],
['class' => SlimUriFactory::class, 'condition' => [SlimRequest::class, SlimUriFactory::class]],
],
HttpAsyncClient::class => [
['class' => Guzzle6::class, 'condition' => Guzzle6::class],
['class' => Curl::class, 'condition' => Curl::class],
['class' => React::class, 'condition' => React::class],
],
HttpClient::class => [
['class' => Guzzle6::class, 'condition' => Guzzle6::class],
['class' => Guzzle5::class, 'condition' => Guzzle5::class],
['class' => Curl::class, 'condition' => Curl::class],
['class' => Socket::class, 'condition' => Socket::class],
['class' => Buzz::class, 'condition' => Buzz::class],
['class' => React::class, 'condition' => React::class],
['class' => Cake::class, 'condition' => Cake::class],
['class' => Zend::class, 'condition' => Zend::class],
['class' => Artax::class, 'condition' => Artax::class],
[
'class' => [self::class, 'buzzInstantiate'],
'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class],
],
],
Psr18Client::class => [
[
'class' => [self::class, 'buzzInstantiate'],
'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class],
],
],
];
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
if (Psr18Client::class === $type) {
$candidates = self::$classes[PSR18Client::class];
// HTTPlug 2.0 clients implements PSR18Client too.
foreach (self::$classes[HttpClient::class] as $c) {
if (is_subclass_of($c['class'], Psr18Client::class)) {
$candidates[] = $c;
}
}
return $candidates;
}
if (isset(self::$classes[$type])) {
return self::$classes[$type];
}
return [];
}
public static function buzzInstantiate()
{
return new \Buzz\Client\FileGetContents(MessageFactoryDiscovery::find());
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Http\Discovery\Strategy;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
/**
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class CommonPsr17ClassesStrategy implements DiscoveryStrategy
{
/**
* @var array
*/
private static $classes = [
RequestFactoryInterface::class => [
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\RequestFactory',
'Http\Factory\Diactoros\RequestFactory',
'Http\Factory\Guzzle\RequestFactory',
'Http\Factory\Slim\RequestFactory',
],
ResponseFactoryInterface::class => [
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\ResponseFactory',
'Http\Factory\Diactoros\ResponseFactory',
'Http\Factory\Guzzle\ResponseFactory',
'Http\Factory\Slim\ResponseFactory',
],
ServerRequestFactoryInterface::class => [
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\ServerRequestFactory',
'Http\Factory\Diactoros\ServerRequestFactory',
'Http\Factory\Guzzle\ServerRequestFactory',
'Http\Factory\Slim\ServerRequestFactory',
],
StreamFactoryInterface::class => [
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\StreamFactory',
'Http\Factory\Diactoros\StreamFactory',
'Http\Factory\Guzzle\StreamFactory',
'Http\Factory\Slim\StreamFactory',
],
UploadedFileFactoryInterface::class => [
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\UploadedFileFactory',
'Http\Factory\Diactoros\UploadedFileFactory',
'Http\Factory\Guzzle\UploadedFileFactory',
'Http\Factory\Slim\UploadedFileFactory',
],
UriFactoryInterface::class => [
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\UriFactory',
'Http\Factory\Diactoros\UriFactory',
'Http\Factory\Guzzle\UriFactory',
'Http\Factory\Slim\UriFactory',
],
];
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
$candidates = [];
if (isset(self::$classes[$type])) {
foreach (self::$classes[$type] as $class) {
$candidates[] = ['class' => $class, 'condition' => [$class]];
}
}
return $candidates;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Http\Discovery\Strategy;
use Http\Discovery\Exception\StrategyUnavailableException;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface DiscoveryStrategy
{
/**
* Find a resource of a specific type.
*
* @param string $type
*
* @return array The return value is always an array with zero or more elements. Each
* element is an array with two keys ['class' => string, 'condition' => mixed].
*
* @throws StrategyUnavailableException if we cannot use this strategy.
*/
public static function getCandidates($type);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Http\Discovery\Strategy;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Mock\Client as Mock;
/**
* Find the Mock client.
*
* @author Sam Rapaport <me@samrapdev.com>
*/
final class MockClientStrategy implements DiscoveryStrategy
{
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
switch ($type) {
case HttpClient::class:
case HttpAsyncClient::class:
return [['class' => Mock::class, 'condition' => Mock::class]];
default:
return [];
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Http\Discovery\Strategy;
use Http\Discovery\ClassDiscovery;
use Http\Discovery\Exception\PuliUnavailableException;
use Puli\Discovery\Api\Discovery;
use Puli\GeneratedPuliFactory;
/**
* Find candidates using Puli.
*
* @internal
* @final
*
* @author David de Boer <david@ddeboer.nl>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class PuliBetaStrategy implements DiscoveryStrategy
{
/**
* @var GeneratedPuliFactory
*/
protected static $puliFactory;
/**
* @var Discovery
*/
protected static $puliDiscovery;
/**
* @return GeneratedPuliFactory
*
* @throws PuliUnavailableException
*/
private static function getPuliFactory()
{
if (null === self::$puliFactory) {
if (!defined('PULI_FACTORY_CLASS')) {
throw new PuliUnavailableException('Puli Factory is not available');
}
$puliFactoryClass = PULI_FACTORY_CLASS;
if (!ClassDiscovery::safeClassExists($puliFactoryClass)) {
throw new PuliUnavailableException('Puli Factory class does not exist');
}
self::$puliFactory = new $puliFactoryClass();
}
return self::$puliFactory;
}
/**
* Returns the Puli discovery layer.
*
* @return Discovery
*
* @throws PuliUnavailableException
*/
private static function getPuliDiscovery()
{
if (!isset(self::$puliDiscovery)) {
$factory = self::getPuliFactory();
$repository = $factory->createRepository();
self::$puliDiscovery = $factory->createDiscovery($repository);
}
return self::$puliDiscovery;
}
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
$returnData = [];
$bindings = self::getPuliDiscovery()->findBindings($type);
foreach ($bindings as $binding) {
$condition = true;
if ($binding->hasParameterValue('depends')) {
$condition = $binding->getParameterValue('depends');
}
$returnData[] = ['class' => $binding->getClassName(), 'condition' => $condition];
}
return $returnData;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Message\StreamFactory;
/**
* Finds a Stream Factory.
*
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*
* @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
*/
final class StreamFactoryDiscovery extends ClassDiscovery
{
/**
* Finds a Stream Factory.
*
* @return StreamFactory
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$streamFactory = static::findOneByType(StreamFactory::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException(
'No stream factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.',
0,
$e
);
}
return static::instantiateClass($streamFactory);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Message\UriFactory;
/**
* Finds a URI Factory.
*
* @author David de Boer <david@ddeboer.nl>
*
* @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
*/
final class UriFactoryDiscovery extends ClassDiscovery
{
/**
* Finds a URI Factory.
*
* @return UriFactory
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$uriFactory = static::findOneByType(UriFactory::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException(
'No uri factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.',
0,
$e
);
}
return static::instantiateClass($uriFactory);
}
}

View File

@@ -0,0 +1,72 @@
# Change Log
## 1.1.0 - 2016-08-31
- Added HttpFulfilledPromise and HttpRejectedPromise which respect the HttpAsyncClient interface
## 1.0.0 - 2016-01-26
### Removed
- Stability configuration from composer
## 1.0.0-RC1 - 2016-01-12
### Changed
- Updated package files
- Updated promise dependency to RC1
## 1.0.0-beta - 2015-12-17
### Added
- Puli configuration and binding types
### Changed
- Exception concept
## 1.0.0-alpha3 - 2015-12-13
### Changed
- Async client does not throw exceptions
### Removed
- Promise interface moved to its own repository: [php-http/promise](https://github.com/php-http/promise)
## 1.0.0-alpha2 - 2015-11-16
### Added
- Async client and Promise interface
## 1.0.0-alpha - 2015-10-26
### Added
- Better domain exceptions.
### Changed
- Purpose of the library: general HTTP CLient abstraction.
### Removed
- Request options: they should be configured at construction time.
- Multiple request sending: should be done asynchronously using Async Client.
- `getName` method
## 0.1.0 - 2015-06-03
### Added
- Initial release

View File

@@ -0,0 +1,20 @@
Copyright (c) 2014-2015 Eric GELOEN <geloen.eric@gmail.com>
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,57 @@
# HTTPlug
[![Latest Version](https://img.shields.io/github/release/php-http/httplug.svg?style=flat-square)](https://github.com/php-http/httplug/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/httplug.svg?style=flat-square)](https://travis-ci.org/php-http/httplug)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/httplug.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/httplug)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/httplug.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/httplug)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/httplug.svg?style=flat-square)](https://packagist.org/packages/php-http/httplug)
[![Slack Status](http://slack.httplug.io/badge.svg)](http://slack.httplug.io)
[![Email](https://img.shields.io/badge/email-team@httplug.io-blue.svg?style=flat-square)](mailto:team@httplug.io)
**HTTPlug, the HTTP client abstraction for PHP.**
## Install
Via Composer
``` bash
$ composer require php-http/httplug
```
## Intro
This is the contract package for HTTP Client.
Use it to create HTTP Clients which are interoperable and compatible with [PSR-7](http://www.php-fig.org/psr/psr-7/).
This library is the official successor of the [ivory http adapter](https://github.com/egeloen/ivory-http-adapter).
## Documentation
Please see the [official documentation](http://docs.php-http.org).
## 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,40 @@
{
"name": "php-http/httplug",
"description": "HTTPlug, the HTTP client abstraction for PHP",
"license": "MIT",
"keywords": ["http", "client"],
"homepage": "http://httplug.io",
"authors": [
{
"name": "Eric GELOEN",
"email": "geloen.eric@gmail.com"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": ">=5.4",
"psr/http-message": "^1.0",
"php-http/promise": "^1.0"
},
"require-dev": {
"phpspec/phpspec": "^2.4",
"henrikbjorn/phpspec-code-coverage" : "^1.0"
},
"autoload": {
"psr-4": {
"Http\\Client\\": "src/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"version": "1.0",
"name": "php-http/httplug",
"binding-types": {
"Http\\Client\\HttpAsyncClient": {
"description": "Async HTTP Client"
},
"Http\\Client\\HttpClient": {
"description": "HTTP Client"
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Client;
/**
* Every HTTP Client related Exception must implement this interface.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Exception
{
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Thrown when a response was received but the request itself failed.
*
* In addition to the request, this exception always provides access to the response object.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class HttpException extends RequestException
{
/**
* @var ResponseInterface
*/
protected $response;
/**
* @param string $message
* @param RequestInterface $request
* @param ResponseInterface $response
* @param \Exception|null $previous
*/
public function __construct(
$message,
RequestInterface $request,
ResponseInterface $response,
\Exception $previous = null
) {
parent::__construct($message, $request, $previous);
$this->response = $response;
$this->code = $response->getStatusCode();
}
/**
* Returns the response.
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response;
}
/**
* Factory method to create a new exception with a normalized error message.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param \Exception|null $previous
*
* @return HttpException
*/
public static function create(
RequestInterface $request,
ResponseInterface $response,
\Exception $previous = null
) {
$message = sprintf(
'[url] %s [http method] %s [status code] %s [reason phrase] %s',
$request->getRequestTarget(),
$request->getMethod(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
return new self($message, $request, $response, $previous);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Exception;
/**
* Thrown when the request cannot be completed because of network issues.
*
* There is no response object as this exception is thrown when no response has been received.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class NetworkException extends RequestException
{
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
/**
* Exception for when a request failed, providing access to the failed request.
*
* This could be due to an invalid request, or one of the extending exceptions
* for network errors or HTTP error responses.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class RequestException extends TransferException
{
/**
* @var RequestInterface
*/
private $request;
/**
* @param string $message
* @param RequestInterface $request
* @param \Exception|null $previous
*/
public function __construct($message, RequestInterface $request, \Exception $previous = null)
{
$this->request = $request;
parent::__construct($message, 0, $previous);
}
/**
* Returns the request.
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Exception;
use Http\Client\Exception;
/**
* Base exception for transfer related exceptions.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class TransferException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Http\Client;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
/**
* Sends a PSR-7 Request in an asynchronous way by returning a Promise.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface HttpAsyncClient
{
/**
* Sends a PSR-7 request in an asynchronous way.
*
* Exceptions related to processing the request are available from the returned Promise.
*
* @param RequestInterface $request
*
* @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception.
*
* @throws \Exception If processing the request is impossible (eg. bad configuration).
*/
public function sendAsyncRequest(RequestInterface $request);
}

Some files were not shown because too many files have changed in this diff Show More