@@ -1322,6 +1322,110 @@ This allows using them where native PHP streams are needed::
13221322 // later on if you need to, you can access the response from the stream
13231323 $response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
13241324
1325+ Extensibility
1326+ -------------
1327+
1328+ In order to extend the behavior of a base HTTP client, decoration is the way to go::
1329+
1330+ class MyExtendedHttpClient implements HttpClientInterface
1331+ {
1332+ private $decoratedClient;
1333+
1334+ public function __construct(HttpClientInterface $decoratedClient = null)
1335+ {
1336+ $this->decoratedClient = $decoratedClient ?? HttpClient::create();
1337+ }
1338+
1339+ public function request(string $method, string $url, array $options = []): ResponseInterface
1340+ {
1341+ // do what you want here with $method, $url and/or $options
1342+
1343+ $response = $this->decoratedClient->request();
1344+
1345+ //!\ calling any method on $response here would break async, see below for a better way
1346+
1347+ return $response;
1348+ }
1349+
1350+ public function stream($responses, float $timeout = null): ResponseStreamInterface
1351+ {
1352+ return $this->decoratedClient->stream($responses, $timeout);
1353+ }
1354+ }
1355+
1356+ A decorator like this one is suited for use cases where processing the
1357+ requests' arguments is enough.
1358+
1359+ By decorating the ``on_progress `` option, one can
1360+ even implement basic monitoring of the response. But since calling responses'
1361+ methods forces synchronous operations, doing so in ``request() `` breaks async.
1362+ The solution then is to also decorate the response object itself.
1363+ :class: `Symfony\\ Component\\ HttpClient\\ TraceableHttpClient ` and
1364+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ TraceableResponse ` are good
1365+ examples as a starting point.
1366+
1367+ .. versionadded :: 5.2
1368+
1369+ ``AsyncDecoratorTrait `` was introduced in Symfony 5.2.
1370+
1371+ In order to help writing more advanced response processors, the component provides
1372+ an :class: `Symfony\\ Component\\ HttpClient\\ AsyncDecoratorTrait `. This trait allows
1373+ processing the stream of chunks as they come back from the network::
1374+
1375+ class MyExtendedHttpClient implements HttpClientInterface
1376+ {
1377+ use AsyncDecoratorTrait;
1378+
1379+ public function request(string $method, string $url, array $options = []): ResponseInterface
1380+ {
1381+ // do what you want here with $method, $url and/or $options
1382+
1383+ $passthru = function (ChunkInterface $chunk, AsyncContext $context) {
1384+
1385+ // do what you want with chunks, e.g. split them
1386+ // in smaller chunks, group them, skip some, etc.
1387+
1388+ yield $chunk;
1389+ };
1390+
1391+ return new AsyncResponse($this->client, $method, $url, $options, $passthru);
1392+ }
1393+ }
1394+
1395+ Because the trait already implements a constructor and the ``stream() `` method,
1396+ you don't need to add them. The ``request() `` method should still be defined;
1397+ it shall return an
1398+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncResponse `.
1399+
1400+ The custom processing of chunks should happen in ``$passthru ``: this generator
1401+ is where you need to write your logic. It will be called for each chunk yielded by
1402+ the underlying client. A ``$passthru `` that does nothing would just ``yield $chunk; ``.
1403+ Of course, you could also yield a modified chunk, split the chunk into many
1404+ ones by yielding several times, or even skip a chunk altogether by issuing a
1405+ ``return; `` instead of yielding.
1406+
1407+ In order to control the stream, the chunk passthru receives an
1408+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncContext ` as second
1409+ argument. This context object has methods to read the current state of the
1410+ response. It also allows altering the response stream with methods to create new
1411+ chunks of content, pause the stream, cancel the stream, change the info of the
1412+ response, replace the current request by another one or change the chunk passthru
1413+ itself.
1414+
1415+ Checking the test cases implemented in
1416+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ Tests\\ AsyncDecoratorTraitTest `
1417+ might be a good start to get various working examples for a better understanding.
1418+ Here are the use cases that it simulates:
1419+
1420+ * retry a failed request;
1421+ * send a preflight request, e.g. for authentication needs;
1422+ * issue subrequests and include their content in the main response's body.
1423+
1424+ The logic in :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncResponse ` has
1425+ many safety checks that will throw a ``LogicException `` if the chunk passthru
1426+ doesn't behave correctly; e.g. if a chunk is yielded after an ``isLast() `` one,
1427+ or if a content chunk is yielded before an ``isFirst() `` one, etc.
1428+
13251429Testing HTTP Clients and Responses
13261430----------------------------------
13271431
0 commit comments