PSR-18


Abstracting HTTP Clients in PHP


PHP Day, Verona - 20.5.2022

© David Buchmann







David Buchmann - david@liip.ch

PHP Engineer, Liip AG, Switzerland

We are hiring: https://liip.ch/jobs

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

PHP Framework Interoperability Group (PHP-FIG)

HTTP Request and Response: PSR-7

=> Matthew Weier O'Phinney

How to create an object without knowing the class? PSR-17

interface RequestFactoryInterface
{
    function createRequest(string $method, $uri)
               : RequestInterface;
}

HTTP Message Factories: PSR-17

=> Woody Gilk

How to send that request? PSR-18

interface ClientInterface
{
  function sendRequest(RequestInterface $request)
           : ResponseInterface;
}
private RequestFactoryInterface $reqFactory;
private ClientInterface $httpClient;

public function getProduct(string $id): Product
{
  $url = '/api/product/'.$id;
  $req = $reqFactory->createRequest('GET', $url);
  $response = $this->client->sendRequest($req);
  if (200 !== $response->getStatusCode()) {
    // error handling ...
  }
  // deserialize body into model...

What is PSR-18?

HTTP Client: PSR-18

=> Tobias Nyholm

Interchangeable: Defined behaviour

What about configuration?

But...

sendRequest(RequestInterface $request, $config)

Injecting multiple clients

public function __construct(
    ClientInterface $publicHttpClient,
    ClientInterface $apiHttpClient
) {
...

API client with host and base path configured, authentication already set up



PSR-18 in reusable libraries

composer.json

"require": {
    "psr/http-client-implementation": "^1.0"
},
"require-dev": {
    "guzzlehttp/guzzle": "^7.4"
}

Bootstrap

What about the cute elephant?

Httplug is still useful

Httplug PluginClient

public function handleRequest(
    RequestInterface $request,
    callable $next,
    callable $first
): Promise;
            

Cache Plugin

CachePlugin::clientCache - max-age, no-cache
CachePlugin::serverCache - additionally: private
            

Httplug Authentication

Other PSR-18 decorators

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 = []):

public function post($uri, array $headers = [],

public function put($uri, array $headers = [],

public function send(string $method, $uri, arr
            

Httplug discovery

 composer require php-http/discovery
public function __construct(
    ?ClientInterface $httpClient = null,
    ?RequestFactoryInterface $requestF = null
) {
    $this->httpClient = $httpClient ?:
        Psr18ClientDiscovery::find();
    $this->requestF = $requestF ?:
        Psr17FactoryDiscovery::findRequestFactory();
    ...

Still allow user to provide client instance!

Symfony HttplugBundle

HttplugBundle configuration

httplug:
  clients:
    app:
      http_methods_client: true
      plugins:
        - header_set:
            headers:
              "User-Agent": "demo-app"
            

HTTP request in Symfony

private HttpMethodsClient $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;

PHP 8.1 introduces the Fibers concept

PHP 8.1: Fibers

Meanwhile: Use Httplug with Promises

interface Promise
{
    public const PENDING = 'pending';
    public const FULFILLED = 'fulfilled';
    public 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;
}

Thank you!

@dbu


https://joind.in/talk/7ecaf