This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<?php
/*
* Configuration for fabpot/php-cs-fixer
*
* @link https://github.com/FriendsOfPHP/PHP-CS-Fixer
*/
$finder = PhpCsFixer\Finder::create()
->in('src')
->in('spec')
;
return PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'@Symfony' => true,
'array_syntax' => [
'syntax' => 'short',
],
'no_empty_phpdoc' => true,
'phpdoc_to_comment' => false,
'single_line_throw' => false,
])
->setFinder($finder);

View File

@@ -0,0 +1,253 @@
# Change Log
## 2.4.0 - 2021-07-05
### Added
- `strict` option to `RedirectPlugin` to allow preserving the request method on redirections with status 300, 301 and 302.
## 2.3.0 - 2020-07-21
### Fixed
- HttpMethodsClient with PSR RequestFactory
- Bug in the cookie plugin with empty cookies
- Bug when parsing null-valued date headers
### Changed
- Deprecation when constructing a HttpMethodsClient with PSR RequestFactory but without a StreamFactory
## 2.2.1 - 2020-07-13
### Fixed
- Support for PHP 8
- Plugin callable phpdoc
## 2.2.0 - 2020-07-02
### Added
- Plugin client builder for making a `PluginClient`
- Support for the PSR-17 request factory in `HttpMethodsClient`
### Changed
- Restored support for `symfony/options-resolver: ^2.6`
- Consistent implementation of union type checking
### Fixed
- Memory leak when using the `PluginClient` with plugins
## 2.1.0 - 2019-11-18
### Added
- Support Symfony 5
## 2.0.0 - 2019-02-03
### Changed
- HttpClientRouter now throws a HttpClientNoMatchException instead of a RequestException if it can not find a client for the request.
- RetryPlugin will only retry exceptions when there is no response, or a response in the 5xx HTTP code range.
- RetryPlugin also retries when no exception is thrown if the responses has HTTP code in the 5xx range.
The callbacks for exception handling have been renamed and callbacks for response handling have been added.
- Abstract method `HttpClientPool::chooseHttpClient()` has now an explicit return type (`Http\Client\Common\HttpClientPoolItem`)
- Interface method `Plugin::handleRequest(...)` has now an explicit return type (`Http\Promise\Promise`)
- Made classes final that are not intended to be extended.
- Added interfaces for BatchClient, HttpClientRouter and HttpMethodsClient.
(These interfaces use the `Interface` suffix to avoid name collisions.)
- Added an interface for HttpClientPool and moved the abstract class to the HttpClientPool sub namespace.
- AddPathPlugin: Do not add the prefix if the URL already has the same prefix.
- All exceptions in `Http\Client\Common\Exception` are final.
### Removed
- Deprecated option `debug_plugins` has been removed from `PluginClient`
- Deprecated options `decider` and `delay` have been removed from `RetryPlugin`, use `exception_decider` and `exception_delay` instead.
## 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

19
vendor/php-http/client-common/LICENSE vendored Normal file
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.

