[COMPOSER] Added predis/predis and updated packages

This commit is contained in:
Miguel Dantas
2019-08-13 01:31:05 +01:00
committed by Diogo Cordeiro
parent 0bb35d7e7f
commit 630a578e1d
641 changed files with 58448 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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