@@ -1495,6 +1495,110 @@ This allows using them where native PHP streams are needed::
1495
1495
// later on if you need to, you can access the response from the stream
1496
1496
$response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
1497
1497
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
+
1498
1602
Testing HTTP Clients and Responses
1499
1603
----------------------------------
1500
1604
0 commit comments