Framework-agnostic HTTP komunikace v PHP

Šimon Podlipský

  1. composer require <any>/<http-client>
  2. `new Client`
  3. `res = client.req(...)`
  4. Process response

How it's done

(often)

composer require guzzlehttp/guzzle

How it's done

Guzzle

use \GuzzleHttp\Client;

$client = new Client();
$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle');

echo $response->getStatusCode(); // 200
echo $response->getHeaderLine('content-type'); // application/json; charset=utf8
echo $response->getBody(); // {"id": 1420053, "name": "guzzle", ...}

How it's done

Guzzle example

composer require symfony/http-client

How it's done

Symfony client

use Symfony\Component\HttpClient\HttpClient;

$client = HttpClient::create();
$response = $client->request('GET', 'https://api.github.com/repos/symfony/symfony-docs');

$statusCode = $response->getStatusCode(); // 200
$contentType = $response->getHeaders()['content-type'][0]; // application/json
$content = $response->getContent(); // {"id":521583, "name":"symfony-docs", ...}
$content = $response->toArray(); // ['id' => 521583, 'name' => 'symfony-docs', ...]

How it's done

Symfony client example

final class Something
{
    public function do()
    {
        $client = new \GuzzleHttp\Client();

        $response = $client->request('POST', 'http://soukupnahrad.cz');
    
        return $this->parseResponse($response);
    }
    
    ...
}

Cannot unit test this

function testProcessResponse()
{
    $something = new Something();

    self::assertX(
        ..., 
        $something->do()
    );
}

SOLID

Dependency inversion principle

SOLID

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).​
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
  1.  
use GuzzleHttp\Client;

final class Something
{
    private Client $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function do()
    {
        $response = $this->client->request('POST', 'http://soukupnahrad.cz');
    
        return $this->parseResponse($response);
    }
}
function testProcessResponse()
{
    $something = new Something(new \GuzzleHttp\Client());

    self::assertX(
        ..., 
        $something->do()
    );
}
use GuzzleHttp\Client;

final class Something
{
    private ClientInterface $client;

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

    public function do()
    {
        $response = $this->client->request('POST', 'http://soukupnahrad.cz');
    
        return $this->parseResponse($response);
    }
}
function testProcessResponse()
{
    $something = new Something(new FakeClient());

    self::assertX(
        ..., 
        $something->do()
    );
}
class Client implements ClientInterface

Issues, issues, issues...

Vendor lock

Gotchas, complexity

$client->request(
    'POST',
    'http://soukupnahrad.cz',
    ['json' => json_encode([true])]
);
$client->request(
    'POST',
    'http://soukupnahrad.cz',
    ['json' => [true]]
);

Guzzle

Legendary json option

$response = $client->request(
    'POST',
    'http://soukupnahrad.cz',
    [
        'json' => '',
        'headers' => [
            'Content-Type' => 'application/hal+json',
        ],
    ]
);

Let's not start with FW/impl

PSR

PHP-FIG

(framework interoperability group)

PSR-7
PSR-17
PSR-18

Message
Factory
Client

composer require psr/http-message  # PSR-7
composer require psr/http-factory  # PSR-17
composer require psr/http-client   # PSR-18

Deps

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

final class Something
{
    private ClientInterface $client;

    private RequestFactoryInterface $requestFactory;

    public function __construct(
        ClientInterface $client,
        RequestFactoryInterface $requestFactory

    ) {
        $this->client = $client;
        $this->requestFactory = $requestFactory;
    }

    
    ...
}

Setup deps

$request = $this->requestFactory->createRequest('POST', 'http://soukupnahrad.cz');
$request = $this->requestFactory->createRequest('POST', 'http://soukupnahrad.cz');
$request = $request->withHeader('Authorization', 'Bearer ' . $token); // Mind immutability
$request = $this->requestFactory->createRequest('POST', 'http://soukupnahrad.cz')
    ->withHeader('Authorization', 'Bearer ' . $token)
    ->withBody(
        $this->streamFactory->create('body contents')
    );

$response = $this->client->send($request);
$request = $this->requestFactory->createRequest('POST', 'http://soukupnahrad.cz')
    ->withHeader('Authorization', 'Bearer ' . $token)
    ->withBody(
        $this->streamFactory->create('body contents')
    );

Create Request

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class Something
{
    private ClientInterface $client;

    private RequestFactoryInterface $requestFactory;
    
    private StreamFactoryInterface $streamFactory;

    public function __construct(
        ClientInterface $client,
        RequestFactoryInterface $requestFactory,
        StreamFactoryInterface $streamFactory

    ) {
        $this->client = $client;
        $this->requestFactory = $requestFactory;
        $this->streamFactory = $streamFactory;
    }

    
    ...
}
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class Something
{
    private ClientInterface $client;

    private RequestFactoryInterface $requestFactory;
    
    private StreamFactoryInterface $streamFactory;

    public function __construct(
        ClientInterface $client,
        RequestFactoryInterface $requestFactory,
        StreamFactoryInterface $streamFactory
    ) {
        $this->client         = $client;
        $this->requestFactory = $requestFactory;
        $this->streamFactory  = $streamFactory;
    }

    
    public function do() 
    {
        $request = $this->requestFactory->createRequest('POST', 'http://soukupnahrad.cz')
            ->withHeader('Authorization', 'Bearer ' . $token)
            ->withBody(
                $this->streamFactory->create('body contents')
            );

        $response = $this->client->send($request);
        
        ...
    }
}

Implementations

composer require nyholm/psr7

Factories

