@@ -908,6 +908,110 @@ This allows using them where native PHP streams are needed::
908
908
// later on if you need to, you can access the response from the stream
909
909
$response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
910
910
911
+ Extensibility
912
+ -------------
913
+
914
+ In order to extend the behavior of a base HTTP client, decoration is the way to go::
915
+
916
+ class MyExtendedHttpClient implements HttpClientInterface
917
+ {
918
+ private $decoratedClient;
919
+
920
+ public function __construct(HttpClientInterface decoratedClient = null)
921
+ {
922
+ $this->decoratedClient = $decoratedClient ?? HttpClient::create();
923
+ }
924
+
925
+ public function request(string $method, string $url, array $options = []): ResponseInterface
926
+ {
927
+ // do what you want here with $method, $url and/or $options
928
+
929
+ $response = $this->decoratedClient->request();
930
+
931
+ //!\ calling any method on $response here would break async
932
+
933
+ return $response;
934
+ }
935
+
936
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
937
+ {
938
+ return $this->decoratedClient->stream($responses, $timeout);
939
+ }
940
+ }
941
+
942
+ A decorator like this one is suited for use cases where processing the
943
+ requests' arguments is enough.
944
+
945
+ By decorating the ``on_progress `` option, one can
946
+ even implement basic monitoring of the response. But since calling responses'
947
+ methods forces synchronous operations, doing so in ``request() `` breaks async.
948
+ The solution then is to also decorate the response object itself.
949
+ :class: `Symfony\\ Component\\ HttpClient\\ TraceableHttpClient ` and
950
+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ TraceableResponse ` are good
951
+ examples as a starting point.
952
+
953
+ .. versionadded :: 5.2
954
+
955
+ ``AsyncDecoratorTrait `` was introduced in Symfony 5.2.
956
+
957
+ In order to help writing more advanced response processors, the component provides
958
+ an :class: `Symfony\\ Component\\ HttpClient\\ AsyncDecoratorTrait `. This trait allows
959
+ processing the stream of chunks as they come back from the network::
960
+
961
+ class MyExtendedHttpClient implements HttpClientInterface
962
+ {
963
+ use AsyncDecoratorTrait;
964
+
965
+ public function request(string $method, string $url, array $options = []): ResponseInterface
966
+ {
967
+ // do what you want here with $method, $url and/or $options
968
+
969
+ $passthru = function (ChunkInterface $chunk, AsyncContext $context) {
970
+
971
+ // do what you want with chunks, e.g. split them
972
+ // in smaller chunks, group them, skip some, etc.
973
+
974
+ yield $chunk;
975
+ };
976
+
977
+ return new AsyncResponse($method, $url, $options, $chunk, $passthru);
978
+ }
979
+ }
980
+
981
+ Because the trait already implements a constructor and the ``stream() `` method,
982
+ you don't need to add them. The ``request() `` method should still be defined;
983
+ it shall return an
984
+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncResponse `.
985
+
986
+ The custom processing of chunks should happen in ``$passthru ``: this generator
987
+ is where you need to write your logic. It will be called for each chunk yielded by
988
+ the underlying client. A ``$passthru `` that does nothing would just ``yield $chunk; ``.
989
+ Of course, you could also yield a modified chunk, split the chunk into many
990
+ ones by yielding several times, or even skip a chunk altogether by issuing a
991
+ ``return; `` instead of yielding.
992
+
993
+ In order to control the stream, the chunk passthru receives an
994
+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncContext ` as second
995
+ argument. This context object has methods to read the current state of the
996
+ response. It also allows altering the response stream with methods to create new
997
+ chunks of content, pause the stream, cancel the stream, change the info of the
998
+ response, replace the current request by another one or change the chunk passthru
999
+ itself.
1000
+
1001
+ Checking the test cases implemented in
1002
+ :class: `Symfony\\ Component\\ HttpClient\\ Response\\ Tests\\ AsyncDecoratorTraitTest `
1003
+ might be a good start to get various working examples for a better understanding.
1004
+ Here are the use cases that it simulates:
1005
+
1006
+ * retry a failed request;
1007
+ * send a preflight request, e.g. for authentication needs;
1008
+ * issue subrequests and include their content in the main response's body.
1009
+
1010
+ The logic in :class: `Symfony\\ Component\\ HttpClient\\ Response\\ AsyncResponse ` has
1011
+ many safety checks that will throw a ``LogicException `` if the chunk passthru
1012
+ doesn't behave correctly; e.g. if a chunk is yielded after an ``isLast() `` one,
1013
+ or if a content chunk is yielded before an ``isFirst() `` one, etc.
1014
+
911
1015
Symfony Framework Integration
912
1016
-----------------------------
913
1017
0 commit comments