@@ -1495,6 +1495,110 @@ This allows using them where native PHP streams are needed::
14951495 // later on if you need to, you can access the response from the stream
14961496 $response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
14971497
1498+ Extensibility
1499+ -------------
1500+
1501+ In order to extend the behavior of a base HTTP client, decoration is the way to go::
1502+
1503+ class MyExtendedHttpClient implements HttpClientInterface
1504+ {
1505+ private $decoratedClient;
1506+
1507+ public function __construct(HttpClientInterface $decoratedClient = null)
1508+ {
1509+ $this->decoratedClient = $decoratedClient ?? HttpClient::create();
1510+ }
1511+
1512+ public function request(string $method, string $url, array $options = []): ResponseInterface
1513+ {
1514+ // do what you want here with $method, $url and/or $options
1515+
1516+ $response = $this->decoratedClient->request();
1517+
1518+ //!\ calling any method on $response here would break async, see below for a better way
1519+
1520+ return $response;
1521+ }
1522+
1523+ public function stream($responses, float $timeout = null): ResponseStreamInterface
1524+ {
1525+ return $this->decoratedClient->stream($responses, $timeout);
1526+ }
1527+ }
1528+
1529+ A decorator like this one is suited for use cases where processing the
1530+ requests' arguments is enough.
1531+
1532+ By decorating the ``on_progress `` option, one can
1533+ even implement basic monitoring of the response. But since calling responses'
1534+ methods forces synchronous operations, doing so in ``request() `` breaks async.
1535+ The solution then is to also decorate the response object itself.
1536+ :class: `Symfony\\ Component\\ HttpClient\\ TraceableHttpClient ` and
1537+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ TraceableResponse ` are good
1538+ examples as a starting point.
1539+
1540+ .. versionadded :: 5.2
1541+
1542+ ``AsyncDecoratorTrait `` was introduced in Symfony 5.2.
1543+
1544+ In order to help writing more advanced response processors, the component provides
1545+ an :class: `Symfony\\ Component\\ HttpClient\\ AsyncDecoratorTrait `. This trait allows
1546+ processing the stream of chunks as they come back from the network::
1547+
1548+ class MyExtendedHttpClient implements HttpClientInterface
1549+ {
1550+ use AsyncDecoratorTrait;
1551+
1552+ public function request(string $method, string $url, array $options = []): ResponseInterface
1553+ {
1554+ // do what you want here with $method, $url and/or $options
1555+
1556+ $passthru = function (ChunkInterface $chunk, AsyncContext $context) {
1557+
1558+ // do what you want with chunks, e.g. split them
1559+ // in smaller chunks, group them, skip some, etc.
1560+
1561+ yield $chunk;
1562+ };
1563+
1564+ return new AsyncResponse($this->client, $method, $url, $options, $passthru);
1565+ }
1566+ }
1567+
1568+ Because the trait already implements a constructor and the ``stream() `` method,
1569+ you don't need to add them. The ``request() `` method should still be defined;
1570+ it shall return an
1571+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncResponse `.
1572+
1573+ The custom processing of chunks should happen in ``$passthru ``: this generator
1574+ is where you need to write your logic. It will be called for each chunk yielded by
1575+ the underlying client. A ``$passthru `` that does nothing would just ``yield $chunk; ``.
1576+ Of course, you could also yield a modified chunk, split the chunk into many
1577+ ones by yielding several times, or even skip a chunk altogether by issuing a
1578+ ``return; `` instead of yielding.
1579+
1580+ In order to control the stream, the chunk passthru receives an
1581+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncContext ` as second
1582+ argument. This context object has methods to read the current state of the
1583+ response. It also allows altering the response stream with methods to create new
1584+ chunks of content, pause the stream, cancel the stream, change the info of the
1585+ response, replace the current request by another one or change the chunk passthru
1586+ itself.
1587+
1588+ Checking the test cases implemented in
1589+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ Tests\\ AsyncDecoratorTraitTest `
1590+ might be a good start to get various working examples for a better understanding.
1591+ Here are the use cases that it simulates:
1592+
1593+ * retry a failed request;
1594+ * send a preflight request, e.g. for authentication needs;
1595+ * issue subrequests and include their content in the main response's body.
1596+
1597+ The logic in :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncResponse ` has
1598+ many safety checks that will throw a ``LogicException `` if the chunk passthru
1599+ doesn't behave correctly; e.g. if a chunk is yielded after an ``isLast() `` one,
1600+ or if a content chunk is yielded before an ``isFirst() `` one, etc.
1601+
14981602Testing HTTP Clients and Responses
14991603----------------------------------
15001604
0 commit comments