@@ -1322,6 +1322,110 @@ This allows using them where native PHP streams are needed::
1322
1322
// later on if you need to, you can access the response from the stream
1323
1323
$response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
1324
1324
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
+
1325
1429
Testing HTTP Clients and Responses
1326
1430
----------------------------------
1327
1431
0 commit comments