PSR
The introduction of PSR-7, PSR-17 and PSR-18 is all part of a plan to make it possible to
build applications that need to send HTTP requests to a server in an HTTP client agnostic way
See PSR-18: The PHP standard for HTTP clients
I have been working with many applications that have historically relied heavily on Guzzle instead of abstract interfaces. Most of these applications make simple API request using GET or POST request containing a JSON body and responses also containing a JSON body or throwing exceptions for HTTP 4xx or 5xx errors.
API Wrapper
This question comes from a recent project where I tried to develop an API package that did not explicitly rely on Guzzle but instead only on the PSR interfaces.
The idea was to make a class ApiWrapper
that could be initiated using:
- An HTTP client fulfilling the PSR-18
ClientInterface
- A Request Factory fulfilling the PSR-17
RequestFactoryInterface
- A Stream Factory fulfilling the PSR-17
StreamFactoryInterface
This class would have anything it needs to:
- Make a request (PSR-7) using the Request Factory and Stream Factory
- Send a request using HTTP client
- Handle the response - since we know this will fulfill the PSR-7
ResponseInterface
Such an API wrapper would not rely on any concrete implementation of the above interfaces but it would merely require any implementation of these. Hence the developer would be able to use his or her favorite HTTP client instead of being forced to use a specific client like Guzzle.
Problem
Now, first of all, I truly love Guzzle, this is not a post to dispute the awesomeness of Guzzle, this is just a post asking how to make it possible for the developers to choose the correct http client for their needs.
But the problem is that relying explicitly on Guzzle provides a lot of nice functionality since Guzzle does more than the above. Guzzle also applies a range of handlers and middlewares like following redirects or throwing exceptions for HTTP 4xx responses.
Question
Long description, but here comes the question: How can one deal with common HTTP request handling like following redirects or throwing exceptions for HTTP 4xx responses in a controlled manner (hence yielding the same response regardless of the HTTP client used) without having to specify exactly what HTTP client to use?
Example
Here is an example of the ApiWrapper
implementation:
<?php
use Psr\Http\Client\ClientExceptionInterface;
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;
/*
* API Wrapper using PSR-18 ClientInterface, PSR-17 RequestFactoryInterface and PSR-7 RequestInterface
*
* Inspired from: https://www.php-fig.org/blog/2018/11/psr-18-the-php-standard-for-http-clients/
* Require the packages `psr/http-client` and `psr/http-factory`
*
* Details about PSR-7 taken from https://www.dotkernel.com/dotkernel3/what-is-psr-7-and-how-to-use-it/
*
* Class Name Description
* Psr\Http\Message\MessageInterface Representation of a HTTP message
* Psr\Http\Message\RequestInterface Representation of an outgoing, client-side request.
* Psr\Http\Message\ServerRequestInterface Representation of an incoming, server-side HTTP request.
* Psr\Http\Message\ResponseInterface Representation of an outgoing, server-side response.
* Psr\Http\Message\StreamInterface Describes a data stream
* Psr\Http\Message\UriInterface Value object representing a URI.
* Psr\Http\Message\UploadedFileInterface Value object representing a file uploaded through an HTTP request.
*/
class ApiWrapper
{
/**
* The PSR-18 compliant ClientInterface.
*
* @var ClientInterface
*/
private $psr18HttpClient;
/**
* The PSR-17 compliant RequestFactoryInterface.
*
* @var RequestFactoryInterface
*/
private $psr17HttpRequestFactory;
/**
* The PSR-17 compliant StreamFactoryInterface.
*
* @var StreamFactoryInterface
*/
private $psr17HttpStreamFactory;
public function __construct(
ClientInterface $psr18HttpClient,
RequestFactoryInterface $psr17HttpRequestFactory,
StreamFactoryInterface $psr17HttpStreamFactory,
array $options = []
) {
$this->psr18HttpClient($psr18HttpClient);
$this->setPsr17HttpRequestFactory($psr17HttpRequestFactory);
$this->setPsr17HttpStreamFactory($psr17HttpStreamFactory);
}
public function psr18HttpClient(ClientInterface $psr18HttpClient): void
{
$this->psr18HttpClient = $psr18HttpClient;
}
public function setPsr17HttpRequestFactory(RequestFactoryInterface $psr17HttpRequestFactory): void
{
$this->psr17HttpRequestFactory = $psr17HttpRequestFactory;
}
public function setPsr17HttpStreamFactory(StreamFactoryInterface $psr17HttpStreamFactory): void
{
$this->psr17HttpStreamFactory = $psr17HttpStreamFactory;
}
public function makeRequest(string $method, $uri, ?array $headers = [], ?string $body = null): RequestInterface
{
$request = $this->psr17HttpRequestFactory->createRequest($method, $uri);
if (! empty($headers)) {
$request = $this->addHeadersToRequest($request, $headers);
}
if (! empty($body)) {
$stream = $this->createStreamFromString($body);
$request = $this->addStreamToRequest($request, $stream);
}
return $request;
}
/**
* Add headers provided as nested array.
*
* Format of headers:
* [
* 'accept' => [
* 'text/html',
* 'application/xhtml+xml',
* ],
* ]
* results in the header: accept:text/html, application/xhtml+xml
* See more details here: https://www.php-fig.org/psr/psr-7/#headers-with-multiple-values
*
* @param \Psr\Http\Message\RequestInterface $request
* @param array $headers
* @return \Psr\Http\Message\RequestInterface
*/
public function addHeadersToRequest(RequestInterface $request, array $headers): RequestInterface
{
foreach ($headers as $headerKey => $headerValue) {
if (is_array($headerValue)) {
foreach ($headerValue as $key => $value) {
if ($key == 0) {
$request->withHeader($headerKey, $value);
} else {
$request->withAddedHeader($headerKey, $value);
}
}
} else {
$request->withHeader($headerKey, $headerValue);
}
}
return $request;
}
/**
* Use the PSR-7 complient StreamFactory to create a stream from a simple string.
*
* @param string $body
* @return \Psr\Http\Message\StreamInterface
*/
public function createStreamFromString(string $body): StreamInterface
{
return $this->psr17HttpStreamFactory->createStream($body);
}
/**
* Add a PSR 7 Stream to a PSR 7 Request.
*
* @param \Psr\Http\Message\RequestInterface $request
* @param \Psr\Http\Message\StreamInterface $body
* @return \Psr\Http\Message\RequestInterface
*/
public function addStreamToRequest(RequestInterface $request, StreamInterface $body): RequestInterface
{
return $request->withBody($body);
}
/**
* Make the actual HTTP request.
*
* @param \Psr\Http\Message\RequestInterface $request
* @return \Psr\Http\Message\ResponseInterface
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function request(RequestInterface $request): ResponseInterface
{
// According to PSR-18:
// A Client MUST throw an instance of Psr\Http\Client\ClientExceptionInterface
// if and only if it is unable to send the HTTP request at all or if the
// HTTP response could not be parsed into a PSR-7 response object.
return $this->psr18HttpClient->sendRequest($request);
}
}