Libérez-vous de votre client HTTP avec PSR7 et Httplug.

Coucou !

Joel Wurtz

Dev & Ops @Jolicode

@joelwurtz

API Client

  • Twitter
  • Facebook
  • Docker
  • Votre API
  • ...

HTTP Client en PHP

Socket

<?php

$socket = stream_socket_client('tcp://api.mon-api.com');
fwrite($socket, "GET / HTTP1.1\r\nHost: api.mon-api.com\r\n");
$response = fread($socket, ...);

HTTP Client en PHP

Curl

<?php

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://api.mon-api.com');
$response = curl_exec($ch);

HTTP Client en PHP

Guzzle

<?php

$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'http://api.mon-api.com');

HTTP Clients en PHP

  • Socket
  • Curl
  • Guzzle 5
  • Guzzle 6
  • React
  • Zend 1
  • Zend 2
  • Buzz
  • Cake
  • ...

Mettre à disposition un client pour son API

Quelle dépendance choisir ?

Conflits ?

composer require stage1/docker-php
composer require aws/aws-sdk-php
Your requirements could not be resolved to an
installable set of packages.

Problem 1
- Conclusion: don't install guzzlehttp/guzzle 6.1.0
- Conclusion: don't install guzzlehttp/guzzle 4.2.3
- Conclusion: don't install guzzlehttp/streams 1.5.1
- Conclusion: don't install guzzlehttp/guzzle 4.2.2
- Conclusion: don't install guzzlehttp/streams 1.5.0
- Conclusion: don't install guzzlehttp/guzzle 6.0.2
- Conclusion: don't install guzzlehttp/guzzle 4.2.1
....

Duplication ?

vendor
├── guzzlehttp
│   └── guzzle
├── kriswallsmith
│   └── buzz
├── react
│   └── http-client
├── zendframework
│   └── zend-http
└── zf1
    └── zend-http

PSR-7 à la rescousse

  • Psr\Http\Message\RequestInterface
  • Psr\Http\Message\ResponseInterface
  • Psr\Http\Message\StreamInterface

PSR-7 et Client API

2 rôles :

  • Créer des requêtes (RequestInterface)
  • Manipuler des réponses (ResponseInterface et StreamInterface)

Envoyer les requêtes ?

Pas votre problème !

Laisser le choix de l'implementation à l'utilisateur final !

Httplug

HTTP Client

<?php

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

interface HttpClient
{
    /**
     * Sends a PSR-7 request.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws Exception
     */
    public function sendRequest(RequestInterface $request);
}

HTTP Async Client

<?php

use Psr\Http\Message\RequestInterface;
use Http\Client\Promise;

interface HttpAsyncClient
{
    /**
     * Sends a PSR-7 request in an asynchronous way.
     *
     * @param RequestInterface $request
     *
     * @return Promise
     */
    public function sendAsyncRequest(RequestInterface $request);
}

Promise ?

  • Extension / Inspiration du monde Javascript (Promises/A+)
  • On "promet" qu'une valeur ou qu'une erreur sera disponible
  • Fonctionnement par callback

Promise

Fonctionnement par callback : onFulfilled / onRejected

<?php

$promise = $httpAsyncClient->sendAsyncRequest($request);

$onFulfilled = function (ResponseInterface $response) {
    echo 'The response is available';

    return $response;
};

$onRejected = function (Exception $e) {
    echo 'An error happens';

    throw $e;
};

$promise->then($onFulfilled, $onRejected);

Promise

Une Promise renvoie toujours une autre Promise

<?php

$promise
    ->then($onFulfilled, $onRejected)
    ->then(...)
;

Résolution ?

L'asynchrone en PHP, c'est pas vraiment ça ... (sauf si on utilise React PHP)

La méthode wait permet de forcer l'attente de la résolution de notre requête

<?php

$promise->wait();

Virtual Package

"require": {
    "php-http/client-implementation": "^1.0"
},
"require": {
    "php-http/async-client-implementation": "^1.0"
},

Implémentations

*-adapter

  • Guzzle 6
  • React (WIP)
  • Guzzle 5 (WIP)
  • Buzz (Prévue)
  • Zend 1 / 2 (Prévue)
  • ...

Implémentations

*-client

  • Socket
  • Curl (WIP)
  • ...

Créer des requêtes PSR-7

