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