final class Psr17Factory implements 
    RequestFactoryInterface, 
    ResponseFactoryInterface, 
    ServerRequestFactoryInterface, 
    StreamFactoryInterface, 
    UploadedFileFactoryInterface, 
    UriFactoryInterface

Nyholm

Runs: 30,000
Runs per second: 11214
Average time per run: 0.0892 ms
Total time: 2.6751 s
1K LOC

Guzzle

Runs: 30,000
Runs per second: 8614
Average time per run: 0.1161 ms
Total time: 3.4824 s
3K LOC

Slim

Runs: 30,000
Runs per second: 7424
Average time per run: 0.1347 ms
Total time: 4.0409 s
1.7K LOC

Zend Diactoros 2

Runs: 30,000
Runs per second: 6422
Average time per run: 0.1557 ms
Total time: 4.6709 s
3K LOC

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns="http://symfony.com/schema/dic/services"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />

        <service id="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Http\Client\Curl\Client" />

        <service id="Psr\Http\Client\ClientInterface" alias="Http\Client\Curl\Client" />
        <service id="Psr\Http\Message\ResponseFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Psr\Http\Message\RequestFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Psr\Http\Message\StreamFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Psr\Http\Message\UriFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
    </services>
</container>

Symfony config

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns="http://symfony.com/schema/dic/services"
           xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <defaults autowire="true" autoconfigure="true" public="false" />

        <service id="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Http\Client\Curl\Client" />

        <service id="Psr\Http\Client\ClientInterface" alias="Http\Client\Curl\Client" />
        <service id="Psr\Http\Message\ResponseFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Psr\Http\Message\RequestFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Psr\Http\Message\StreamFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
        <service id="Psr\Http\Message\UriFactoryInterface" alias="Nyholm\Psr7\Factory\Psr17Factory" />
    </services>
</container>
services:
    - Nyholm\Psr7\Factory\Psr17Factory
    - Http\Client\Curl\Client

Nette (maybe)

Implementations

Client

$response = $this->client->sendRequest($request);

// ?????

Response Handling

Response Handling

Success | Fail

$response = $this->client->sendRequest($request);

if ($response->getStatusCode() === 200) {
	return ...;
}

if ($response->getStatusCode() === 400) {
	throw ...;
}

if ($response->getStatusCode() >= 500) {
	throw ...;
}

Response is always returned

...except...

on network errors etc.

try {
    $response = $this->client->request(...);
} catch (ClientException $exception) {
    // Handle e4xx
} catch (ServerException $exception) {
    // Handle e5xx
}

// Handle success

Guzzle way

<?php

namespace Psr\Http\Client;

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

interface ClientInterface
{
    /**
     * Sends a PSR-7 request and returns a PSR-7 response.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
     */
    public function sendRequest(RequestInterface $request): ResponseInterface;
}
<?php

namespace Psr\Http\Client;

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

interface ClientInterface
{
    /**
     * Sends a PSR-7 request and returns a PSR-7 response.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
     */
    public function sendRequest(RequestInterface $request): ResponseInterface;
}

Just read the docs

Response Handling

Body

interface ResponseInterface {

    ...

    /**
     * Gets the body of the message.
     *
     * @return StreamInterface Returns the body as a stream.
     */
    public function getBody();
}
$contents  = $response->getBody()->getContents(); // "body content"
<?php

namespace Psr\Http\Message;

/**
 * Describes a data stream.
 *
 * Typically, an instance will wrap a PHP stream; this interface provides
 * a wrapper around the most common operations, including serialization of
 * the entire stream to a string.
 */
interface StreamInterface
{
    ...

    /**
     * Returns the remaining contents in a string
     *
     * @return string
     * @throws \RuntimeException if unable to read or an error occurs while
     *     reading.
     */
    public function getContents();
    
    ...
}
$contents  = $response->getBody()->getContents(); // "body content"

$contents2 = $response->getBody()->getContents(); // ""

Response Handling

Body

$contents  = (string) $response->getBody();
<?php

namespace Psr\Http\Message;

/**
 * Describes a data stream.
 *
 * Typically, an instance will wrap a PHP stream; this interface provides
 * a wrapper around the most common operations, including serialization of
 * the entire stream to a string.
 */
interface StreamInterface
{
    /**
     * Reads all data from the stream into a string, from the beginning to end.
     *
     * This method MUST attempt to seek to the beginning of the stream before
     * reading data and read the stream until the end is reached.
     *
     * Warning: This could attempt to load a large amount of data into memory.
     *
     * This method MUST NOT raise an exception in order to conform with PHP's
     * string casting operations.
     *
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
     * @return string
     */
    public function __toString();
    
    ...
}
<?php

namespace Psr\Http\Message;

/**
 * Describes a data stream.
 *
 * Typically, an instance will wrap a PHP stream; this interface provides
 * a wrapper around the most common operations, including serialization of
 * the entire stream to a string.
 */
interface StreamInterface
{
    /**
     * Reads all data from the stream into a string, from the beginning to end.
     *
     * This method MUST attempt to seek to the beginning of the stream before
     * reading data and read the stream until the end is reached.
     *
     * Warning: This could attempt to load a large amount of data into memory.
     *
     * This method MUST NOT raise an exception in order to conform with PHP's
     * string casting operations.
     *
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
     * @return string
     */
    public function __toString();
    
    ...
}

Recap

SOLID

Start with interfaces, abstractions

Avoid vendor locks

Recap

PSR + Nyholm + HTTP Plug = 👌 

PSR Messages are immutable

Recap

res.getBody() -> Stream

Work with status codes

Made with Slides.com