PSR-7 implementations

  • guzzle/psr7
  • zendframework/zend-diactoros

php-http/message-factory

Ensemble d'interfaces permettant l'abstraction des implémentations PSR-7...

MessageFactory

<?php

public function createRequest(
    $method,
    $uri,
    array $headers = [],
    $body = null,
    $protocolVersion = '1.1'
);

MessageFactory

<?php

public function createResponse(
    $statusCode = 200,
    $reasonPhrase = null,
    array $headers = [],
    $body = null,
    $protocolVersion = '1.1'
);

StreamFactory

<?php

/**
* Creates a new PSR-7 stream.
*
* @param string|resource|StreamInterface|null $body
*
* @return StreamInterface
*
* @throws \InvalidArgumentException If the stream body is invalid.
*/
public function createStream($body = null);

UriFactory

<?php

/**
* Creates an PSR-7 URI.
*
* @param mixed $uri
*
* @return UriInterface
*
* @throws \InvalidArgumentException If the URI is invalid.
*/
public function createUri($uri);

php-http/discovery

Instanciation automatique des clients et factory selon les dépendances présentes

HttpClientDiscovery

Création d'un client HTTP

<?php

use Http\Discovery\HttpClientDiscovery;

$client = HttpClientDiscovery::find();

MessageFactoryDiscovery

Création d'une factory pour créer des requêtes PSR-7

<?php

use Http\Discovery\MessageFactoryDiscovery;

$factory = MessageFactoryDiscovery::find();
$request = $factory->create('GET', 'http://api.mon-api.com/');

Mon client API dans tout ça ?

composer.json

"require": {
    "php-http/client-implementation": "^1.0",
    "php-http/discovery": "^0.4@dev"
},

class ClientApi

<?php

class ClientApi
{
    public function __construct(
        HttpClient $client = null,
        MessageFactory $factory = null
    )
    {
        $this->client  = $client ?: HttpClientDiscovery::find();
        $this->factory = $factory ?: MessageFactoryDiscovery::find();
    }

    public function getObjects()
    {
        return $this->client->sendRequest(
            $this->messageFactory->create('GET', '/objects')
        );
    }
}

Bonus : php-http/plugins

  • Ensemble de "middlewares" permettant de décorer un HttpClient
  • Approche "Async first"
  • Mais fonctionne pour les 2 contrats tout en ayant une implémentation unique

Authentication

Couche d'authentification pour les clients

  • Token (Bearer)
  • BasicAuth
  • Wsse

ContentLength

  • Définit le header ContentLength de la requête selon le flux d'entrée
  • Si taille incalculable, transforme le flux en "chunked"
  • Permet l'upload de fichiers volumineux sans aucun impact sur la mémoire

Cookie

Chargement, Sauvegarde des cookies

Decoder

Décode le flux d'une réponse HTTP (chunked, gzip, compress, deflate)

Error

Transforme certaines réponses HTTP en Exception (status code 400 à 599)

Logger

Log les requêtes, réponses et erreurs sous la norme PSR-3

Redirect

Suit les redirections d'une réponse HTTP (301, 302, ...)

Retry

Réessaye automatiquement l'envoi de la requête en cas d'exception

Stopwatch

Calcule le temps d'appel de la requête avec le composant Stopwatch de Symfony

Bien d'autres à venir...

  • BaseUri (Prefix automatique des Uri)
  • Journal (Historique)
  • Vcr (Replay des requêtes HTTP dans les tests)
  • ...

PHP-HTTP

Organisation derrière toutes ces librairies

  • Actuellement en alpha2 (rc d'ici la fin de l'année)
  • Bundle Symfony en cours
  • Librairie utilitaire
  • Contributions plus que bienvenues :)

Arretez d'écrire vos API Clientes !

Générez-les !

jane/jane

  • Prend un JSON Schema en entrée
  • Génère un modèle objet en PHP
  • Génère les serializer / deserializer de ce modèle (interfaces Symfony/Serializer)
  • Génère un AST (pas de templating)

jane/swagger

Jane++

  • Prend une spécification Swagger en entrée (qui "étend" / utilise JSON Schema)
  • Génère un modèle objet en PHP
  • Génère les serializer / deserializer de ce modèle (interfaces Symfony/Serializer)
  • Génère les appels API (utilise Httplug)
  • Genère un AST (pas de templating)

Merci !