From f41ef122c26ec0bb532dab399d2ba4c9f93af8eb Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 30 May 2020 15:42:21 +0200 Subject: [PATCH] [HttpClient] add doc about extending and AsyncDecoratorTrait --- http_client.rst | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/http_client.rst b/http_client.rst index 42a4b0388da..7c4c50932ae 100644 --- a/http_client.rst +++ b/http_client.rst @@ -1322,6 +1322,110 @@ This allows using them where native PHP streams are needed:: // later on if you need to, you can access the response from the stream $response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse(); +Extensibility +------------- + +In order to extend the behavior of a base HTTP client, decoration is the way to go:: + + class MyExtendedHttpClient implements HttpClientInterface + { + private $decoratedClient; + + public function __construct(HttpClientInterface $decoratedClient = null) + { + $this->decoratedClient = $decoratedClient ?? HttpClient::create(); + } + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + // do what you want here with $method, $url and/or $options + + $response = $this->decoratedClient->request(); + + //!\ calling any method on $response here would break async, see below for a better way + + return $response; + } + + public function stream($responses, float $timeout = null): ResponseStreamInterface + { + return $this->decoratedClient->stream($responses, $timeout); + } + } + +A decorator like this one is suited for use cases where processing the +requests' arguments is enough. + +By decorating the ``on_progress`` option, one can +even implement basic monitoring of the response. But since calling responses' +methods forces synchronous operations, doing so in ``request()`` breaks async. +The solution then is to also decorate the response object itself. +:class:`Symfony\\Component\\HttpClient\\TraceableHttpClient` and +:class:`Symfony\\Component\\HttpClient\\Response\\TraceableResponse` are good +examples as a starting point. + +.. versionadded:: 5.2 + + ``AsyncDecoratorTrait`` was introduced in Symfony 5.2. + +In order to help writing more advanced response processors, the component provides +an :class:`Symfony\\Component\\HttpClient\\AsyncDecoratorTrait`. This trait allows +processing the stream of chunks as they come back from the network:: + + class MyExtendedHttpClient implements HttpClientInterface + { + use AsyncDecoratorTrait; + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + // do what you want here with $method, $url and/or $options + + $passthru = function (ChunkInterface $chunk, AsyncContext $context) { + + // do what you want with chunks, e.g. split them + // in smaller chunks, group them, skip some, etc. + + yield $chunk; + }; + + return new AsyncResponse($this->client, $method, $url, $options, $passthru); + } + } + +Because the trait already implements a constructor and the ``stream()`` method, +you don't need to add them. The ``request()`` method should still be defined; +it shall return an +:class:`Symfony\\Component\\HttpClient\\Response\\AsyncResponse`. + +The custom processing of chunks should happen in ``$passthru``: this generator +is where you need to write your logic. It will be called for each chunk yielded by +the underlying client. A ``$passthru`` that does nothing would just ``yield $chunk;``. +Of course, you could also yield a modified chunk, split the chunk into many +ones by yielding several times, or even skip a chunk altogether by issuing a +``return;`` instead of yielding. + +In order to control the stream, the chunk passthru receives an +:class:`Symfony\\Component\\HttpClient\\Response\\AsyncContext` as second +argument. This context object has methods to read the current state of the +response. It also allows altering the response stream with methods to create new +chunks of content, pause the stream, cancel the stream, change the info of the +response, replace the current request by another one or change the chunk passthru +itself. + +Checking the test cases implemented in +:class:`Symfony\\Component\\HttpClient\\Response\\Tests\\AsyncDecoratorTraitTest` +might be a good start to get various working examples for a better understanding. +Here are the use cases that it simulates: + +* retry a failed request; +* send a preflight request, e.g. for authentication needs; +* issue subrequests and include their content in the main response's body. + +The logic in :class:`Symfony\\Component\\HttpClient\\Response\\AsyncResponse` has +many safety checks that will throw a ``LogicException`` if the chunk passthru +doesn't behave correctly; e.g. if a chunk is yielded after an ``isLast()`` one, +or if a content chunk is yielded before an ``isFirst()`` one, etc. + Testing HTTP Clients and Responses ----------------------------------