PSR-18
Abstracting HTTP Clients in PHP
PHP Benelux, Antwerp - January 26th, 2019
© David Buchmann
What is the difference between
guzzle/guzzle
and
guzzlehttp/guzzle?
Depending on an implementation
Once upon a time
FOSHttpCache needs to send HTTP requests to varnish for cache invalidation
HTTPlug
=> Márk Sági-Kazár
- HTTP client interfaces
- FOSHttpCache 2.0 depends on HTTPlug instead of Guzzle 6
PHP Framework Interoperability Group (PHP-FIG)
- Initiative from framework and library projects
- Defines PHP Standard Recommendations (PSR)
HTTP Request and Response: PSR-7
- Interface for request
- Extended interface for server side request
- Interface for response
- For server side applications, this is all you need
- When building a client, you need more!
How to create an object without knowing the class?
interface RequestFactoryInterface
{
function createRequest(string $method, $uri)
: RequestInterface;
}
HTTP Message Factories: PSR-17
=> Woody Gilk
- Request factory
- Stream factory
- URI factory
- (Server Request factory, Uploaded File factory, Response factory)
How to send that request?
interface ClientInterface
{
function sendRequest(RequestInterface $request)
: ResponseInterface;
}
What is PSR-18?
HTTP Client: PSR-18
=> Tobias Nyholm
- Psr\Http\Client\ClientInterface
- And a bunch of exceptions...
- ClientExceptionInterface: base exception
- RequestExceptionInterface: request invalid
- NetworkExceptionInterface: DNS issue with target host, connection error
Interchangeable: Defined behaviour
- Consumers of the client must know what to expect
- No exceptions for HTTP reponses with error status codes, those are valid responses on HTTP level
- Instead, throw application exceptions in your adapter
What about configuration?
- Timeout settings, TLS, follow redirects, ...
- Not for the caller to decide
- Configure on the implementation
(e.g. constructor arguments)
But...
sendRequest(RequestInterface $request, $config)
- Would make the PSR define configuration options
- What about unknown options?
- Or limit to a known set?
- Instead, if you need different behaviours, inject multiple clients for specific purposes.
What about the cute elephant?
Httplug is still useful
- Httplug Client interface extends PSR-18 interface, allowing a smooth upgrade path
- Libraries should require ^1.0 || ^2.0
- PSR-18 adapters for existing clients like Guzzle
- Decorators for PSR-18 HTTP clients
- Convenience clients on top of PSR-18
Httplug PluginClient
public function handleRequest(
RequestInterface $request,
callable $next,
callable $first
): Promise;
- AddHost, AddPath, QueryDefaults
- Header manipulation
- Authentication, Cookie, Redirect, Retry
- ContentLenght, ContentType, Decoder
Cache Plugin
- Cache responses with a PSR-6 cache
- Respect Cache-Control response header
- Are we a proxy or a single client?
CachePlugin::clientCache - max-age, no-cache
CachePlugin::serverCache - additionally: private
Httplug Authentication
- AuthPlugin
- Authentication interface
- Basic (username:password), Bearer, Wsse
- Chain, Matching, RequestConditional
- Alter request to add auth credentials
- If you need a token, inject another client to the auth to send the requests to get the token
Other decorators
- HttpClientPool: Round robin or least recently used over PSR-18 instances
- HttpClientRouter: Use request matcher to chose which client to use for each request
BatchClient
/**
* @param RequestInterface[] The requests to send
*
* @throws BatchException If any request threw
*/
public function sendRequests(
array $requests
): BatchResult
HttpMethodsClient
public function get($uri, array $headers = []): Re
public function post($uri, array $headers = [], $b
public function put($uri, array $headers = [], $bo
public function send(string $method, $uri, array $
Httplug discovery
- Prefer dependency injection if possible!
- Static methods to instantiate
- PSR-17 factories
- PSR-18 client
- or Httplug clients / factories
Symfony HttplugBundle
- Configure PSR-18 Http clients
- Configure plugins
- Symfony toolbar integration
- Flex: composer require http
HttplugBundle configuration
httplug:
clients:
app:
http_methods_client: true
plugins:
- header_defaults:
headers:
"X-Conference": "Benelux"
- header_set:
headers:
"User-Agent": "PHP/Symfony"
HTTP request in Symfony
/** @var HttpMethodsClient */
private $httpClient;
public function status(): Response {
try {
$r = $this->httpClient->get('http://php.net/');
} catch (Exception $e) {
return new Response('Failed', 502);
}
return new Response(200 === $r->getStatusCode()
? 'Success'
: 'Error');
Outlook
Who can promise a Promise?
sendAsync(RequestInterface $request): Promise;
Promise::wait(): ResponseInterface;
- PSR for asynchronous clients would be nice, but first needs a standard for Promises
- The PSR for HTTP clients is not the right place to define Promises
Meanwhile: Use Httplug
interface Promise
{
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
public function then(?callable $onFulfilled,
public function getState();
public function wait($unwrap = true);
}
Fire off requests
try {
$promises = [];
foreach ($uris as $u) {
$promises[$u] = $httpClient->sendAsyncRequest(
$requestFactory->createRequest('GET', $u)
);
}
} catch (\Exception $e) {
return new Response('Configuration error', 500);
}
Wait for it
foreach ($promises as $uri => $promise) {
$s .= $uri.':';
try {
$r = $promise->wait();
if (200 === $r->getStatusCode()) {
$s .= 'Up and running';
} else {
$s .= 'Error: '.$r->getStatusCode();
}
} catch (\Exception $e) {
$s .= 'Network Error: '.$e->getMessage();
}
}
then()
$results = [];
$promises = [];
foreach ($uris as $u) {
$promise = $httpClient->sendAsyncRequest(
$requestFactory->createRequest('GET', $u));
$onFulfilled = function (ResponseInterface $r)
use ($u, $results) {
if (200 === $r->getStatusCode()) {
$results[$u] = 'Up and running';
} else {
$results[$u] = 'Error: '.$r->getStatusCode();
}
};
}
then() callbacks
...
$onRejected = function (Exception $r)
use ($u, $results) {
$results[$u] = 'Error: '.$e->getMessage();
};
$promise->then($onFulfilled, $onRejected);
$promises[] = $promise;
}
} catch (Exception $e) {
return new Response('Configuration error', 500);
}
Wait and build result page
foreach ($promises as $promise) {
$promise->wait(false);
}
foreach ($results as $u => $text) {
$s .= $uri.':'.text;
}