Unplug the HTTPlug

Stéphane Hulard

Consultant, Formateur, Contributeur.

HTTPlug ?

Un éléphant qui parle HTTP !

Pourquoi ?

Centraliser les appels HTTP de plus en plus présents.
Faciliter les tests et le debug.

🐣 L'arrivée d'HTTPlug

Avril 2015 - Premier commit
Octobre 2018 - Version 2.0, compatible PSR18
Novembre 2018 - Sortie de la PSR18
2020 - Intégration de plus en plus répandue

Un écosystème complet

Clients: cURL, socket.

Adapteurs: Guzzle(5/6), Symfony HTTPClient, ReactPHP…

Plugins: Authentification, Log, Cache…

                            composer require php-http/[client,plugin]
                        

Encore un nouvel outil ?

Pourquoi faire simple…

Prendre du recul

Interopérabilité ?

Utilisation des PSRs

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

class MyApiDataRetriever
{
    private $httpClient;

    public function __construct(ClientInterface $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    public function retrieveData(RequestInterface $message): AwesomeDTO
    {
        return AwesomeDTO::fromResponse(
            $this->httpClient->sendRequest($message)
        );
    }
}
                            
                        
                            use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

class MyApiDataRetriever
{
    private $httpClient;

    public function __construct(ClientInterface $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    public function retrieveData(RequestInterface $message): AwesomeDTO
    {
        return AwesomeDTO::fromResponse(
            $this->httpClient->sendRequest($message)
        );
    }
}
                            
                        
                            use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

class MyApiDataRetriever
{
    private $httpClient;

    public function __construct(ClientInterface $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    public function retrieveData(RequestInterface $message): AwesomeDTO
    {
        return AwesomeDTO::fromResponse(
            $this->httpClient->sendRequest($message)
        );
    }
}
                            
                        

Implémentation

                            use Http\Adapter\React\Client;
use Laminas\Diactoros\RequestFactory;

$client = new Client(/* options */);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    (new RequestFactory)->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            use Http\Adapter\React\Client;
use Laminas\Diactoros\RequestFactory;

$client = new Client(/* options */);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    (new RequestFactory)->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            use Http\Adapter\React\Client;
use Laminas\Diactoros\RequestFactory;

$client = new Client(/* options */);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    (new RequestFactory)->createRequest('GET', 'http://example.com/api')
);
                            
                        

Automatisation

                            use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;

$client         = HttpClientDiscovery::find();
$messageFactory = MessageFactoryDiscovery::find();

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;

$client         = HttpClientDiscovery::find();
$messageFactory = MessageFactoryDiscovery::find();

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        

Une complexité faible

Séparation des responsabilités (client, messages…),
Définition des prérequis pour chacun de vos objets.

Adaptabilité et décorateurs

                            class AwesomeDecorator implements ClientInterface
{
    private $decorated;
    public function __construct(ClientInterface $client)
    {
        $this->decorated = $client;
    }

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        /* Vérification ou traitement spécifique */
        $response = $this->decorated->sendRequest($request);
        /* Vérification ou traitement spécifique */
        return $response;
    }
}
                            
                        
                            class AwesomeDecorator implements ClientInterface
{
    private $decorated;
    public function __construct(ClientInterface $client)
    {
        $this->decorated = $client;
    }

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        /* Vérification ou traitement spécifique */
        $response = $this->decorated->sendRequest($request);
        /* Vérification ou traitement spécifique */
        return $response;
    }
}
                            
                        
                            class AwesomeDecorator implements ClientInterface
{
    private $decorated;
    public function __construct(ClientInterface $client)
    {
        $this->decorated = $client;
    }

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        /* Vérification ou traitement spécifique */
        $response = $this->decorated->sendRequest($request);
        /* Vérification ou traitement spécifique */
        return $response;
    }
}
                            
                        
                            class AwesomeDecorator implements ClientInterface
{
    private $decorated;
    public function __construct(ClientInterface $client)
    {
        $this->decorated = $client;
    }

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        /* Vérification ou traitement spécifique */
        $response = $this->decorated->sendRequest($request);
        /* Vérification ou traitement spécifique */
        return $response;
    }
}
                            
                        

Tester les appels HTTP 😱…

…dès qu'il y a des tiers.

Les interfaces à la rescousse

Psr\Http\Client\ClientInterface
Psr\Http\Message\*

php-http/mock-client

                            $client = new Http\Mock\Client;

$client->on(
    new RequestMatcher('/api', 'example.com'),
    function (): ResponseInterface {
        /* Créer une réponse adaptée */
        return $response;
    }
);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            $client = new Http\Mock\Client;

$client->on(
    new RequestMatcher('/api', 'example.com'),
    function (): ResponseInterface {
        /* Créer une réponse adaptée */
        return $response;
    }
);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            $client = new Http\Mock\Client;

$client->on(
    new RequestMatcher('/api', 'example.com'),
    function (): ResponseInterface {
        /* Créer une réponse adaptée */
        return $response;
    }
);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            $client = new Http\Mock\Client;

$client->on(
    new RequestMatcher('/api', 'example.com'),
    function (): ResponseInterface {
        /* Créer une réponse adaptée */
        return $response;
    }
);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        
                            $client = new Http\Mock\Client;

$client->on(
    new RequestMatcher('/api', 'example.com'),
    function (): ResponseInterface {
        /* Créer une réponse adaptée */
        return $response;
    }
);

$dto = (new MyApiDataRetriever($client))->retrieveData(
    $messageFactory->createRequest('GET', 'http://example.com/api')
);
                            
                        

Vous avez le pouvoir