Slide 1

Slide 1 text

en asynchrone avec Symfony HttpClient

Slide 2

Slide 2 text

Allison Guilhem @Les-Tilleuls.coop Alli_g83 Allison E.Guilhem a_guilhem Qui suis je ?

Slide 3

Slide 3 text

Jongler en asynchrone avec Symfony HttpClient Symfony HttpClient : quelques rappels L’AsyncDecoratorTrait et son « contexte » Et si on passait à la pratique ?

Slide 4

Slide 4 text

Il était une fois Symfony 4.3 ⾣ composer require symfony/http-client

Slide 5

Slide 5 text

HttpClient: quelques rappels - un composant standalone - pour consommer des APIs - par des opérations synchrones ou asynchrones - « Native PHP Streams » et cURL.

Slide 6

Slide 6 text

HttpClient: quelques rappels - http_client service - sans état - injecté automatiquement via type hint HttpClientInterface - Symfony/http-client-contracts: ensemble d’abstractions découplé du composant en lui même - Multitude d’options public function __construct( private HttpClientInterface $client, ) { } public function test(): array { $response = $this->client->request( 'GET', ‘https://url..’, […] );

Slide 7

Slide 7 text

HttpClient: quelques rappels - Réponses lazy - Requêtes concurrentes - Multiplexing : stream - Appréhension des erreurs par défaut - Inter-opérable et extensible foreach ($client->stream($responses)
 as $response => $chunk
 ) { if ($chunk->isFirst()) { … } elseif ($chunk->isLast()) { … } else { … } }

Slide 8

Slide 8 text

Le début d’une ré fl exion

Slide 9

Slide 9 text

Con fi rmation d’un besoin ?

Slide 10

Slide 10 text

- un réel besoin - décorateurs / middlewares / event ? - préserver la dimension asynchrone Manipulation des chunks

Slide 11

Slide 11 text

Des ré fl exions, des ré fl exions… et puis

Slide 12

Slide 12 text

AsyncDecoratorTrait - Pour des décorateurs de HttpClient - Qui ne consomment pas les réponses 
 pour préserver le cadre asynchrone

Slide 13

Slide 13 text

- Facilite et allège l’écriture de décorateurs 
 => Symfony\...\HttpClient\DecoratorTrait - Pour la réponse qu’il nous indique de câbler 
 => Symfony\...\HttpClient\Response\AsyncResponse AsyncDecoratorTrait

Slide 14

Slide 14 text

AsyncDecoratorTrait Va venir se câbler sur la méthode stream de l’AsyncResponse Un point d’entrée Coeur de la logique métier

Slide 15

Slide 15 text

Qui est cette AsyncResponse ?

Slide 16

Slide 16 text

AsyncResponse: Qui suis je ? class MyExtendedHttpClient implements HttpClientInterface { use AsyncDecoratorTrait; public function request(string $method, string $url, array $options = []): ResponseInterface { $passthru = function (ChunkInterface $chunk, AsyncContext $context) { yield $chunk; }; return new AsyncResponse($this->client, $method, $url, $options, $passthru); } }

Slide 17

Slide 17 text

Trop de magie ? Démysti fi ons tout ça !

Slide 18

Slide 18 text

AsyncResponse: comment je m’y prends? Le passthru callable /** * @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru */ public function __construct(HttpClientInterface $client, string $method, string $url, array $options, callable $passthru = null) { - AsyncResponse: un dernier argument non banal

Slide 19

Slide 19 text

AsyncResponse: comment je m’y prends? Le passthru callable - Une partie de logic « customizable » - Une dé fi nition dynamique, évolutive - Une dé fi nition qui doit respecter certaines règles $passthru = function (ChunkInterface $chunk, AsyncContext $context) { // do what you want with chunks yield $chunk; };

Slide 20

Slide 20 text

AsyncResponse: comment je m’y prends? Le passthru callable Quid: Comment faire le lien entre cet itérateur et la réponse de l’AsyncResponse

Slide 21

Slide 21 text

class AsyncContext { public function __construct( private ?callable &$passthru, private HttpClientInterface $client, private ResponseInterface &$response, private array &$info, private /*resource*/ $content, private int $offset, ) { } AsyncResponse: comment je m’y prends? Le passthru callable, un chunk et un context !

Slide 22

Slide 22 text

AsyncResponse: comment je m’y prends? Le passthru callable, un chunk et un context ! - DTO pour piloter l’AsyncResponse - Moyen de modi fi er le fl ux de réponse au niveau substantiel et temporel - Interroge la réponse sans pour autant la consommer - Agir sur la réponse (annulation, remplacement d’une réponse) - Agir sur l’itérateur maitrisant fl ux

Slide 23

Slide 23 text

AsyncResponse: comment je m’y prends? Le passthru callable Quid du contrôle de ce qui a été manipulé

Slide 24

Slide 24 text

Que fait AsyncResponse en interne ? Je suis gardien de la cohérence du fl ux de chunks : - pas de double fi rstChunk - Rien après le lastChunk Etc .. Générateur qui va émettre 
 0, 1 ou N chunks

Slide 25

Slide 25 text

Une simple question de timing et d’emboitement ! AsyncDecoratorTrait

Slide 26

Slide 26 text

AsyncDecoratorTrait - Symfony\Component\HttpClient\RetryableHttpClient - Symfony\Component\HttpClient\EventSourceHttpClient Décorateurs l’utilisant: Pour un aperçu des différentes pratiques: - Symfony\Component\HttpClient\Tests\AsyncDecoratorTraitTest

Slide 27

Slide 27 text

A l’attaque ?

Slide 28

Slide 28 text

Ne prenez pas peur !

Slide 29

Slide 29 text

Transclusion de plusieurs points d’API - Requête principale sur une API privée pour récupérer des résultats avec pagination - Besoin d’y greffer des réponses de deux autres points d’API

Slide 30

Slide 30 text

Transclusion de plusieurs points d’API public function index(MyExtendedHttpClient $client): Response { $response = $client->request( 'GET', ‘https://my-url.com/collection', [ // options you need etc 'user_data' => [ 'add' => [ 'availabilities' => 'https://my-url.com/collections/{id}/something', 'posts' => 'https://my-url.com/collections/{id}/somethingelse', ], 'concurrency' => null ], ] ); … }

Slide 31

Slide 31 text

Transclusion de plusieurs points d’API class MyExtendedHttpClient implements HttpClientInterface { use AsyncDecoratorTrait; … public function request(string $method, string $url, array $options = []): ResponseInterface { … $passthru = function (ChunkInterface $chunk, AsyncContext $context) use ($options) { … } return new AsyncResponse($this->client, $method, $url, $options, $passthru); } … }

Slide 32

Slide 32 text

Transclusion de plusieurs points d’API - Recherche sur secteur de prédilection et si une 404 est retournée pour la page 1 on relance pour rechercher secteur proche. - Si l’erreur est reçue sur une pagination haute, on laisse l’erreur. Premier enjeu:

Slide 33

Slide 33 text

$passthru = function (ChunkInterface $chunk, AsyncContext $context) use ($options) { static $content = ''; if ($chunk->isFirst()) { if (404 === $context->getStatusCode()) { $page = preg_split('/page=(\d)/', $context->getInfo()['original_url'], -1, PREG_SPLIT_DELIM_CAPTURE)[1]; if (1 === (int) $page) { // look for another sector // do what you want $context->getResponse()->cancel(); $context->replaceRequest( 'GET', ‘https://my-url.com/collection', $options ); return; } $context->passthru(); } yield $chunk; } $content .= $chunk->getContent(); if(!$chunk->isLast()) { return; } … Annule la réponse Permet de relancer et permet de remplacer la réponse par le résultat de cette nouvelle requête Permet de remettre en position neutre en l’occurence l’itérateur qui fi ltre nos chunks => pour recevoir 404

Slide 34

Slide 34 text

[\n {\n "id": 1,\n "name": "Leanne Graham",\n "username": "Bret",\n "email": "[email protected]",\n "address": {\n "street": "Kulas Light",\n "suite": "Apt. 556",\n "city": "Gwenborough",\n "zipcode": "92998-3874",\n "geo": {\n "lat": "-37.3159",\n "lng": "81.1496"\n }\n },\n "phone": "1-770-736-8031 x56442",\n },\n {\n …… }] Extrait du JSON de la réponse principale si pas d’erreur

Slide 35

Slide 35 text

Transclusion de plusieurs points d’API Deuxième enjeu: - Questionnement sur la meilleure façon d’aborder le problème. - Bonne performance et fl uidité => manipuler du json n’est pas toujours le plus léger - Nécessité d’avoir la possibilité de manipuler les bouts de réponses pour y effectuer des insertions de propriétés

Slide 36

Slide 36 text

Transclusion de plusieurs points d’API Car le but est de leur inclure une information additionnelle par des sous requêtes lancées de manière concurrente Pourquoi? Et pouvoir facilement identi fi premier bloc à traiter

Slide 37

Slide 37 text

array:10 [▼ 0 => '{'id':1,'name':'Leanne Graham','username':'Bret','email':'Sincere@april . biz','address':{'street':'Kulas Light','suite':'Apt . 556','city':'Gwenborough','zipcode' ▶’ 1 => '{'id':2,'name':'Ervin Howell','username':'Antonette','email':'Shanna@melissa . tv','address':{'street':'Victor Plains','suite':'Suite 879','city':'Wisokyburgh','z ▶' 2 => ‘{…} … ] JSON MANIPULABLE: parsedMainResponse

Slide 38

Slide 38 text

public static function updateData(array $parsedMainResponse, AsyncContext $context, array $options): array { … foreach ($parsedMainResponse as $mainKey => $subChunk) { ... $tempAdditionalResponses = []; … foreach ($toAdd as $key => $url) { preg_match('/"id":(\d+)/',$subChunk, $match); … $tempAdditionalResponses[] = sprintf(', "%s":', $key); $additionalResponse = $context->replaceRequest( 'GET', $url, $options ); $tempAdditionalResponses[] = $additionalResponse; } $tempAdditionalResponses[] = substr($subChunk, -1); … $blocks[] = $tempAdditionalResponses; } $blocks[] = $original; return $blocks; } 1. 1. identi fi e la requête principale grace au contexte 2. 2. Manipuler les sous parties pour envisager une place pour les insertions 3. 3. replace la dernière parenthèse après les insertions tant que c'est pas le dernier sous bloc on ajoute délimiteur etc 4. 4. Dans le but de terminer sur la réponse pour laquelle la requête a été faite initialement

Slide 39

Slide 39 text

array:7 [▼ 0 => '{'id':1,'name':'Leanne Graham','username':'Bret','email':'Sincere@april . biz','address': {'street':'Kulas Light','suite':'Apt . 556','city':'Gwenborough','zipcode' ▶' 1 => ', 'availabilities':' 2 => Symfony\Component\HttpClient\Response\TraceableResponse {#514 ▶} 3 => ', 'posts':' 4 => Symfony\Component\HttpClient\Response\TraceableResponse {#557 ▶} 5 => '}' 6 => ',' ] Un bloc parmi les blocs Transclusion de plusieurs points d’API

Slide 40

Slide 40 text

Transclusion de plusieurs points d’API //$parsedMainResponse = halaxa/json-machine ease json manipulation w/ PassThruDecoder yield $context->createChunk('['); //on va récupérer $part qui est un bloc (à ce niveau le 1er) while (null !== $chunk = self::passthru($context, $part)->current()) { yield $chunk; } $context->passthru(static function (ChunkInterface $chunk, AsyncContext $context) use (&$parsedMainResponse, &$part, &$blocks, $options) { … } Sous requêtes de manière concurrente + distinction du premier bloc Itérateur parcouru jusqu’à ce qu’il atteigne une ResponseInterface: AsyncContext - replaceReponse Dé fi nit un nouveau passthru callable par lequel passeront les chunks => en l’occurence reprendre sur la réponse de la première propriété ajoutée On est ici !

Slide 41

Slide 41 text

Transclusion de plusieurs points d’API Rappel de la situation: Bout de réponse de requête principale Traitement du « premier» bloc Jusquà’ce qu’il tombe sur ResponseInterface is LastChunk ? Action: remplacer la réponse via AsyncContext On est ici ! Découpage de la réponse de requête principale + Suivi des blocs re-manipulés

Slide 42

Slide 42 text

Transclusion de plusieurs points d’API Troisième enjeu: - A partir de ce nouveau passthru, nécessité d’assurer une continuité pour reconstruire notre réponse principale tant que notre traitement n’est pas terminé. - Trois points important à gérer: - Quand un premier chunk est émis - Quand un dernier chunk est émis et qu’il y a toujours des blocs à traiter - Ce qui ne rentre pas dans un des cas précédents

Slide 43

Slide 43 text

Transclusion de plusieurs points d’API - A déjà été « émis » et ne peut pas émettre plusieurs fi rstChunk - Important d’émettre ce qui est émis => dans notre cas il va s’agir des réponses aux sous requêtes if ($chunk->isFirst()) { return; } yield $chunk;

Slide 44

Slide 44 text

Transclusion de plusieurs points d’API Quid du lastChunk émis relatif à la sous requête ? L’émettre ? Le manipuler ? Requête mal formée Oui mais comment ?

Slide 45

Slide 45 text

Transclusion de plusieurs points d’API On reboucle avec l’AsyncContext !

Slide 46

Slide 46 text

Transclusion de plusieurs points d’API Quand on arrive au lastChunk on va se trouver dans cette situation: array:4 [▼ 3 => ', 'posts':' 4 => Symfony\Component\HttpClient\ Response\TraceableResponse {#550 ▶} 5 => '}' 6 => ',' ] Bloc / Part array:7 [▼ 0 => '{'id':1,'name':'Leanne Graham','username':'Bret','email':'Sinc ere@april . biz','address': {'street':'Kulas Light','suite':'Apt . 556','city':'Gwenborough','zipcode' ▶' 1 => ', 'availabilities':' 2 => Symfony\Component\HttpClient\Response\T raceableResponse {#514 ▶} 3 => ', 'posts':' 4 => Symfony\Component\HttpClient\Response\T raceableResponse {#557 ▶} 5 => '}' 6 => ',' ] Bloc / Part

Slide 47

Slide 47 text

Bloc à ce niveau créer le chunk à ce niveau pour l’émettre Remplacer la réponse pour pouvoir la traiter et sera émise au prochain passage Transclusion de plusieurs points d’API Que devons nous faire ? Créer un chunk à ce niveau et sera émis par la suite Avec l’aide de l’AsyncContext array:4 [▼ 3 => ', 'posts':' 4 => Symfony\Component\HttpClient\ Response\TraceableResponse {#550 ▶} 5 => '}' 6 => ',' ]

Slide 48

Slide 48 text

Transclusion de plusieurs points d’API Bout de réponse de requête principale Traitement du bloc Jusqu'à ce qu’il tombe sur ResponseInterface is LastChunk? Action: remplacer la réponse via AsyncContext Ou Bloc fi ni Traitement bloc à n+1 LastChunk Ignore les fi rstChunk Découpage de la réponse de requête principale + Suivi des blocs re-manipulés

Slide 49

Slide 49 text

Transclusion de plusieurs points d’API Important d’identi fi er quand ça se termine - nécessité d’émettre un chunk - revenir sur la réponse pour laquelle on a fait l’appel initialement. Faire attention à bien reconstituer le json ! Syntax Error !

Slide 50

Slide 50 text

Transclusion de plusieurs points d’API: Qu’est ce qu’on y a gagné - Flux de réponse facilement manipulable dans un cadre asynchrone préservé : un asyncContext bien utile - Des requêtes additionnelles effectuées de manière concurrente peu importe le sous objet qu’elles référencent dans la première réponse - Une très bonne performance QUID de la limite du nombre de requêtes concurrentes émises ? Simulation sur fake api

Slide 51

Slide 51 text

Transclusion de plusieurs points d’API Et la limite sur le nombre de requêtes concurrentes dans tout ça ? public static function updateData(array $parsedMainResponse, AsyncContext $context, array $options): array { … $concurrencyLimit = (int) $options['user_data']['concurrency']; … foreach ($parsedMainResponse as $mainKey => $subChunk) { if ($concurrencyLimit && $concurrencyLimit < $mainKey) { break; } … } … }

Slide 52

Slide 52 text

Transclusion de plusieurs points d’API Bout de réponse de requête principale Découpage de la réponse de requête principale + Suivi des blocs re-manipulés Traitement du bloc Jusqu'à ce qu’il tombe sur ResponseInterface is LastChunk ? Action: remplacer la réponse via AsyncContext Ou Bloc fi ni Traitement bloc à n+1 Un nombre limité de blocs re manipulés LastChunk Ignore les fi rstChunk Quatrième enjeu : contrôle des requêtes concurrentes

Slide 53

Slide 53 text

Transclusion de plusieurs points d’API - Un fl ux de requêtes concurrentes facilement identi fi able et manipulable - AsyncContext : fonctionnalité de « pause » si nécessité également Simulation sur fake api

Slide 54

Slide 54 text

Jongler en asynchrone avec Symfony HttpClient Et donc au fi nal ? - Un AsyncDecoratorTrait indispensable ? - Un combo « générateur » + AsyncContext + AsyncResponse gagnant - Un combo gagnant mais pas pour toutes les situations non plus - Une fl exibilité dans la manipulation des chunks non handicapante pour la performance

Slide 55

Slide 55 text

Jongler en asynchrone avec Symfony HttpClient Et donc au fi nal ? Mais, vous savez, moi je ne crois pas qu’il y ait de bon ou de mauvais processus en soi. Moi, si je devais résumer la vie de Symfony HttpClient aujourd’hui avec vous, je dirais que c’est d’abord des recherches, une documentation qui m’a tendu la main, peut-être à un moment où je ne pouvais pas, où j’étais seule dans mes pensées. Et c’est assez curieux de se dire que les hasards, les frustrations, les documentations forgent une destinée… Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas le processus en face, je dirais, le processus qui vous aide à avancer. Alors ce n’est pas mon cas, comme je le disais là, puisque moi au contraire, j’ai pu ; et je dis Merci à l’AsyncDecoratorTrait, je dis merci à la communauté Symfony… Monologue de Otis dans Asterix et Cléopatre revisité