Slide 1

Slide 1 text

THE MODERN AND FAST HTTP CLIENT

Slide 2

Slide 2 text

WHO AM I? Web Developer and Software Engineer since 2011. Brazilian living in Ireland. Data Science and DevOps student. Member of PHP DF and PHP Dublin community. Speaker, Father and Guitar player Bruno Souza @Bruno_HSouza

Slide 3

Slide 3 text

What is the HttpClient? Installation and Configuration Requests Responses 01 02 03 04 CONTENT 05 Handling Exceptions 06 Testing the Component 07 New Features on Symfony 5.2

Slide 4

Slide 4 text

HTTP CLIENT

Slide 5

Slide 5 text

WHAT IS THE SYMFONY HTTP CLIENT?

Slide 6

Slide 6 text

WHAT IS THE SYMFONY HTTP CLIENT? Launched as experimental in Symfony 4.3 First official release with the Symfony 4.4 PSR-7 and PSR-18 compatibility Support HTTP and HTTP/2 Request and Response caching; Scoped Clients; HTTP Authentications;

Slide 7

Slide 7 text

Installation Configuration

Slide 8

Slide 8 text

Installation composer require symfony/http-client

Slide 9

Slide 9 text

App Example https://api.first.org/ https://openweathermap.org/api

Slide 10

Slide 10 text

# config/packages/framework.yaml framework: http_client: http_version: '2.0' Standalone component: public function getClient() { $this->client = HttpClient::create([ 'base_uri' => 'http://api.openweathermap.org', 'http_version' => '2.0', ]); } Configuration Symfony Framework:

Slide 11

Slide 11 text

HTTP/2 ● libcurl: ^7.36 ● amphp/http-client: ^4.2 ● HTTP/2 Server Push: libcurl >= 7.61 and PHP >= 7.2.17 || 7.3.4

Slide 12

Slide 12 text

HTTP/2 public function getAmpHttpClient() { $client = new AmpHttpClient([ 'base_uri' => 'https://http2.pro' ]); $response = $client->request( 'GET', '/api/v1' ); var_dump($response->getStatusCode()); dd($response->getInfo()); } public function getCurlHttpClient() { $client = new CurlHttpClient([ 'base_uri' => 'https://http2.pro' ]); $response = $client->request( 'GET', '/api/v1' ); var_dump($response->getStatusCode()); dd($response->getInfo()); }

Slide 13

Slide 13 text

HTTP/2

Slide 14

Slide 14 text

public function __construct(HttpClientInterface $client) { $this->client = $client; } Basic Usage Using a Symfony Framework application: ● Check the service container: bin/console debug:autowiring; ● Using Dependency Injection (Inversion) to set the class in the constructor:

Slide 15

Slide 15 text

Create static client /** * @param array $defaultOptions Default request's options * @param int $maxHostConnections The maximum number of connections to a single host * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue * * @see HttpClientInterface::OPTIONS_DEFAULTS for available options */ public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface

Slide 16

Slide 16 text

