Skip to content

[HttpClient] add doc about extending and AsyncDecoratorTrait #13736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 20, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions http_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------------------

Expand Down