55
vendor/php-http/client-common/README.md vendored Normal file
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/master.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,67 @@
{
"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": "^7.1 || ^8.0",
"php-http/httplug": "^2.0",
"php-http/message-factory": "^1.0",
"php-http/message": "^1.6",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0",
"symfony/options-resolver": "^2.6 || ^3.4.20 || ~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0",
"symfony/polyfill-php80": "^1.17"
},
"require-dev": {
"doctrine/instantiator": "^1.1",
"guzzlehttp/psr7": "^1.4",
"nyholm/psr7": "^1.2",
"phpspec/phpspec": "^5.1 || ^6.0",
"phpspec/prophecy": "^1.10.2",
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3"
},
"suggest": {
"ext-json": "To detect JSON responses with the ContentTypePlugin",
"ext-libxml": "To detect XML responses with the ContentTypePlugin",
"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/"
}
},
"autoload-dev": {
"psr-4": {
"spec\\Http\\Client\\Common\\": "spec/"
}
},
"scripts": {
"test": [
"vendor/bin/phpspec run",
"vendor/bin/phpunit"
],
"test-ci": [
"vendor/bin/phpspec run -c phpspec.ci.yml",
"vendor/bin/phpunit"
]
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\Common\Exception\BatchException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
final class BatchClient implements BatchClientInterface
{
/**
* @var ClientInterface
*/
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function sendRequests(array $requests): BatchResult
{
$batchResult = new BatchResult();
foreach ($requests as $request) {
try {
$response = $this->client->sendRequest($request);
$batchResult = $batchResult->addResponse($request, $response);
} catch (ClientExceptionInterface $e) {
$batchResult = $batchResult->addException($request, $e);
}
}
if ($batchResult->hasExceptions()) {
throw new BatchException($batchResult);
}
return $batchResult;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\Common\Exception\BatchException;
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>
*/
interface BatchClientInterface
{
/**
* 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[] $requests 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;
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Psr\Http\Client\ClientExceptionInterface;
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<RequestInterface, ResponseInterface>
*/
private $responses;
/**
* @var \SplObjectStorage<RequestInterface, ClientExceptionInterface>
*/
private $exceptions;
public function __construct()
{
$this->responses = new \SplObjectStorage();
$this->exceptions = new \SplObjectStorage();
}
/**
* Checks if there are any successful responses at all.
*/
public function hasResponses(): bool
{
return $this->responses->count() > 0;
}
/**
* Returns all successful responses.
*
* @return ResponseInterface[]
*/
public function getResponses(): array
{
$responses = [];
foreach ($this->responses as $request) {
$responses[] = $this->responses[$request];
}
return $responses;
}
/**
* Checks if there is a successful response for a request.
*/
public function isSuccessful(RequestInterface $request): bool
{
return $this->responses->contains($request);
}
/**
* Returns the response for a successful request.
*
* @throws \UnexpectedValueException If request was not part of the batch or failed
*/
public function getResponseFor(RequestInterface $request): ResponseInterface
{
try {
return $this->responses[$request];
} catch (\UnexpectedValueException $e) {
throw new \UnexpectedValueException('Request not found', $e->getCode(), $e);
}
}
/**
* Adds a response in an immutable way.
*
* @return BatchResult the new BatchResult with this request-response pair added to it
*/
public function addResponse(RequestInterface $request, ResponseInterface $response): self
{
$new = clone $this;
$new->responses->attach($request, $response);
return $new;
}
/**
* Checks if there are any unsuccessful requests at all.
*/
public function hasExceptions(): bool
{
return $this->exceptions->count() > 0;
}
/**
* Returns all exceptions for the unsuccessful requests.
*
* @return ClientExceptionInterface[]
*/
public function getExceptions(): array
{
$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.
*/
public function isFailed(RequestInterface $request): bool
{
return $this->exceptions->contains($request);
}
/**
* Returns the exception for a failed request.
*
* @throws \UnexpectedValueException If request was not part of the batch or was successful
*/
public function getExceptionFor(RequestInterface $request): ClientExceptionInterface
{
try {
return $this->exceptions[$request];
} catch (\UnexpectedValueException $e) {
throw new \UnexpectedValueException('Request not found', $e->getCode(), $e);
}
}
/**
* Adds an exception in an immutable way.
*
* @return BatchResult the new BatchResult with this request-exception pair added to it
*/
public function addException(RequestInterface $request, ClientExceptionInterface $exception): self
{
$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,152 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Promise\Promise;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A deferred allow to return a promise which has not been resolved yet.
*/
final class Deferred implements Promise
{
/**
* @var ResponseInterface|null
*/
private $value;
/**
* @var ClientExceptionInterface|null
*/
private $failure;
/**
* @var string
*/
private $state;
/**
* @var callable
*/
private $waitCallback;
/**
* @var callable[]
*/
private $onFulfilledCallbacks;
/**
* @var callable[]
*/
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): Promise
{
$deferred = new self($this->waitCallback);
$this->onFulfilledCallbacks[] = function (ResponseInterface $response) use ($onFulfilled, $deferred) {
try {
if (null !== $onFulfilled) {
$response = $onFulfilled($response);
}
$deferred->resolve($response);
} catch (ClientExceptionInterface $exception) {
$deferred->reject($exception);
}
};
$this->onRejectedCallbacks[] = function (ClientExceptionInterface $exception) use ($onRejected, $deferred) {
try {
if (null !== $onRejected) {
$response = $onRejected($exception);
$deferred->resolve($response);
return;
}
$deferred->reject($exception);
} catch (ClientExceptionInterface $newException) {
$deferred->reject($newException);
}
};
return $deferred;
}
/**
* {@inheritdoc}
*/
public function getState(): string
{
return $this->state;
}
/**
* Resolve this deferred with a Response.
*/
public function resolve(ResponseInterface $response): void
{
if (Promise::PENDING !== $this->state) {
return;
}
$this->value = $response;
$this->state = Promise::FULFILLED;
foreach ($this->onFulfilledCallbacks as $onFulfilledCallback) {
$onFulfilledCallback($response);
}
}
/**
* Reject this deferred with an Exception.
*/
public function reject(ClientExceptionInterface $exception): void
{
if (Promise::PENDING !== $this->state) {
return;
}
$this->failure = $exception;
$this->state = Promise::REJECTED;
foreach ($this->onRejectedCallbacks as $onRejectedCallback) {
$onRejectedCallback($exception);
}
}
/**
* {@inheritdoc}
*/
public function wait($unwrap = true)
{
if (Promise::PENDING === $this->state) {
$callback = $this->waitCallback;
$callback();
}
if (!$unwrap) {
return null;
}
if (Promise::FULFILLED === $this->state) {
return $this->value;
}
/** @var ClientExceptionInterface */
throw $this->failure;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
/**
* Emulates an async HTTP client with the help of a synchronous client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient
{
use HttpAsyncClientEmulator;
use HttpClientDecorator;
public function __construct(ClientInterface $httpClient)
{
$this->httpClient = $httpClient;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
/**
* Emulates a synchronous HTTP client with the help of an asynchronous client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class EmulatedHttpClient implements HttpClient, HttpAsyncClient
{
use HttpAsyncClientDecorator;
use HttpClientEmulator;
public function __construct(HttpAsyncClient $httpAsyncClient)
{
$this->httpAsyncClient = $httpAsyncClient;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Exception;
use Http\Client\Common\BatchResult;
use Http\Client\Exception\TransferException;
/**
* 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;
public function __construct(BatchResult $result)
{
$this->result = $result;
parent::__construct();
}
/**
* Returns the BatchResult that contains all responses and exceptions.
*/
public function getResult(): BatchResult
{
return $this->result;
}
}

View File

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

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
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>
*/
final class ClientErrorException extends HttpException
{
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Exception;
use Http\Client\Exception\TransferException;
use Psr\Http\Message\RequestInterface;
/**
* Thrown when a http client match in the HTTPClientRouter.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class HttpClientNoMatchException extends TransferException
{
/**
* @var RequestInterface
*/
private $request;
public function __construct(string $message, RequestInterface $request, \Exception $previous = null)
{
$this->request = $request;
parent::__construct($message, 0, $previous);
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
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>
*/
final class HttpClientNotFoundException extends TransferException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
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>
*/
final class LoopException extends RequestException
{
}

View File

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

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
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>
*/
final class ServerErrorException extends HttpException
{
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
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 ClientInterface|HttpAsyncClient $client
*/
public function __construct($client)
{
if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) {
throw new \TypeError(
sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client))
);
}
$this->httpClient = $client instanceof ClientInterface ? $client : new EmulatedHttpClient($client);
$this->httpAsyncClient = $client instanceof HttpAsyncClient ? $client : new EmulatedHttpAsyncClient($client);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
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,39 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\Exception;
use Http\Client\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* 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): ResponseInterface;
/**
* {@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,32 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Decorates an HTTP Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait HttpClientDecorator
{
/**
* @var ClientInterface
*/
protected $httpClient;
/**
* {@inheritdoc}
*
* @see ClientInterface::sendRequest
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->httpClient->sendRequest($request);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* 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): ResponseInterface
{
$promise = $this->sendAsyncRequest($request);
return $promise->wait();
}
/**
* {@inheritdoc}
*
* @see HttpAsyncClient::sendAsyncRequest
*/
abstract public function sendAsyncRequest(RequestInterface $request);
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\Common\HttpClientPool\HttpClientPoolItem;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
/**
* A http client pool allows to send requests on a pool of different http client using a specific strategy (least used,
* round robin, ...).
*/
interface HttpClientPool extends HttpAsyncClient, HttpClient
{
/**
* Add a client to the pool.
*
* @param ClientInterface|HttpAsyncClient|HttpClientPoolItem $client
*/
public function addHttpClient($client): void;
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool as HttpClientPoolInterface;
use Http\Client\HttpAsyncClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* 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 HttpClientPoolInterface
{
/**
* @var HttpClientPoolItem[]
*/
protected $clientPool = [];
/**
* Add a client to the pool.
*
* @param ClientInterface|HttpAsyncClient $client
*/
public function addHttpClient($client): void
{
// no need to check for HttpClientPoolItem here, since it extends the other interfaces
if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) {
throw new \TypeError(
sprintf('%s::addHttpClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($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(): HttpClientPoolItem;
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
return $this->chooseHttpClient()->sendAsyncRequest($request);
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->chooseHttpClient()->sendRequest($request);
}
}

View File

@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\FlexibleHttpClient;
use Http\Client\Exception;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A HttpClientPoolItem represent a HttpClient inside a Pool.
*
* It is disabled when a request failed and can be reenabled after a certain number of seconds.
* It also keep tracks of the current number of open requests the client is currently being sending
* (only usable for async method).
*
* This class is used internally in the client pools and is not supposed to be used anywhere else.
*
* @final
*
* @internal
*
* @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;
/**
* Number of seconds until this client is enabled again after an error.
*
* null: never reenable this client.
*
* @var int|null
*/
private $reenableAfter;
/**
* @var FlexibleHttpClient A http client responding to async and sync request
*/
private $client;
/**
* @param ClientInterface|HttpAsyncClient $client
* @param int|null $reenableAfter Number of seconds until this client is enabled again after an error
*/
public function __construct($client, int $reenableAfter = null)
{
if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) {
throw new \TypeError(
sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client))
);
}
$this->client = new FlexibleHttpClient($client);
$this->reenableAfter = $reenableAfter;
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
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.
*
* If the client was disabled, calling this method checks if the client can
* be reenabled and if so enables it.
*/
public function isDisabled(): bool
{
if (null !== $this->reenableAfter && null !== $this->disabledAt) {
// Reenable after a certain time
$now = new \DateTime();
if (($now->getTimestamp() - $this->disabledAt->getTimestamp()) >= $this->reenableAfter) {
$this->enable();
return false;
}
return true;
}
return null !== $this->disabledAt;
}
/**
* Get current number of request that are currently being sent by the underlying HTTP client.
*/
public function getSendingRequestCount(): int
{
return $this->sendingRequestCount;
}
/**
* Increment the request count.
*/
private function incrementRequestCount(): void
{
++$this->sendingRequestCount;
}
/**
* Decrement the request count.
*/
private function decrementRequestCount(): void
{
--$this->sendingRequestCount;
}
/**
* Enable the current client.
*/
private function enable(): void
{
$this->disabledAt = null;
}
/**
* Disable the current client.
*/
private function disable(): void
{
$this->disabledAt = new \DateTime('now');
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
/**
* 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(): HttpClientPoolItem
{
$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
declare(strict_types=1);
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
/**
* 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(): HttpClientPoolItem
{
$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,42 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\HttpClientPool;
use Http\Client\Common\Exception\HttpClientNotFoundException;
/**
* 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(): HttpClientPoolItem
{
$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,74 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\Common\Exception\HttpClientNoMatchException;
use Http\Client\HttpAsyncClient;
use Http\Message\RequestMatcher;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* {@inheritdoc}
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class HttpClientRouter implements HttpClientRouterInterface
{
/**
* @var (array{matcher: RequestMatcher, client: FlexibleHttpClient})[]
*/
private $clients = [];
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->chooseHttpClient($request)->sendRequest($request);
}
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
return $this->chooseHttpClient($request)->sendAsyncRequest($request);
}
/**
* Add a client to the router.
*
* @param ClientInterface|HttpAsyncClient $client
*/
public function addClient($client, RequestMatcher $requestMatcher): void
{
if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) {
throw new \TypeError(
sprintf('%s::addClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client))
);
}
$this->clients[] = [
'matcher' => $requestMatcher,
'client' => new FlexibleHttpClient($client),
];
}
/**
* Choose an HTTP client given a specific request.
*/
private function chooseHttpClient(RequestInterface $request): FlexibleHttpClient
{
foreach ($this->clients as $client) {
if ($client['matcher']->matches($request)) {
return $client['client'];
}
}
throw new HttpClientNoMatchException('No client found for the specified request', $request);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Message\RequestMatcher;
use Psr\Http\Client\ClientInterface;
/**
* Route a request to a specific client in the stack based using a RequestMatcher.
*
* This is not a HttpClientPool client because it uses a matcher to select the client.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface HttpClientRouterInterface extends HttpClient, HttpAsyncClient
{
/**
* Add a client to the router.
*
* @param ClientInterface|HttpAsyncClient $client
*/
public function addClient($client, RequestMatcher $requestMatcher): void;
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Message\RequestFactory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
final class HttpMethodsClient implements HttpMethodsClientInterface
{
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactory|RequestFactoryInterface
*/
private $requestFactory;
/**
* @var StreamFactoryInterface|null
*/
private $streamFactory;
/**
* @param RequestFactory|RequestFactoryInterface $requestFactory
*/
public function __construct(ClientInterface $httpClient, $requestFactory, StreamFactoryInterface $streamFactory = null)
{
if (!$requestFactory instanceof RequestFactory && !$requestFactory instanceof RequestFactoryInterface) {
throw new \TypeError(
sprintf('%s::__construct(): Argument #2 ($requestFactory) must be of type %s|%s, %s given', self::class, RequestFactory::class, RequestFactoryInterface::class, get_debug_type($requestFactory))
);
}
if (!$requestFactory instanceof RequestFactory && null === $streamFactory) {
@trigger_error(sprintf('Passing a %s without a %s to %s::__construct() is deprecated as of version 2.3 and will be disallowed in version 3.0. A stream factory is required to create a request with a non-empty string body.', RequestFactoryInterface::class, StreamFactoryInterface::class, self::class));
}
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
}
public function get($uri, array $headers = []): ResponseInterface
{
return $this->send('GET', $uri, $headers, null);
}
public function head($uri, array $headers = []): ResponseInterface
{
return $this->send('HEAD', $uri, $headers, null);
}
public function trace($uri, array $headers = []): ResponseInterface
{
return $this->send('TRACE', $uri, $headers, null);
}
public function post($uri, array $headers = [], $body = null): ResponseInterface
{
return $this->send('POST', $uri, $headers, $body);
}
public function put($uri, array $headers = [], $body = null): ResponseInterface
{
return $this->send('PUT', $uri, $headers, $body);
}
public function patch($uri, array $headers = [], $body = null): ResponseInterface
{
return $this->send('PATCH', $uri, $headers, $body);
}
public function delete($uri, array $headers = [], $body = null): ResponseInterface
{
return $this->send('DELETE', $uri, $headers, $body);
}
public function options($uri, array $headers = [], $body = null): ResponseInterface
{
return $this->send('OPTIONS', $uri, $headers, $body);
}
public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface
{
if (!is_string($uri) && !$uri instanceof UriInterface) {
throw new \TypeError(
sprintf('%s::send(): Argument #2 ($uri) must be of type string|%s, %s given', self::class, UriInterface::class, get_debug_type($uri))
);
}
if (!is_string($body) && !$body instanceof StreamInterface && null !== $body) {
throw new \TypeError(
sprintf('%s::send(): Argument #4 ($body) must be of type string|%s|null, %s given', self::class, StreamInterface::class, get_debug_type($body))
);
}
return $this->sendRequest(
self::createRequest($method, $uri, $headers, $body)
);
}
/**
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*/
private function createRequest(string $method, $uri, array $headers = [], $body = null): RequestInterface
{
if ($this->requestFactory instanceof RequestFactory) {
return $this->requestFactory->createRequest(
$method,
$uri,
$headers,
$body
);
}
if (is_string($body) && '' !== $body && null === $this->streamFactory) {
throw new \RuntimeException('Cannot create request: A stream factory is required to create a request with a non-empty string body.');
}
$request = $this->requestFactory->createRequest($method, $uri);
foreach ($headers as $key => $value) {
$request = $request->withHeader($key, $value);
}
if (null !== $body && '' !== $body) {
$request = $request->withBody(
is_string($body) ? $this->streamFactory->createStream($body) : $body
);
}
return $request;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->httpClient->sendRequest($request);
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\Exception;
use Http\Client\HttpClient;
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>
*/
interface HttpMethodsClientInterface extends HttpClient
{
/**
* Sends a GET request.
*
* @param string|UriInterface $uri
*
* @throws Exception
*/
public function get($uri, array $headers = []): ResponseInterface;
/**
* Sends an HEAD request.
*
* @param string|UriInterface $uri
*
* @throws Exception
*/
public function head($uri, array $headers = []): ResponseInterface;
/**
* Sends a TRACE request.
*
* @param string|UriInterface $uri
*
* @throws Exception
*/
public function trace($uri, array $headers = []): ResponseInterface;
/**
* Sends a POST request.
*
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*
* @throws Exception
*/
public function post($uri, array $headers = [], $body = null): ResponseInterface;
/**
* Sends a PUT request.
*
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*
* @throws Exception
*/
public function put($uri, array $headers = [], $body = null): ResponseInterface;
/**
* Sends a PATCH request.
*
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*
* @throws Exception
*/
public function patch($uri, array $headers = [], $body = null): ResponseInterface;
/**
* Sends a DELETE request.
*
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*
* @throws Exception
*/
public function delete($uri, array $headers = [], $body = null): ResponseInterface;
/**
* Sends an OPTIONS request.
*
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*
* @throws Exception
*/
public function options($uri, array $headers = [], $body = null): ResponseInterface;
/**
* Sends a request with any HTTP method.
*
* @param string $method HTTP method to use
* @param string|UriInterface $uri
* @param string|StreamInterface|null $body
*
* @throws Exception
*/
public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface;
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
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 callable(RequestInterface): Promise $next Next middleware in the chain, the request is passed as the first argument
* @param callable(RequestInterface): Promise $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): Promise;
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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 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): Promise
{
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);
}
private function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'replace' => false,
]);
$resolver->setAllowedTypes('replace', 'bool');
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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;
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;
}
/**
* Adds a prefix in the beginning of the URL's path.
*
* The prefix is not added if that prefix is already on the URL's path. This will fail on the edge
* case of the prefix being repeated, for example if `https://example.com/api/api/foo` is a valid
* URL on the server and the configured prefix is `/api`.
*
* We looked at other solutions, but they are all much more complicated, while still having edge
* cases:
* - Doing an spl_object_hash on `$first` will lead to collisions over time because over time the
* hash can collide.
* - Have the PluginClient provide a magic header to identify the request chain and only apply
* this plugin once.
*
* There are 2 reasons for the AddPathPlugin to be executed twice on the same request:
* - A plugin can restart the chain by calling `$first`, e.g. redirect
* - A plugin can call `$next` more than once, e.g. retry
*
* Depending on the scenario, the path should or should not be added. E.g. `$first` could
* be called after a redirect response from the server. The server likely already has the
* correct path.
*
* No solution fits all use cases. This implementation will work fine for the common use cases.
* If you have a specific situation where this is not the right thing, you can build a custom plugin
* that does exactly what you need.
*
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
$prepend = $this->uri->getPath();
$path = $request->getUri()->getPath();
if (substr($path, 0, strlen($prepend)) !== $prepend) {
$request = $request->withUri($request->getUri()
->withPath($prepend.$path)
);
}
return $next($request);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\Authentication;
use Http\Promise\Promise;
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;
public function __construct(Authentication $authentication)
{
$this->authentication = $authentication;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
$request = $this->authentication->authenticate($request);
return $next($request);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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 can 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): Promise
{
$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,39 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\Encoding\ChunkStream;
use Http\Promise\Promise;
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): Promise
{
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,122 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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)
*/
private $skipDetection;
/**
* Determine the size stream limit for which the detection as to be skipped (default to 16Mb).
*
* @var int
*/
private $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): Promise
{
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);
}
private function isJson(StreamInterface $stream): bool
{
if (!function_exists('json_decode')) {
return false;
}
$stream->rewind();
json_decode($stream->getContents());
return JSON_ERROR_NONE === json_last_error();
}
private function isXml(StreamInterface $stream): bool
{
if (!function_exists('simplexml_load_string')) {
return false;
}
$stream->rewind();
$previousValue = libxml_use_internal_errors(true);
$isXml = simplexml_load_string($stream->getContents());
libxml_use_internal_errors($previousValue);
return false !== $isXml;
}
}

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
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 Http\Promise\Promise;
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;
public function __construct(CookieJar $cookieJar)
{
$this->cookieJar = $cookieJar;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
$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.
*
* @throws TransferException
*/
private function createCookie(RequestInterface $request, string $setCookieHeader): ?Cookie
{
$parts = array_map('trim', explode(';', $setCookieHeader));
if (empty($parts) || !strpos($parts[0], '=')) {
return null;
}
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((string) $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 string $part A single cookie value in format key=value
*
* @return array{0:string, 1:?string}
*/
private function createValueKey(string $part): array
{
$parts = explode('=', $part, 2);
$key = trim($parts[0]);
$value = isset($parts[1]) ? trim($parts[1]) : null;
return [$key, $value];
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\Encoding;
use Http\Promise\Promise;
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): Promise
{
$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.
*/
private function decodeResponse(ResponseInterface $response): ResponseInterface
{
$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).
*/
private function decodeOnEncodingHeader(string $headerName, ResponseInterface $response): ResponseInterface
{
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.
*
* @return StreamInterface|false A new stream interface or false if encoding is not supported
*/
private function decorateStream(string $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,92 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Exception\ClientErrorException;
use Http\Client\Common\Exception\ServerErrorException;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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.
*
* Warning
* =======
*
* Throwing an exception on a valid response violates the PSR-18 specification.
* This plugin is provided as a convenience when writing a small application.
* When providing a client to a third party library, this plugin must not be
* included, or the third party library will have problems with error handling.
*
* @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
{
$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
*/
private function transformResponseToException(RequestInterface $request, ResponseInterface $response): ResponseInterface
{
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,48 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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): Promise
{
foreach ($this->headers as $header => $headerValue) {
$request = $request->withAddedHeader($header, $headerValue);
}
return $next($request);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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): Promise
{
foreach ($this->headers as $header => $headerValue) {
if (!$request->hasHeader($header)) {
$request = $request->withHeader($header, $headerValue);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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): Promise
{
foreach ($this->headers as $header) {
if ($request->hasHeader($header)) {
$request = $request->withoutHeader($header);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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): Promise
{
foreach ($this->headers as $header => $headerValue) {
$request = $request->withHeader($header, $headerValue);
}
return $next($request);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
use Psr\Http\Client\ClientExceptionInterface;
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;
public function __construct(Journal $journal)
{
$this->journal = $journal;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
$journal = $this->journal;
return $next($request)->then(function (ResponseInterface $response) use ($request, $journal) {
$journal->addSuccess($request, $response);
return $response;
}, function (ClientExceptionInterface $exception) use ($request, $journal) {
$journal->addFailure($request, $exception);
throw $exception;
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Psr\Http\Client\ClientExceptionInterface;
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 ClientExceptionInterface $exception Exception returned by the call
*/
public function addFailure(RequestInterface $request, ClientExceptionInterface $exception);
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Promise\Promise;
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): Promise
{
$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,267 @@
<?php
declare(strict_types=1);
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 Http\Promise\Promise;
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>
*/
final class RedirectPlugin implements Plugin
{
/**
* Rule on how to redirect, change method for the new request.
*
* @var array
*/
private $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
*/
private $preserveHeader;
/**
* Store all previous redirect from 301 / 308 status code.
*
* @var array
*/
private $redirectStorage = [];
/**
* Whether the location header must be directly used for a multiple redirection status code (300).
*
* @var bool
*/
private $useDefaultForMultiple;
/**
* @var string[][] Chain identifier => list of URLs for this chain
*/
private $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)
* @var bool $strict When true, redirect codes 300, 301, 302 will not modify request method and body.
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'preserve_header' => true,
'use_default_for_multiple' => true,
'strict' => false,
]);
$resolver->setAllowedTypes('preserve_header', ['bool', 'array']);
$resolver->setAllowedTypes('use_default_for_multiple', 'bool');
$resolver->setAllowedTypes('strict', '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'];
if ($options['strict']) {
$this->redirectCodes[300]['switch'] = false;
$this->redirectCodes[301]['switch'] = false;
$this->redirectCodes[302]['switch'] = false;
}
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
// 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): ResponseInterface {
$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 synchronously
$redirectPromise = $first($redirectRequest);
return $redirectPromise->wait();
});
}
private function buildRedirectRequest(RequestInterface $originalRequest, UriInterface $targetUri, int $statusCode): RequestInterface
{
$originalRequest = $originalRequest->withUri($targetUri);
if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($originalRequest->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) {
$originalRequest = $originalRequest->withMethod($this->redirectCodes[$statusCode]['switch']['to']);
}
if (is_array($this->preserveHeader)) {
$headers = array_keys($originalRequest->getHeaders());
foreach ($headers as $name) {
if (!in_array($name, $this->preserveHeader)) {
$originalRequest = $originalRequest->withoutHeader($name);
}
}
}
return $originalRequest;
}
/**
* Creates a new Uri from the old request and the location header.
*
* @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)
*/
private function createUri(ResponseInterface $redirectResponse, RequestInterface $originalRequest): UriInterface
{
if ($this->redirectCodes[$redirectResponse->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$redirectResponse->hasHeader('Location'))) {
throw new MultipleRedirectionException('Cannot choose a redirection', $originalRequest, $redirectResponse);
}
if (!$redirectResponse->hasHeader('Location')) {
throw new HttpException('Redirect status code, but no location header present in the response', $originalRequest, $redirectResponse);
}
$location = $redirectResponse->getHeaderLine('Location');
$parsedLocation = parse_url($location);
if (false === $parsedLocation) {
throw new HttpException(sprintf('Location %s could not be parsed', $location), $originalRequest, $redirectResponse);
}
$uri = $originalRequest->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,56 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Message\RequestMatcher;
use Http\Promise\Promise;
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|null
*/
private $successPlugin;
/**
* @var Plugin|null
*/
private $failurePlugin;
public function __construct(RequestMatcher $requestMatcher, ?Plugin $delegateOnMatch, Plugin $delegateOnNoMatch = null)
{
$this->requestMatcher = $requestMatcher;
$this->successPlugin = $delegateOnMatch;
$this->failurePlugin = $delegateOnNoMatch;
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
if ($this->requestMatcher->matches($request)) {
if (null !== $this->successPlugin) {
return $this->successPlugin->handleRequest($request, $next, $first);
}
} elseif (null !== $this->failurePlugin) {
return $this->failurePlugin->handleRequest($request, $next, $first);
}
return $next($request);
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Message\Stream\BufferedStream;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
/**
* Allow body used in request to be always seekable.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class RequestSeekableBodyPlugin extends SeekableBodyPlugin
{
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
if (!$request->getBody()->isSeekable()) {
$request = $request->withBody(new BufferedStream($request->getBody(), $this->useFileBuffer, $this->memoryBufferSize));
}
return $next($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Message\Stream\BufferedStream;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Allow body used in response to be always seekable.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class ResponseSeekableBodyPlugin extends SeekableBodyPlugin
{
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
return $next($request)->then(function (ResponseInterface $response) {
if ($response->getBody()->isSeekable()) {
return $response;
}
return $response->withBody(new BufferedStream($response->getBody(), $this->useFileBuffer, $this->memoryBufferSize));
});
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Http\Client\Exception\HttpException;
use Http\Promise\Promise;
use Psr\Http\Client\ClientExceptionInterface;
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 $errorResponseDelay;
/**
* @var callable
*/
private $errorResponseDecider;
/**
* @var callable
*/
private $exceptionDecider;
/**
* @var callable
*/
private $exceptionDelay;
/**
* 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 $error_response_decider A callback that gets a request and response to decide whether the request should be retried
* @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 $error_response_delay A callback that gets a request and response and the current number of retries and returns how many microseconds we should wait before trying again
* @var callable $exception_delay A callback that gets a request, an exception and the current number of retries and returns how many microseconds we should wait before trying again
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'retries' => 1,
'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) {
// do not retry client errors
return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600;
},
'exception_decider' => function (RequestInterface $request, ClientExceptionInterface $e) {
// do not retry client errors
return !$e instanceof HttpException || $e->getCode() >= 500 && $e->getCode() < 600;
},
'error_response_delay' => __CLASS__.'::defaultErrorResponseDelay',
'exception_delay' => __CLASS__.'::defaultExceptionDelay',
]);
$resolver->setAllowedTypes('retries', 'int');
$resolver->setAllowedTypes('error_response_decider', 'callable');
$resolver->setAllowedTypes('exception_decider', 'callable');
$resolver->setAllowedTypes('error_response_delay', 'callable');
$resolver->setAllowedTypes('exception_delay', 'callable');
$options = $resolver->resolve($config);
$this->retry = $options['retries'];
$this->errorResponseDecider = $options['error_response_decider'];
$this->errorResponseDelay = $options['error_response_delay'];
$this->exceptionDecider = $options['exception_decider'];
$this->exceptionDelay = $options['exception_delay'];
}
/**
* {@inheritdoc}
*/
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
{
$chainIdentifier = spl_object_hash((object) $first);
return $next($request)->then(function (ResponseInterface $response) 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]);
return $response;
}
if (call_user_func($this->errorResponseDecider, $request, $response)) {
$time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]);
$response = $this->retry($request, $next, $first, $chainIdentifier, $time);
}
if (array_key_exists($chainIdentifier, $this->retryStorage)) {
unset($this->retryStorage[$chainIdentifier]);
}
return $response;
}, function (ClientExceptionInterface $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]);
return $this->retry($request, $next, $first, $chainIdentifier, $time);
});
}
/**
* @param int $retries The number of retries we made before. First time this get called it will be 0.
*/
public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, int $retries): int
{
return pow(2, $retries) * 500000;
}
/**
* @param int $retries The number of retries we made before. First time this get called it will be 0.
*/
public static function defaultExceptionDelay(RequestInterface $request, ClientExceptionInterface $e, int $retries): int
{
return pow(2, $retries) * 500000;
}
/**
* @throws \Exception if retrying returns a failed promise
*/
private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay): ResponseInterface
{
usleep($delay);
// Retry synchronously
++$this->retryStorage[$chainIdentifier];
$promise = $this->handleRequest($request, $next, $first);
return $promise->wait();
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Client\Common\Plugin;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @internal
*/
abstract class SeekableBodyPlugin implements Plugin
{
/**
* @var bool
*/
protected $useFileBuffer;
/**
* @var int
*/
protected $memoryBufferSize;
/**
* @param array $config {
*
* @var bool $use_file_buffer Whether this plugin should use a file as a buffer if the stream is too big, defaults to true
* @var int $memory_buffer_size Max memory size in bytes to use for the buffer before it use a file, defaults to 2097152 (2 mb)
* }
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'use_file_buffer' => true,
'memory_buffer_size' => 2097152,
]);
$resolver->setAllowedTypes('use_file_buffer', 'bool');
$resolver->setAllowedTypes('memory_buffer_size', 'int');
$options = $resolver->resolve($config);
$this->useFileBuffer = $options['use_file_buffer'];
$this->memoryBufferSize = $options['memory_buffer_size'];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common\Plugin;
use Http\Promise\Promise;
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): Promise
{
return $this->doHandleRequest($request, $next, $first);
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use function array_reverse;
use Http\Client\Common\Exception\LoopException;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
final class PluginChain
{
/** @var Plugin[] */
private $plugins;
/** @var callable(RequestInterface): Promise */
private $clientCallable;
/** @var int */
private $maxRestarts;
/** @var int */
private $restarts = 0;
/**
* @param Plugin[] $plugins A plugin chain
* @param callable(RequestInterface): Promise $clientCallable Callable making the HTTP call
* @param array $options {
*
* @var int $max_restarts
* }
*/
public function __construct(array $plugins, callable $clientCallable, array $options = [])
{
$this->plugins = $plugins;
$this->clientCallable = $clientCallable;
$this->maxRestarts = (int) ($options['max_restarts'] ?? 0);
}
private function createChain(): callable
{
$lastCallable = $this->clientCallable;
$reversedPlugins = array_reverse($this->plugins);
foreach ($reversedPlugins as $plugin) {
$lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable) {
return $plugin->handleRequest($request, $lastCallable, $this);
};
}
return $lastCallable;
}
public function __invoke(RequestInterface $request): Promise
{
if ($this->restarts > $this->maxRestarts) {
throw new LoopException('Too many restarts in plugin client', $request);
}
++$this->restarts;
return $this->createChain()($request);
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
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 Http\Promise\Promise;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
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 ClientInterface|HttpAsyncClient $client An HTTP async client
* @param Plugin[] $plugins A plugin chain
* @param array $options {
*
* @var int $max_restarts
* }
*/
public function __construct($client, array $plugins = [], array $options = [])
{
if ($client instanceof HttpAsyncClient) {
$this->client = $client;
} elseif ($client instanceof ClientInterface) {
$this->client = new EmulatedHttpAsyncClient($client);
} else {
throw new \TypeError(
sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client))
);
}
$this->plugins = $plugins;
$this->options = $this->configure($options);
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
// If the client doesn't support sync calls, call async
if (!$this->client instanceof ClientInterface) {
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.
*/
private function configure(array $options = []): array
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'max_restarts' => 10,
]);
$resolver->setAllowedTypes('max_restarts', 'int');
return $resolver->resolve($options);
}
/**
* Create the plugin chain.
*
* @param Plugin[] $plugins A plugin chain
* @param callable $clientCallable Callable making the HTTP call
*
* @return callable(RequestInterface): Promise
*/
private function createPluginChain(array $plugins, callable $clientCallable): callable
{
/** @var callable(RequestInterface): Promise */
return new PluginChain($plugins, $clientCallable, $this->options);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
use Psr\Http\Client\ClientInterface;
/**
* Build an instance of a PluginClient with a dynamic list of plugins.
*
* @author Baptiste Clavié <clavie.b@gmail.com>
*/
final class PluginClientBuilder
{
/** @var Plugin[][] List of plugins ordered by priority [priority => Plugin[]]). */
private $plugins = [];
/** @var array Array of options to give to the plugin client */
private $options = [];
/**
* @param int $priority Priority of the plugin. The higher comes first.
*/
public function addPlugin(Plugin $plugin, int $priority = 0): self
{
$this->plugins[$priority][] = $plugin;
return $this;
}
/**
* @param mixed $value
*/
public function setOption(string $name, $value): self
{
$this->options[$name] = $value;
return $this;
}
public function removeOption(string $name): self
{
unset($this->options[$name]);
return $this;
}
/**
* @param ClientInterface|HttpAsyncClient $client
*/
public function createClient($client): PluginClient
{
if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) {
throw new \TypeError(
sprintf('%s::createClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client))
);
}
$plugins = $this->plugins;
if (0 === count($plugins)) {
$plugins[] = [];
}
krsort($plugins);
$plugins = array_merge(...$plugins);
return new PluginClient(
$client,
array_values($plugins),
$this->options
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Http\Client\HttpAsyncClient;
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(ClientInterface|HttpAsyncClient, Plugin[], array): PluginClient)|null
*/
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(ClientInterface|HttpAsyncClient, Plugin[], array): PluginClient $factory
*/
public static function setFactory(callable $factory): void
{
static::$factory = $factory;
}
/**
* @param ClientInterface|HttpAsyncClient $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.
*/
public function createClient($client, array $plugins = [], array $options = []): PluginClient
{
if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) {
throw new \TypeError(
sprintf('%s::createClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client))
);
}
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,24 @@
<?php
declare(strict_types=1);
namespace Http\Client\Common;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* 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): ResponseInterface
{
return $this->doSendRequest($request);
}
}

View File

@@ -0,0 +1,112 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [2.0.2] - 2021-03-02
### Added
- Add Support for PHP 8
## [2.0.1] - 2018-12-16
### Fixed
- `\Http\Adapter\Guzzle6\Client::sendRequest` no longer throws any exceptions that do not implement
the PSR exception interface.
Instead of `\UnexpectedValueException` we now throw `Http\Adapter\Guzzle6\Exception\UnexpectedValueException`
(which extends `\UnexpectedValueException` and implements `Psr\Http\Client\ClientExceptionInterface`).
Instead of `\RuntimeException` we now throw `Http\Client\Exception\TransferException`
(which extends `\RuntimeException` and implements `Psr\Http\Client\ClientExceptionInterface`).
## [2.0.0] - 2018-11-14
### Added
- Support for HTTPlug 2.0 and PSR-18
### Changed
- `Client` and `Promise` are both final
### Removed
- Support for PHP <7.1
## [1.1.1] - 2016-05-10
### Fixed
- Adapter can again be instantiated without a guzzle client.
## [1.1.0] - 2016-05-09
### Added
- Factory method Client::createWithConfig to create an adapter with custom
configuration for the underlying guzzle client.
## [1.0.0] - 2016-01-26
## [0.4.1] - 2016-01-13
### Changed
- Updated integration tests
### Removed
- Client common dependency
## [0.4.0] - 2016-01-12
### Changed
- Updated package files
- Updated HTTPlug to RC1
## [0.3.1] - 2015-12-31
## [0.3.0] - 2015-12-31
## [0.2.1] - 2015-12-17
### Added
- Puli configuration and bindings
### Changed
- Guzzle setup conforms to HTTPlug requirement now: Minimal functionality in client
## [0.2.0] - 2015-12-15
### Added
- Async client capabalities
### Changed
- HTTPlug instead of HTTP Adapter
## 0.1.0 - 2015-06-12
### Added
- Initial release

20
vendor/php-http/guzzle6-adapter/LICENSE vendored Normal file
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,62 @@
# Guzzle 6 HTTP Adapter
[![Latest Version](https://img.shields.io/github/release/php-http/guzzle6-adapter.svg?style=flat-square)](https://github.com/php-http/guzzle6-adapter/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/guzzle6-adapter.svg?style=flat-square)](https://travis-ci.org/php-http/guzzle6-adapter)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/guzzle6-adapter.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/guzzle6-adapter)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/guzzle6-adapter.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/guzzle6-adapter)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/guzzle6-adapter.svg?style=flat-square)](https://packagist.org/packages/php-http/guzzle6-adapter)
**Guzzle 6 HTTP Adapter.**
**Note for PHP 8**: This adapter can now be installed with PHP 8, even though Guzzle 6 is not tested on PHP 8.
If you need a PSR-18 client, use Guzzle 7 which natively implements PSR-18.
If you need the HTTPlug interfaces for asynchronous calls or for a system that still requires HTTPlug, use the [guzzle7-adapter](https://github.com/php-http/guzzle7-adapter/) instead of this repository.
## Install
Via Composer
``` bash
$ composer require php-http/guzzle6-adapter
```
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/clients/guzzle6-adapter.html).
## Testing
First launch the http server:
```bash
$ ./vendor/bin/http_test_server > /dev/null 2>&1 &
```
Then the test suite:
``` 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).
## Credits
Thanks to [David de Boer](https://github.com/ddeboer) for implementing this adapter.
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,52 @@
{
"name": "php-http/guzzle6-adapter",
"description": "Guzzle 6 HTTP Adapter",
"license": "MIT",
"keywords": ["guzzle", "http"],
"homepage": "http://httplug.io",
"authors": [
{
"name": "David de Boer",
"email": "david@ddeboer.nl"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": "^7.1 || ^8.0",
"php-http/httplug": "^2.0",
"psr/http-client": "^1.0",
"guzzlehttp/guzzle": "^6.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^7.4 || ^8.4",
"php-http/client-integration-tests": "^2.0 || ^3.0"
},
"provide": {
"php-http/client-implementation": "1.0",
"php-http/async-client-implementation": "1.0",
"psr/http-client-implementation": "1.0"
},
"autoload": {
"psr-4": {
"Http\\Adapter\\Guzzle6\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Http\\Adapter\\Guzzle6\\Tests\\": "tests/"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"version": "1.0",
"name": "php-http/guzzle6-adapter",
"bindings": {
"04b5a002-71a8-473d-a8df-75671551b84a": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Adapter\\Guzzle6\\Client",
"type": "Http\\Client\\HttpClient"
},
"9c856476-7f6b-43df-a740-15420a5f839c": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Adapter\\Guzzle6\\Client",
"type": "Http\\Client\\HttpAsyncClient"
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Http\Adapter\Guzzle6;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* HTTP Adapter for Guzzle 6.
*
* @author David de Boer <david@ddeboer.nl>
*/
final class Client implements HttpClient, HttpAsyncClient
{
/**
* @var ClientInterface
*/
private $client;
/**
* If you pass a Guzzle instance as $client, make sure to configure Guzzle to not
* throw exceptions on HTTP error status codes, or this adapter will violate PSR-18.
* See also self::buildClient at the bottom of this class.
*/
public function __construct(?ClientInterface $client = null)
{
if (!$client) {
$client = self::buildClient();
}
$this->client = $client;
}
/**
* Factory method to create the Guzzle 6 adapter with custom Guzzle configuration.
*/
public static function createWithConfig(array $config): Client
{
return new self(self::buildClient($config));
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
$promise = $this->sendAsyncRequest($request);
return $promise->wait();
}
/**
* {@inheritdoc}
*/
public function sendAsyncRequest(RequestInterface $request)
{
$promise = $this->client->sendAsync($request);
return new Promise($promise, $request);
}
/**
* Build the Guzzle client instance.
*/
private static function buildClient(array $config = []): GuzzleClient
{
$handlerStack = new HandlerStack(\GuzzleHttp\choose_handler());
$handlerStack->push(Middleware::prepareBody(), 'prepare_body');
$config = array_merge(['handler' => $handlerStack], $config);
return new GuzzleClient($config);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Http\Adapter\Guzzle6\Exception;
use Http\Client\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements Exception
{
}

View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Http\Adapter\Guzzle6;
use GuzzleHttp\Exception as GuzzleExceptions;
use GuzzleHttp\Promise\PromiseInterface;
use Http\Adapter\Guzzle6\Exception\UnexpectedValueException;
use Http\Client\Exception as HttplugException;
use Http\Promise\Promise as HttpPromise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Wrapper around Guzzle promises.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class Promise implements HttpPromise
{
/**
* @var PromiseInterface
*/
private $promise;
/**
* @var string State of the promise
*/
private $state;
/**
* @var ResponseInterface
*/
private $response;
/**
* @var HttplugException
*/
private $exception;
/**
* @var RequestInterface
*/
private $request;
public function __construct(PromiseInterface $promise, RequestInterface $request)
{
$this->request = $request;
$this->state = self::PENDING;
$this->promise = $promise->then(function ($response) {
$this->response = $response;
$this->state = self::FULFILLED;
return $response;
}, function ($reason) use ($request) {
$this->state = self::REJECTED;
if ($reason instanceof HttplugException) {
$this->exception = $reason;
} elseif ($reason instanceof GuzzleExceptions\GuzzleException) {
$this->exception = $this->handleException($reason, $request);
} elseif ($reason instanceof \Throwable) {
$this->exception = new HttplugException\TransferException('Invalid exception returned from Guzzle6', 0, $reason);
} else {
$this->exception = new UnexpectedValueException('Reason returned from Guzzle6 must be an Exception');
}
throw $this->exception;
});
}
/**
* {@inheritdoc}
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
return new static($this->promise->then($onFulfilled, $onRejected), $this->request);
}
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->state;
}
/**
* {@inheritdoc}
*/
public function wait($unwrap = true)
{
$this->promise->wait(false);
if ($unwrap) {
if (self::REJECTED == $this->getState()) {
throw $this->exception;
}
return $this->response;
}
}
/**
* Converts a Guzzle exception into an Httplug exception.
*/
private function handleException(GuzzleExceptions\GuzzleException $exception, RequestInterface $request): HttplugException
{
if ($exception instanceof GuzzleExceptions\SeekException) {
return new HttplugException\RequestException($exception->getMessage(), $request, $exception);
}
if ($exception instanceof GuzzleExceptions\ConnectException) {
return new HttplugException\NetworkException($exception->getMessage(), $exception->getRequest(), $exception);
}
if ($exception instanceof GuzzleExceptions\RequestException) {
// Make sure we have a response for the HttpException
if ($exception->hasResponse()) {
return new HttplugException\HttpException(
$exception->getMessage(),
$exception->getRequest(),
$exception->getResponse(),
$exception
);
}
return new HttplugException\RequestException($exception->getMessage(), $exception->getRequest(), $exception);
}
return new HttplugException\TransferException($exception->getMessage(), 0, $exception);
}
}

View File

@@ -0,0 +1,16 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->name('*.php')
;
$config = (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
])
->setFinder($finder)
;
return $config;

144
vendor/php-http/httplug/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,144 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [2.4.1] - 2024-09-23
- Updated code to not raise warnings for nullable parameters in PHP 8.4.
## [2.4.0] - 2023-04-14
### Changed
- Allow `psr/http-message` v2 in addition to v1
- Deprecate `Http\Client\HttpClient`, use [PSR-18](https://www.php-fig.org/psr/psr-18/) instead
## [2.3.0] - 2022-02-21
### Changed
- Enabled the `$onRejected` callback of `HttpRejectedPromise` to return a promise for implementing a retry
mechanism [#168](https://github.com/php-http/httplug/pull/168)
## [2.2.0] - 2020-07-13
### Changed
- Support PHP 7.1-8.0
## [2.1.0] - 2019-12-27
### Changed
- `Http\Client\Exception\NetworkException` no longer extends `Http\Client\Exception\RequestException`,
in accordance with [PSR-18](https://www.php-fig.org/psr/psr-18/)
## [2.0.0] - 2018-10-31
This version is no BC break for consumers using HTTPlug. However, HTTP clients that
implement HTTPlug need to adjust because we add return type declarations.
### Added
- Support for PSR-18 (HTTP client).
### Changed
- **BC Break:** `HttpClient::sendRequest(RequestInterface $request)` has a return type annotation. The new
signature is `HttpClient::sendRequest(RequestInterface $request): ResponseInterface`.
- **BC Break:** `RequestException::getRequest()` has a return type annotation. The new
signature is `RequestException::getRequest(): RequestInterface`.
### Removed
- PHP 5 support
## [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
[Unreleased]: https://github.com/php-http/httplug/compare/v2.0.0...HEAD
[2.0.0]: https://github.com/php-http/httplug/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/php-http/httplug/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/php-http/httplug/compare/v1.0.0-RC1...v1.0.0
[1.0.0-RC1]: https://github.com/php-http/httplug/compare/v1.0.0-beta...v1.0.0-RC1
[1.0.0-beta]: https://github.com/php-http/httplug/compare/v1.0.0-alpha3...v1.0.0-beta
[1.0.0-alpha3]: https://github.com/php-http/httplug/compare/v1.0.0-alpha2...v1.0.0-alpha3
[1.0.0-alpha2]: https://github.com/php-http/httplug/compare/v1.0.0-alpha...v1.0.0-alpha2
[1.0.0-alpha]: https://github.com/php-http/httplug/compare/v0.1.0...v1.0.0-alpha

20
vendor/php-http/httplug/LICENSE vendored Normal file
View File

@@ -0,0 +1,20 @@
Copyright (c) 2014 Eric GELOEN <geloen.eric@gmail.com>
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.

54
vendor/php-http/httplug/README.md vendored Normal file
View File

@@ -0,0 +1,54 @@
# 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://github.com/php-http/httplug/actions/workflows/ci.yml/badge.svg)](https://github.com/php-http/httplug/actions/workflows/ci.yml)
[![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)
[![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.**
## Intro
HTTP client standard built on [PSR-7](http://www.php-fig.org/psr/psr-7/) HTTP
messages. The HttpAsyncClient defines an asynchronous HTTP client for PHP.
This package also provides a synchronous HttpClient interface with the same
method signature as the [PSR-18](http://www.php-fig.org/psr/psr-18/) client.
For synchronous requests, we recommend using PSR-18 directly.
## History
HTTPlug is the official successor of the [ivory http adapter](https://github.com/egeloen/ivory-http-adapter).
HTTPlug is a predecessor of [PSR-18](http://www.php-fig.org/psr/psr-18/)
## Install
Via Composer
``` bash
$ composer require php-http/httplug
```
## Documentation
Please see the [official documentation](http://docs.php-http.org).
## Testing
``` bash
$ composer test
```
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

40
vendor/php-http/httplug/composer.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "php-http/httplug",
"description": "HTTPlug, the HTTP client abstraction for PHP",
"keywords": [
"http",
"client"
],
"homepage": "http://httplug.io",
"license": "MIT",
"authors": [
{
"name": "Eric GELOEN",
"email": "geloen.eric@gmail.com"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"require": {
"php": "^7.1 || ^8.0",
"php-http/promise": "^1.1",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0 || ^2.0"
},
"require-dev": {
"friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0",
"phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
},
"autoload": {
"psr-4": {
"Http\\Client\\": "src/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
}
}

12
vendor/php-http/httplug/puli.json vendored Normal file
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,14 @@
<?php
namespace Http\Client;
use Psr\Http\Client\ClientExceptionInterface as PsrClientException;
/**
* Every HTTP Client related Exception must implement this interface.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Exception extends PsrClientException
{
}

View File

@@ -0,0 +1,65 @@
<?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
*/
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.
*/
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 static($message, $request, $response, $previous);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Client\NetworkExceptionInterface as PsrNetworkException;
use Psr\Http\Message\RequestInterface;
/**
* 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 TransferException implements PsrNetworkException
{
use RequestAwareTrait;
/**
* @param string $message
*/
public function __construct($message, RequestInterface $request, ?\Exception $previous = null)
{
$this->setRequest($request);
parent::__construct($message, 0, $previous);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
trait RequestAwareTrait
{
/**
* @var RequestInterface
*/
private $request;
private function setRequest(RequestInterface $request)
{
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Client\RequestExceptionInterface as PsrRequestException;
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 implements PsrRequestException
{
use RequestAwareTrait;
/**
* @param string $message
*/
public function __construct($message, RequestInterface $request, ?\Exception $previous = null)
{
$this->setRequest($request);
parent::__construct($message, 0, $previous);
}
}

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,25 @@
<?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.
*
* @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);
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Http\Client;
use Psr\Http\Client\ClientInterface;
/**
* {@inheritdoc}
*
* Provide the Httplug HttpClient interface for BC.
* You should typehint Psr\Http\Client\ClientInterface in new code
*
* @deprecated since version 2.4, use Psr\Http\Client\ClientInterface instead; see https://www.php-fig.org/psr/psr-18/
*/
interface HttpClient extends ClientInterface
{
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Client\Promise;
use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\ResponseInterface;
final class HttpFulfilledPromise implements Promise
{
/**
* @var ResponseInterface
*/
private $response;
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
{
if (null === $onFulfilled) {
return $this;
}
try {
return new self($onFulfilled($this->response));
} catch (Exception $e) {
return new HttpRejectedPromise($e);
}
}
public function getState()
{
return Promise::FULFILLED;
}
public function wait($unwrap = true)
{
if ($unwrap) {
return $this->response;
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Http\Client\Promise;
use Http\Client\Exception;
use Http\Promise\Promise;
final class HttpRejectedPromise implements Promise
{
/**
* @var Exception
*/
private $exception;
public function __construct(Exception $exception)
{
$this->exception = $exception;
}
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
{
if (null === $onRejected) {
return $this;
}
try {
$result = $onRejected($this->exception);
if ($result instanceof Promise) {
return $result;
}
return new HttpFulfilledPromise($result);
} catch (Exception $e) {
return new self($e);
}
}
public function getState()
{
return Promise::REJECTED;
}
public function wait($unwrap = true)
{
if ($unwrap) {
throw $this->exception;
}
}
}

View File

@@ -0,0 +1,72 @@
# Change Log
## 1.1.0 - 2023-04-14
### Changed
- Allow `psr/http-message` v2 in addition to v1
- Deprecate all interfaces in favor of [PSR-17](https://www.php-fig.org/psr/psr-17/)
## 1.0.2 - 2015-12-19
### Added
- Request and Response factory binding types to Puli
## 1.0.1 - 2015-12-17
### Added
- Puli configuration and binding types
## 1.0.0 - 2015-12-15
### Added
- Response Factory in order to be reused in Message and Server Message factories
- Request Factory
### Changed
- Message Factory extends Request and Response factories
## 1.0.0-RC1 - 2015-12-14
### Added
- CS check
### Changed
- RuntimeException is thrown when the StreamFactory cannot write to the underlying stream
## 0.3.0 - 2015-11-16
### Removed
- Client Context Factory
- Factory Awares and Templates
## 0.2.0 - 2015-11-16
### Changed
- Reordered the parameters when creating a message to have the protocol last,
as its the least likely to need to be changed.
## 0.1.0 - 2015-06-01
### Added
- Initial release
### Changed
- Helpers are renamed to templates

View File

@@ -0,0 +1 @@
Please see http://docs.php-http.org/en/latest/development/contributing.html

19
vendor/php-http/message-factory/LICENSE vendored Normal file
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,42 @@
# PSR-7 Message Factory
[![Latest Version](https://img.shields.io/github/release/php-http/message-factory.svg?style=flat-square)](https://github.com/php-http/message-factory/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/message-factory.svg?style=flat-square)](https://packagist.org/packages/php-http/message-factory)
**Factory interfaces for PSR-7 HTTP Message.**
## Obsolete
The PHP-HTTP factories have become obsolete with the [PSR-17](https://www.php-fig.org/psr/psr-17/) factories standard.
All major HTTP client implementors provide [PSR-17 factories](https://packagist.org/packages/psr/http-factory).
This package will remain available for the time being to not break legacy code, but we encourage everybody to move to PSR-17.
## Install
Via Composer
``` bash
$ composer require php-http/message-factory
```
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/message/message-factory.html).
## 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,27 @@
{
"name": "php-http/message-factory",
"description": "Factory interfaces for PSR-7 HTTP Message",
"license": "MIT",
"keywords": ["http", "factory", "message", "stream", "uri"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": ">=5.4",
"psr/http-message": "^1.0 || ^2.0"
},
"autoload": {
"psr-4": {
"Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
}
}

View File

@@ -0,0 +1,44 @@
{
"version": "1.0",
"name": "php-http/message-factory",
"binding-types": {
"Http\\Message\\MessageFactory": {
"description": "PSR-7 Message Factory",
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\RequestFactory": {
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\ResponseFactory": {
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\StreamFactory": {
"description": "PSR-7 Stream Factory",
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\UriFactory": {
"description": "PSR-7 URI Factory",
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Message;
/**
* Factory for PSR-7 Request and Response.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since version 1.1, use Psr\Http\Message\RequestFactoryInterface and Psr\Http\Message\ResponseFactoryInterface instead.
*/
interface MessageFactory extends RequestFactory, ResponseFactory
{
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Http\Message;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
/**
* Factory for PSR-7 Request.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since version 1.1, use Psr\Http\Message\RequestFactoryInterface instead.
*/
interface RequestFactory
{
/**
* Creates a new PSR-7 request.
*
* @param string $method
* @param string|UriInterface $uri
* @param array $headers
* @param resource|string|StreamInterface|null $body
* @param string $protocolVersion
*
* @return RequestInterface
*/
public function createRequest(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Http\Message;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Factory for PSR-7 Response.
*
* This factory contract can be reused in Message and Server Message factories.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since version 1.1, use Psr\Http\Message\ResponseFactoryInterface instead.
*/
interface ResponseFactory
{
/**
* Creates a new PSR-7 response.
*
* @param int $statusCode
* @param string|null $reasonPhrase
* @param array $headers
* @param resource|string|StreamInterface|null $body
* @param string $protocolVersion
*
* @return ResponseInterface
*/
public function createResponse(
$statusCode = 200,
$reasonPhrase = null,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Http\Message;
use Psr\Http\Message\StreamInterface;
/**
* Factory for PSR-7 Stream.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since version 1.1, use Psr\Http\Message\StreamFactoryInterface instead.
*/
interface StreamFactory
{
/**
* Creates a new PSR-7 stream.
*
* @param string|resource|StreamInterface|null $body
*
* @return StreamInterface
*
* @throws \InvalidArgumentException if the stream body is invalid
* @throws \RuntimeException if creating the stream from $body fails
*/
public function createStream($body = null);
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Http\Message;
use Psr\Http\Message\UriInterface;
/**
* Factory for PSR-7 URI.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since version 1.1, use Psr\Http\Message\UriFactoryInterface instead.
*/
interface UriFactory
{
/**
* Creates an PSR-7 URI.
*
* @param string|UriInterface $uri
*
* @return UriInterface
*
* @throws \InvalidArgumentException if the $uri argument can not be converted into a valid URI
*/
public function createUri($uri);
}

View File

@@ -0,0 +1,19 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->in(__DIR__.'/spec')
->name('*.php')
;
$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'single_line_throw' => false,
'trailing_comma_in_multiline' => false, // for methods this is incompatible with PHP 7
])
->setFinder($finder)
;

278
vendor/php-http/message/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,278 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [1.16.1] - 2024-03-07
- Adjust phpdoc to avoid warnings about return types.
## [1.16.0] - 2023-05-17
- Remove direct dependency on `php-http/message-factory` as it is only needed for the deprecated Httplug factories.
Upgrade to PSR-17 message factories provided by your HTTP client implementation.
If you need the Httplug factories for the time being, you can `composer require php-http/message-factory` in your application.
## [1.15.0] - 2023-05-10
**If you use the decorator classes, you might need to adjust your code to the parameter and return type declarations added in PSR-7**
- Actually make compatible with PSR-7 1.1 and 2.0
- Drop support for PHP 7.1
## [1.14.0] - 2023-04-14 (broken)
**This release is not actually compatible with PSR-7 1.1 or 2.0**
- Allow installation with http-message (PSR-7) version 2 in addition to version 1.
- Support for PHP 8.2
## [1.13.0] - 2022-02-11
- Added `Formatter::formatResponseForRequest()` to allow the formatter to get context from the request to decide what of the response to output.
- Deprecated `Formatter::formatResponse()` in favor of the new `formatResponseForRequest` method.
## [1.12.0] - 2021-08-29
- Added support for adjusting binary detection regex in FullHttpMessageFormatter.
## [1.11.2] - 2021-08-03
- Support GuzzleHttp/Psr7 version 2.0 in the (deprecated) GuzzleStreamFactory.
## [1.11.1] - 2021-05-24
- Support GuzzleHttp/Psr7 version 2.0 in the (deprecated) GuzzleUriFactory.
## [1.11.0] - 2020-02-01
- Migrated from `zendframework/zend-diactoros` to `laminas/laminas-diactoros`.
Users are encouraged to update their dependencies by simply replacing the Zend package with the Laminas package.
Due to the [laminas-zendframework-brige](https://github.com/laminas/laminas-zendframework-bridge), BC changes
are not expected and legacy code does not need to be refactored (though it is
[recommended and simple](https://docs.laminas.dev/migration/)).
- The diactoros factories of `php-http/message` will return objects from the `Laminas\Diactoros\` namespace, if
the respective classes are available via autoloading, but continue to return objects from `Zend\Diactoros\`
namespace otherwise.
- Allow to specify the hashing algorithm for WSSE authentication.
## [1.10.0] - 2020-11-11
- Added support for PHP 8.0.
## [1.9.1] - 2020-10-13
- Improved detection of binary stream to not consider newlines, carriage return or tabs as binary.
## [1.9.0] - 2020-08-17
- Omitted binary body in FullHttpMessageFormatter and CurlCommandFormatter.
`[binary stream omitted]` will be shown instead.
### Added
- New Header authentication method for arbitrary header authentication.
## [1.8.0] - 2019-08-05
### Changed
- Raised minimum PHP version to 7.1
### Fixed
- Fatal error on `CurlCommandFormatter` when body is larger than `escapeshellarg` allowed length.
- Do not read stream in message formatter if stream is not seekable.
## [1.7.2] - 2018-10-30
### Fixed
- FilteredStream uses `@trigger_error` instead of throwing exceptions to not
break careless users. You still need to fix your stream code to respect
`isSeekable`. Seeking does not work as expected, and we will add exceptions
in version 2.
## [1.7.1] - 2018-10-29
### Fixed
- FilteredStream is not actually seekable
## [1.7.0] - 2018-08-15
### Fixed
- Fix CurlCommandFormatter for binary request payloads
- Fix QueryParam authentication to assemble proper URL regardless of PHP `arg_separator.output` directive
- Do not pass `null` parameters to `Clue\StreamFilter\fun`
### Changed
- Dropped tests on HHVM
## [1.6.0] - 2017-07-05
### Added
- CookieUtil::parseDate to create a date from cookie date string
### Fixed
- Fix curl command of CurlFormatter when there is an user-agent header
## [1.5.0] - 2017-02-14
### Added
- Check for empty string in Stream factories
- Cookie::createWithoutValidation Static constructor to create a cookie. Will not perform any attribute validation during instantiation.
- Cookie::isValid Method to check if cookie attributes are valid.
### Fixed
- FilteredStream::getSize returns null because the contents size is unknown.
- Stream factories does not rewinds streams. The previous behavior was not coherent between factories and inputs.
### Deprecated
- FilteredStream::getReadFilter The read filter is internal and should never be used by consuming code.
- FilteredStream::getWriteFilter We did not implement writing to the streams at all. And if we do, the filter is an internal information and should not be used by consuming code.
## [1.4.1] - 2016-12-16
### Fixed
- Cookie::matchPath Cookie with root path (`/`) will not match sub path (e.g. `/cookie`).
## [1.4.0] - 2016-10-20
### Added
- Message, stream and URI factories for [Slim Framework](https://github.com/slimphp/Slim)
- BufferedStream that allow you to decorate a non-seekable stream with a seekable one.
- cUrlFormatter to be able to redo the request with a cURL command
## [1.3.1] - 2016-07-15
### Fixed
- FullHttpMessageFormatter will not read from streams that you cannot rewind (non-seekable)
- FullHttpMessageFormatter will not read from the stream if $maxBodyLength is zero
- FullHttpMessageFormatter rewinds streams after they are read
## [1.3.0] - 2016-07-14
### Added
- FullHttpMessageFormatter to include headers and body in the formatted message
### Fixed
- #41: Response builder broke header value
## [1.2.0] - 2016-03-29
### Added
- The RequestMatcher is built after the Symfony RequestMatcher and separates
scheme, host and path expressions and provides an option to filter on the
method
- New RequestConditional authentication method using request matchers
- Add automatic basic auth info detection based on the URL
### Changed
- Improved ResponseBuilder
### Deprecated
- RegexRequestMatcher, use RequestMatcher instead
- Matching authenitcation method, use RequestConditional instead
## [1.1.0] - 2016-02-25
### Added
- Add a request matcher interface and regex implementation
- Add a callback request matcher implementation
- Add a ResponseBuilder, to create PSR7 Response from a string
### Fixed
- Fix casting string on a FilteredStream not filtering the output
## [1.0.0] - 2016-01-27
## [0.2.0] - 2015-12-29
### Added
- Autoregistration of stream filters using Composer autoload
- Cookie
- [Apigen](http://www.apigen.org/) configuration
## [0.1.2] - 2015-12-26
### Added
- Request and response factory bindings
### Fixed
- Chunk filter namespace in Dechunk stream
## [0.1.1] - 2015-12-25
### Added
- Formatter
## 0.1.0 - 2015-12-24
### Added
- Authentication
- Encoding
- Message decorator
- Message factory (Guzzle, Diactoros)
[Unreleased]: https://github.com/php-http/message/compare/1.10.0...HEAD
[1.10.0]: https://github.com/php-http/message/compare/1.9.1...1.10.0
[1.9.1]: https://github.com/php-http/message/compare/1.9.0...1.9.1
[1.9.0]: https://github.com/php-http/message/compare/1.8.0...1.9.0
[1.8.0]: https://github.com/php-http/message/compare/1.7.2...1.8.0
[1.7.2]: https://github.com/php-http/message/compare/v1.7.1...1.7.2
[1.7.1]: https://github.com/php-http/message/compare/1.7.0...v1.7.1
[1.7.0]: https://github.com/php-http/message/compare/1.6.0...1.7.0
[1.6.0]: https://github.com/php-http/message/compare/1.5.0...1.6.0
[1.5.0]: https://github.com/php-http/message/compare/v1.4.1...1.5.0
[1.4.1]: https://github.com/php-http/message/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/php-http/message/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/php-http/message/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/php-http/message/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/php-http/message/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/php-http/message/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/php-http/message/compare/0.2.0...v1.0.0
[0.2.0]: https://github.com/php-http/message/compare/v0.1.2...0.2.0
[0.1.2]: https://github.com/php-http/message/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/php-http/message/compare/v0.1.0...v0.1.1

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