Client transport types public function getClient() { // uses the native PHP streams $this->client = new NativeHttpClient(); } public function getClient() { // uses the cURL PHP extension $this->client = new CurlHttpClient(); }

Slide 17

Slide 17 text

# config/packages/framework.yaml framework: http_client: scoped_clients: # only requests matching scope will use these options openweather: scope: 'http://api.openweathermap.org' headers: Accept: 'application/json' base_uri: 'http://api.openweathermap.org' query: key: 'appid' value: "%env(OPENWEATHERMAP_ID)%" apifirst: base_uri: 'https://api.first.org' headers: Accept: 'application/json' query: key: 'access' value: 'full' Configuration - Scoping Client

Slide 18

Slide 18 text

public function __construct(HttpClientInterface $apifirst) { $this->client = $apifirst; } bin/console debug:autowiring: Scoped Clients

Slide 19

Slide 19 text

dump($client): Scoped Clients

Slide 20

Slide 20 text

Creating Requests

Slide 21

Slide 21 text

Requests $response = $this->client->request( 'GET', '/data/v1/countries', [ 'headers' => ['Content-Type' => 'application/json'], 'query' => [ 'limit' => 251, ], ] ); return $response->getContent();

Slide 22

Slide 22 text

public const OPTIONS_DEFAULTS = [ 'auth_basic' => null, // array|string - an array containing the username as first value, and optionally the // password as the second one; or string like username:password - enabling HTTP Basic // authentication (RFC 7617) 'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750) 'query' => [], // string[] - associative array of query string values to merge with the request's URL 'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values 'body' => '', // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string // smaller than the amount requested as argument; the empty string signals EOF; if // an array is passed, it is meant as a form payload of field names and values 'json' => null, // mixed - if set, implementations MUST set the "body" option to the JSON-encoded // value and set the "content-type" header to a JSON-compatible value if it is not // explicitly defined in the headers option - typically "application/json" 'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that // MUST be available via $response->getInfo('user_data') - not used internally 'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0 // means redirects should not be followed; "Authorization" and "Cookie" headers MUST // NOT follow except for the initial host name 'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0 'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2 'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not, // or a stream resource where the response body should be written, // or a closure telling if/where the response should be buffered based on its headers 'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort // the request; it MUST be called on DNS resolution, on arrival of headers and on // completion; it SHOULD be called on upload/download of data and at least 1/s 'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution 'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored 'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached 'timeout' => null, // float - the idle timeout - defaults to ini_get('default_socket_timeout') ... Options FTW #vendor/symfony/http-client-contracts/HttpClientInterface.php 'max_duration' => 0, 'bindto' => '0', 'verify_peer' => true, 'verify_host' => true, 'cafile' => null, 'capath' => null, 'local_cert' => null, 'local_pk' => null, 'passphrase' => null, 'ciphers' => null, 'peer_fingerprint' => null, 'capture_peer_cert_chain' => false, 'extra' => [],

Slide 23

Slide 23 text

JSON Payload $response = $this->client->request( 'POST', '/data/v1/address', [ 'headers' => ['Content-Type' => 'application/json'], 'auth_bearer' => $token, 'json' => $requestContent, ] ); return $response->getContent();

Slide 24

Slide 24 text

Receiving Responses

Slide 25

Slide 25 text

Response Interface

Slide 26

Slide 26 text

Response Info

Slide 27

Slide 27 text

Response to Array $query = [ 'query' => [ 'id' => $cityId, 'appid' => $this->openweathermap_id ] ]; $response = $this->client->request('GET', '/data/2.5/weather', $query); return json_decode($response->getBody()); $query = [ 'query' => [ 'id' => $cityId, 'appid' => $this->openweathermap_id ] ]; $response = $this->client->request('GET', '/data/2.5/weather', $query); return $response->toArray();

Slide 28

Slide 28 text

Multiplexing Concurrent Requests

Slide 29

Slide 29 text

Multiplexing “ A way of sending multiple signals or streams of information over a communications link at the same time in the form of a single, complex signal; the receiver recovers the separate signals, a process called demultiplexing” Margaret Rouse, Whatis.com

Slide 30

Slide 30 text

Multiplexing in HttpClient $url = 'https://.../ubuntu-18.04.1-desktop-amd64.iso'; $response = $this->httpClient->request('GET', $url); // Responses are lazy: this code is executed as soon as headers are received if (200 !== $response->getStatusCode()) { throw new \Exception('...'); } // response chunks implement Symfony\Contracts\HttpClient\ChunkInterface $fileHandler = fopen('/ubuntu.iso', 'w'); foreach ($this->httpClient->stream($response, 0.0) as $chunk) { if ($chunk->isFirst()) { // headers of $response just arrived $response->getHeaders(); } elseif ($chunk->isLast()) { // the full content of $response just completed fwrite($fileHandler, $chunk->getContent()); } else { // $chunk->getContent() will return a piece $chunk->getContent(); } }

Slide 31

Slide 31 text

$startTime = microtime(true); try { $responses = []; for ($i = 0; $i < 379; ++$i) { $uri = "https://http2.akamai.com/demo/tile-$i.png"; $responses[] = $this->httpClient->request('GET', $uri); } $content = ''; foreach ($responses as $response) { $content = $response->getContent(); } $endTime = microtime(true); return 'Process Time 379 requests with HTTP2: ' . $endTime - $startTime; } catch (\Exception $exception) { throw $exception; } Concurrent Requests

Slide 32

Slide 32 text

Concurrent Requests (Response time)

Slide 33

Slide 33 text

Handling Exceptions

Slide 34

Slide 34 text

Handling Exceptions #vendor/symfony/http-client-contracts/ResponseInterface.php /** * Gets the response body decoded as array, typically from a JSON payload. * * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes * * @throws DecodingExceptionInterface When the body cannot be decoded to an array * @throws TransportExceptionInterface When a network error occurs * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached * @throws ClientExceptionInterface On a 4xx when $throw is true * @throws ServerExceptionInterface On a 5xx when $throw is true */ public function toArray(bool $throw = true): array;

Slide 35

Slide 35 text

Handling Exceptions #vendor/symfony/http-client-contracts/ResponseInterface.php /** * Gets the response body decoded as array, typically from a JSON payload. * * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes * * @throws DecodingExceptionInterface When the body cannot be decoded to an array * @throws TransportExceptionInterface When a network error occurs * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached * @throws ClientExceptionInterface On a 4xx when $throw is true * @throws ServerExceptionInterface On a 5xx when $throw is true */ public function toArray(bool $throw = true): array;

Slide 36

Slide 36 text

Handling Exceptions return $response->getContent(); } catch (ClientExceptionInterface $e) { $this->logger->error($e->getMessage(), $e->getResponse()->toArray()); } catch (RedirectionExceptionInterface $e) { $this->logger->warning($e->getMessage(), $e->getResponse()->toArray()); } catch (ServerExceptionInterface $e) { $this->logger->error($e->getMessage(), [ 'status' => $e->getResponse()->getStatusCode(), 'content' => $e->getResponse()->toArray(), 'info' => $e->getResponse()->getInfo() ]); } catch (TransportExceptionInterface $e) { // we do not have response in this case as should be a network issue $this->logger->error($e->getMessage()); }

Slide 37

Slide 37 text

Tests

Slide 38

Slide 38 text

public function httpResponseProvider(): \Generator { yield 'response-success' => [ (function() { $body = file_get_contents('data/weather-response.json'); $info = (array) file_get_contents('data/weather-response-info.txt'); return new MockResponse($body, $info); })() ]; yield 'response-failed' => [ (function() { $body = file_get_contents('data/weather-response-failed.json'); $info = (array) file_get_contents('data/weather-response-failed-info.txt'); return new MockResponse($body, $info); })() ]; } Testing the component /** * @dataProvider httpResponseProvider */ public function testGetWeatherFromApi(array $response) :void { $client = new MockHttpClient([$response]); $response = $client->request( 'GET', 'http://api.openweathermap.org/data/2.5/weather', [ 'query' => [ 'name' => 'Dublin', 'appid' => bin2hex('app-id') ], 'timeout' => 5.0 ] ); $content = json_decode($response->getContent(), true); $this->assertEquals(200, $response->getStatusCode()); $this->assertArrayHasKey('name', $content); $this->assertEquals('Dublin', $content['name']); $this->assertArrayHasKey('weather', $content); $this->assertArrayHasKey('main', $content); $this->assertArrayHasKey('temp', $content['main']); $this->assertEquals(1, $client->getRequestsCount()); }

Slide 39

Slide 39 text

New Features Symfony 5.2

Slide 40

Slide 40 text

New Features in Symfony 5.2 - Retry Failed framework: http_client: default_options: retry_failed: # retry_strategy: app.custom_strategy http_codes: 0: [ 'GET', 'HEAD' ] # retry network errors if request method is GET or HEAD 429: true # retry all responses with 429 status code 500: [ 'GET', 'HEAD' ] max_retries: 2 delay: 1000 multiplier: 3 max_delay: 5000 jitter: 0.3 #config/packages/framework.yaml

Slide 41

Slide 41 text

New Features in Symfony 5.2 - Retry Failed public const IDEMPOTENT_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']; public const DEFAULT_RETRY_STATUS_CODES = [ 0 => self::IDEMPOTENT_METHODS, // for transport exceptions 423, 425, 429, 500 => self::IDEMPOTENT_METHODS, 502, 503, 504 => self::IDEMPOTENT_METHODS, 507 => self::IDEMPOTENT_METHODS, 510 => self::IDEMPOTENT_METHODS, ]; #Retry/GenericRetryStrategy.php

Slide 42

Slide 42 text

New Features in Symfony 5.2 - Retry Failed $response = $this->client->request( 'GET', 'http://httpstat.us/500', [ 'headers' => ['Content-type' => 'application/json'] ] ); echo $response->getStatusCode(); dd($response->getInfo());

Slide 43

Slide 43 text

New Features in Symfony 5.2 - Retry Failed

Slide 44

Slide 44 text

class AsyncDecoratorTraitExampleService implements HttpClientInterface { use AsyncDecoratorTrait; public function request(string $method, string $url, array $options = []): ResponseInterface { return new AsyncResponse($method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) { // do what you want with chunks, e.g. split them // in smaller chunks, group them, skip some, etc. return $context->getResponse(); }); } New Features in Symfony 5.2 - Async Decorator Trait

Slide 45

Slide 45 text

New Features in Symfony 5.2 - Event Source Http Client use Symfony\Component\HttpClient\Chunk\ServerSentEvent; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Component\HttpClient\HttpClient; $client = new EventSourceHttpClient($client); // this is the URL that sends server events $source = $client->connect('https://localhost:8080/events'); while ($source) { foreach ($client->stream($source, 2) as $r => $chunk) { // this is a special ServerSentEvent chunk holding the pushed message if ($chunk instanceof ServerSentEvent) { // do something with the message } } }

Slide 46

Slide 46 text

New Features in Symfony 5.2 - Extra Curl Options $response = $this->client->request( 'GET', '/data/2.5/weather', [ 'query' => [ 'id' => $cityId, ], 'extra' => [ 'curl' => [ CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V6 ] ] ...

Slide 47

Slide 47 text

References https:/ /symfony.com/doc/current/http_client.html https:/ /github.com/symfony https:/ /github.com/brunohsouza/http-client-examples That Podcast With Beau And Dave

Slide 48

Slide 48 text

THANK YOU