The HttpClient Component

The HttpClient component is a low-level HTTP client with support for bothPHP stream wrappers and cURL. It provides utilities to consume APIs andsupports synchronous and asynchronous operations.

New in version 4.3: The HttpClient component was introduced in Symfony 4.3 and it's stillconsidered an experimental feature.

Installation

  1. $ composer require symfony/http-client

Note

If you install this component outside of a Symfony application, you mustrequire the vendor/autoload.php file in your code to enable the classautoloading mechanism provided by Composer. Readthis article for more details.

Basic Usage

Use the HttpClient class to create thelow-level HTTP client that makes requests, like the following GET request:

  1. use Symfony\Component\HttpClient\HttpClient;
  2.  
  3. $client = HttpClient::create();
  4. $response = $client->request('GET', 'https://api.github.com/repos/symfony/symfony-docs');
  5.  
  6. $statusCode = $response->getStatusCode();
  7. // $statusCode = 200
  8. $contentType = $response->getHeaders()['content-type'][0];
  9. // $contentType = 'application/json'
  10. $content = $response->getContent();
  11. // $content = '{"id":521583, "name":"symfony-docs", ...}'
  12. $content = $response->toArray();
  13. // $content = ['id' => 521583, 'name' => 'symfony-docs', ...]

Performance

The component is built for maximum HTTP performance. By design, it is compatiblewith HTTP/2 and with doing concurrent asynchronous streamed and multiplexedrequests/responses. Even when doing regular synchronous calls, this designallows keeping connections to remote hosts open between requests, improvingperformance by saving repetitive DNS resolution, SSL negotiation, etc.To leverage all these design benefits, the cURL extension is needed.

Enabling cURL Support

This component supports both the native PHP streams and cURL to make the HTTPrequests. Although both are interchangeable and provide the same features,including concurrent requests, HTTP/2 is only supported when using cURL.

HttpClient::create() selects the cURL transport if the cURL PHP extensionis enabled and falls back to PHP streams otherwise. If you prefer to selectthe transport explicitly, use the following classes to create the client:

  1. use Symfony\Component\HttpClient\CurlHttpClient;
  2. use Symfony\Component\HttpClient\NativeHttpClient;
  3.  
  4. // uses native PHP streams
  5. $client = new NativeHttpClient();
  6.  
  7. // uses the cURL PHP extension
  8. $client = new CurlHttpClient();

When using this component in a full-stack Symfony application, this behavior isnot configurable and cURL will be used automatically if the cURL PHP extensionis installed and enabled. Otherwise, the native PHP streams will be used.

HTTP/2 Support

When requesting an https URL, HTTP/2 is enabled by default if libcurl >= 7.36is used. To force HTTP/2 for http URLs, you need to enable it explicitly viathe http_version option:

  1. $client = HttpClient::create(['http_version' => '2.0']);

Support for HTTP/2 PUSH works out of the box when libcurl >= 7.61 is used withPHP >= 7.2.17 / 7.3.4: pushed responses are put into a temporary cache and areused when a subsequent request is triggered for the corresponding URLs.

Making Requests

The client created with the HttpClient class provides a single request()method to perform all kinds of HTTP requests:

  1. $response = $client->request('GET', 'https://...');
  2. $response = $client->request('POST', 'https://...');
  3. $response = $client->request('PUT', 'https://...');
  4. // ...

Responses are always asynchronous, so that the call to the method returnsimmediately instead of waiting to receive the response:

  1. // code execution continues immediately; it doesn't wait to receive the response
  2. $response = $client->request('GET', 'http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso');
  3.  
  4. // getting the response headers waits until they arrive
  5. $contentType = $response->getHeaders()['content-type'][0];
  6.  
  7. // trying to get the response contents will block the execution until
  8. // the full response contents are received
  9. $contents = $response->getContent();

This component also supports streaming responsesfor full asynchronous applications.

Note

HTTP compression and chunked transfer encoding are automatically enabled whenboth your PHP runtime and the remote server support them.

Authentication

The HTTP client supports different authentication mechanisms. They can bedefined globally when creating the client (to apply it to all requests) and toeach request (which overrides any global authentication):

  1. // Use the same authentication for all requests
  2. $client = HttpClient::create([
  3. // HTTP Basic authentication with only the username and not a password
  4. 'auth_basic' => ['the-username'],
  5.  
  6. // HTTP Basic authentication with a username and a password
  7. 'auth_basic' => ['the-username', 'the-password'],
  8.  
  9. // HTTP Bearer authentication (also called token authentication)
  10. 'auth_bearer' => 'the-bearer-token',
  11. ]);
  12.  
  13. $response = $client->request('GET', 'https://...', [
  14. // use a different HTTP Basic authentication only for this request
  15. 'auth_basic' => ['the-username', 'the-password'],
  16.  
  17. // ...
  18. ]);

Query String Parameters

You can either append them manually to the requested URL, or define them as anassociative array via the query option, that will be merged with the URL:

  1. // it makes an HTTP GET request to https://httpbin.org/get?token=...&name=...
  2. $response = $client->request('GET', 'https://httpbin.org/get', [
  3. // these values are automatically encoded before including them in the URL
  4. 'query' => [
  5. 'token' => '...',
  6. 'name' => '...',
  7. ],
  8. ]);

Headers

Use the headers option to define both the default headers added to allrequests and the specific headers for each request:

  1. // this header is added to all requests made by this client
  2. $client = HttpClient::create(['headers' => [
  3. 'User-Agent' => 'My Fancy App',
  4. ]]);
  5.  
  6. // this header is only included in this request and overrides the value
  7. // of the same header if defined globally by the HTTP client
  8. $response = $client->request('POST', 'https://...', [
  9. 'headers' => [
  10. 'Content-Type' => 'text/plain',
  11. ],
  12. ]);

Uploading Data

This component provides several methods for uploading data using the bodyoption. You can use regular strings, closures, iterables and resources and they'll beprocessed automatically when making the requests:

  1. $response = $client->request('POST', 'https://...', [
  2. // defining data using a regular string
  3. 'body' => 'raw data',
  4.  
  5. // defining data using an array of parameters
  6. 'body' => ['parameter1' => 'value1', '...'],
  7.  
  8. // using a closure to generate the uploaded data
  9. 'body' => function (int $size): string {
  10. // ...
  11. },
  12.  
  13. // using a resource to get the data from it
  14. 'body' => fopen('/path/to/file', 'r'),
  15. ]);

When uploading data with the POST method, if you don't define theContent-Type HTTP header explicitly, Symfony assumes that you're uploadingform data and adds the required'Content-Type: application/x-www-form-urlencoded' header for you.

When the body option is set as a closure, it will be called several times untilit returns the empty string, which signals the end of the body. Each time, theclosure should return a string smaller than the amount requested as argument.

A generator or any Traversable can also be used instead of a closure.

Tip

When uploading JSON payloads, use the json option instead of body. Thegiven content will be JSON-encoded automatically and the request will add theContent-Type: application/json automatically too:

  1. $response = $client->request('POST', 'https://...', [
  2. 'json' => ['param1' => 'value1', '...'],
  3. ]);
  4.  
  5. $decodedPayload = $response->toArray();

To submit a form with file uploads, it is your responsibility to encode the bodyaccording to the multipart/form-data content-type. TheSymfony Mime component makes it a few lines of code:

  1. use Symfony\Component\Mime\Part\DataPart;
  2. use Symfony\Component\Mime\Part\Multipart\FormDataPart;
  3.  
  4. $formFields = [
  5. 'regular_field' => 'some value',
  6. 'file_field' => DataPart::fromPath('/path/to/uploaded/file'),
  7. ];
  8. $formData = new FormDataPart($formFields);
  9. $client->request('POST', 'https://...', [
  10. 'headers' => $formData->getPreparedHeaders()->toArray(),
  11. 'body' => $formData->bodyToIterable(),
  12. ]);

Cookies

The HTTP client provided by this component is stateless but handling cookiesrequires a stateful storage (because responses can update cookies and they mustbe used for subsequent requests). That's why this component doesn't handlecookies automatically.

You can either handle cookies yourself using the Cookie HTTP header or usethe BrowserKit component which provides thisfeature and integrates seamlessly with the HttpClient component.

Redirects

By default, the HTTP client follows redirects, up to a maximum of 20, whenmaking a request. Use the max_redirects setting to configure this behavior(if the number of redirects is higher than the configured value, you'll get aRedirectionException):

  1. $response = $client->request('GET', 'https://...', [
  2. // 0 means to not follow any redirect
  3. 'max_redirects' => 0,
  4. ]);

HTTP Proxies

By default, this component honors the standard environment variables that yourOperating System defines to direct the HTTP traffic through your local proxy.This means there is usually nothing to configure to have the client work withproxies, provided these env vars are properly configured.

You can still set or override these settings using the proxy and no_proxyoptions:

  • proxy should be set to the http://… URL of the proxy to get through
  • no_proxy disables the proxy for a comma-separated list of hosts that do notrequire it to get reached.

Progress Callback

By providing a callable to the on_progress option, one can trackuploads/downloads as they complete. This callback is guaranteed to be called onDNS resolution, on arrival of headers and on completion; additionally it iscalled when new data is uploaded or downloaded and at least once per second:

  1. $response = $client->request('GET', 'https://...', [
  2. 'on_progress' => function (int $dlNow, int $dlSize, array $info): void {
  3. // $dlNow is the number of bytes downloaded so far
  4. // $dlSize is the total size to be downloaded or -1 if it is unknown
  5. // $info is what $response->getInfo() would return at this very time
  6. },
  7. ]);

Any exceptions thrown from the callback will be wrapped in an instance ofTransportExceptionInterface and will abort the request.

Advanced Options

The HttpClientInterface defines all theoptions you might need to take full control of the way the request is performed,including DNS pre-resolution, SSL parameters, public key pinning, etc.

Processing Responses

The response returned by all HTTP clients is an object of typeResponseInterface which provides thefollowing methods:

  1. $response = $client->request('GET', 'https://...');
  2.  
  3. // gets the HTTP status code of the response
  4. $statusCode = $response->getStatusCode();
  5.  
  6. // gets the HTTP headers as string[][] with the header names lower-cased
  7. $headers = $response->getHeaders();
  8.  
  9. // gets the response body as a string
  10. $content = $response->getContent();
  11.  
  12. // casts the response JSON contents to a PHP array
  13. $content = $response->toArray();
  14.  
  15. // cancels the request/response
  16. $response->cancel();
  17.  
  18. // returns info coming from the transport layer, such as "response_headers",
  19. // "redirect_count", "start_time", "redirect_url", etc.
  20. $httpInfo = $response->getInfo();
  21. // you can get individual info too
  22. $startTime = $response->getInfo('start_time');
  23.  
  24. // returns detailed logs about the requests and responses of the HTTP transaction
  25. $httpLogs = $response->getInfo('debug');

Note

$response->getInfo() is non-blocking: it returns live informationabout the response. Some of them might not be known yet (e.g. http_code)when you'll call it.

Streaming Responses

Call the stream() method of the HTTP client to get chunks of theresponse sequentially instead of waiting for the entire response:

  1. $url = 'https://releases.ubuntu.com/18.04.1/ubuntu-18.04.1-desktop-amd64.iso';
  2. $response = $client->request('GET', $url, [
  3. // optional: if you don't want to buffer the response in memory
  4. 'buffer' => false,
  5. ]);
  6.  
  7. // Responses are lazy: this code is executed as soon as headers are received
  8. if (200 !== $response->getStatusCode()) {
  9. throw new \Exception('...');
  10. }
  11.  
  12. // get the response contents in chunk and save them in a file
  13. // response chunks implement Symfony\Contracts\HttpClient\ChunkInterface
  14. $fileHandler = fopen('/ubuntu.iso', 'w');
  15. foreach ($client->stream($response) as $chunk) {
  16. fwrite($fileHandler, $chunk->getContent());
  17. }

Canceling Responses

To abort a request (e.g. because it didn't complete in due time, or you want tofetch only the first bytes of the response, etc.), you can either use thecancel() method of ResponseInterface:

  1. $response->cancel()

Or throw an exception from a progress callback:

  1. $response = $client->request('GET', 'https://...', [
  2. 'on_progress' => function (int $dlNow, int $dlSize, array $info): void {
  3. // ...
  4.  
  5. throw new \MyException();
  6. },
  7. ]);

The exception will be wrapped in an instance of TransportExceptionInterfaceand will abort the request.

Handling Exceptions

When the HTTP status code of the response is in the 300-599 range (i.e. 3xx,4xx or 5xx) your code is expected to handle it. If you don't do that, thegetHeaders() and getContent() methods throw an appropriate exception:

  1. // the response of this request will be a 403 HTTP error
  2. $response = $client->request('GET', 'https://httpbin.org/status/403');
  3.  
  4. // this code results in a Symfony\Component\HttpClient\Exception\ClientException
  5. // because it doesn't check the status code of the response
  6. $content = $response->getContent();
  7.  
  8. // pass FALSE as the optional argument to not throw an exception and return
  9. // instead the original response content (even if it's an error message)
  10. $content = $response->getContent(false);

Concurrent Requests

Thanks to responses being lazy, requests are always managed concurrently.On a fast enough network, the following code makes 379 requests in less thanhalf a second when cURL is used:

  1. use Symfony\Component\HttpClient\CurlHttpClient;
  2.  
  3. $client = new CurlHttpClient();
  4.  
  5. $responses = [];
  6.  
  7. for ($i = 0; $i < 379; ++$i) {
  8. $uri = "https://http2.akamai.com/demo/tile-$i.png";
  9. $responses[] = $client->request('GET', $uri);
  10. }
  11.  
  12. foreach ($responses as $response) {
  13. $content = $response->getContent();
  14. // ...
  15. }

As you can read in the first "for" loop, requests are issued but are not consumedyet. That's the trick when concurrency is desired: requests should be sentfirst and be read later on. This will allow the client to monitor all pendingrequests while your code waits for a specific one, as done in each iteration ofthe above "foreach" loop.

Multiplexing Responses

If you look again at the snippet above, responses are read in requests' order.But maybe the 2nd response came back before the 1st? Fully asynchronous operationsrequire being able to deal with the responses in whatever order they come back.

In order to do so, the stream() method of HTTP clients accepts a list ofresponses to monitor. As mentioned previously,this method yields response chunks as they arrive from the network. By replacingthe "foreach" in the snippet with this one, the code becomes fully async:

  1. foreach ($client->stream($responses) as $response => $chunk) {
  2. if ($chunk->isFirst()) {
  3. // headers of $response just arrived
  4. // $response->getHeaders() is now a non-blocking call
  5. } elseif ($chunk->isLast()) {
  6. // the full content of $response just completed
  7. // $response->getContent() is now a non-blocking call
  8. } else {
  9. // $chunk->getContent() will return a piece
  10. // of the response body that just arrived
  11. }
  12. }

Tip

Use the user_data option combined with $response->getInfo('user_data')to track the identity of the responses in your foreach loops.

Dealing with Network Timeouts

This component allows dealing with both request and response timeouts.

A timeout can happen when e.g. DNS resolution takes too much time, when the TCPconnection cannot be opened in the given time budget, or when the responsecontent pauses for too long. This can be configured with the timeout requestoption:

  1. // A TransportExceptionInterface will be issued if nothing
  2. // happens for 2.5 seconds when accessing from the $response
  3. $response = $client->request('GET', 'https://...', ['timeout' => 2.5]);

The default_socket_timeout PHP ini setting is used if the option is not set.

The option can be overridden by using the 2nd argument of the stream() method.This allows monitoring several responses at once and applying the timeout to allof them in a group. If all responses become inactive for the given duration, themethod will yield a special chunk whose isTimeout() will return true:

  1. foreach ($client->stream($responses, 1.5) as $response => $chunk) {
  2. if ($chunk->isTimeout()) {
  3. // $response staled for more than 1.5 seconds
  4. }
  5. }

A timeout is not necessarily an error: you can decide to stream again theresponse and get remaining contents that might come back in a new timeout, etc.

Tip

Passing 0 as timeout allows monitoring responses in a non-blocking way.

Note

Timeouts control how long one is willing to wait while the HTTP transactionis idle. Big responses can last as long as needed to complete, provided theyremain active during the transfer and never pause for longer than specified.

Dealing with Network Errors

Network errors (broken pipe, failed DNS resolution, etc.) are thrown as instancesof TransportExceptionInterface.

First of all, you don't have to deal with them: letting errors bubble to yourgeneric exception-handling stack might be really fine in most use cases.

If you want to handle them, here is what you need to know:

To catch errors, you need to wrap calls to $client->request() but also callsto any methods of the returned responses. This is because responses are lazy, sothat network errors can happen when calling e.g. getStatusCode() too:

  1. try {
  2. // both lines can potentially throw
  3. $response = $client->request(...);
  4. $headers = $response->getHeaders();
  5. // ...
  6. } catch (TransportExceptionInterface $e) {
  7. // ...
  8. }

Note

Because $response->getInfo() is non-blocking, it shouldn't throw by design.

When multiplexing responses, you can deal with errors for individual streams bycatching TransportExceptionInterface in the foreach loop:

  1. foreach ($client->stream($responses) as $response => $chunk) {
  2. try {
  3. if ($chunk->isLast()) {
  4. // ... do something with $response
  5. }
  6. } catch (TransportExceptionInterface $e) {
  7. // ...
  8. }
  9. }

Caching Requests and Responses

This component provides a CachingHttpClientdecorator that allows caching responses and serving them from the local storagefor next requests. The implementation leverages theHttpCache class under the hoodso that the HttpKernel component needs to beinstalled in your application:

  1. use Symfony\Component\HttpClient\CachingHttpClient;
  2. use Symfony\Component\HttpClient\HttpClient;
  3. use Symfony\Component\HttpKernel\HttpCache\Store;
  4.  
  5. $store = new Store('/path/to/cache/storage/');
  6. $client = HttpClient::create();
  7. $client = new CachingHttpClient($client, $store);
  8.  
  9. // this won't hit the network if the resource is already in the cache
  10. $response = $client->request('GET', 'https://example.com/cacheable-resource');

CachingHttpClient accepts a third argument to set the options of the HttpCache.

Scoping Client

It's common that some of the HTTP client options depend on the URL of therequest (e.g. you must set some headers when making requests to GitHub API butnot for other hosts). If that's your case, this component provides a specialHTTP client via the ScopingHttpClientclass to autoconfigure the HTTP client based on the requested URL:

  1. use Symfony\Component\HttpClient\HttpClient;
  2. use Symfony\Component\HttpClient\ScopingHttpClient;
  3.  
  4. $client = HttpClient::create();
  5. $client = new ScopingHttpClient($client, [
  6. // the options defined as values apply only to the URLs matching
  7. // the regular expressions defined as keys
  8. 'https://api\.github\.com/' => [
  9. 'headers' => [
  10. 'Accept' => 'application/vnd.github.v3+json',
  11. 'Authorization' => 'token '.$githubToken,
  12. ],
  13. ],
  14. // ...
  15. ]);

You can define several scopes, so that each set of options is added only if arequested URL matches one of the regular expressions provided as keys.

If the request URL is relative (because you use the base_uri option), thescoping HTTP client can't make a match. That's why you can define a thirdoptional argument in its constructor which will be considered the defaultregular expression applied to relative URLs:

  1. // ...
  2.  
  3. $client = new ScopingHttpClient($client,
  4. [
  5. 'https://api\.github\.com/' => [
  6. 'base_uri' => 'https://api.github.com/',
  7. // ...
  8. ],
  9. ],
  10. // this is the index in the previous array that defines
  11. // the base URI that shoud be used to resolve relative URLs
  12. 'https://api\.github\.com/'
  13. );

The above example can be reduced to a simpler call:

  1. // ...
  2.  
  3. $client = ScopingHttpClient::forBaseUri($client, 'https://api.github.com/', [
  4. // ...
  5. ]);

This way, the provided options will be used only if the requested URL is relativeor if it matches the https://api.github.com/ base URI.

Interoperability

The component is interoperable with two different abstractions for HTTP clients:Symfony Contracts and PSR-18. If your application uses libraries that needany of them, the component is compatible with both. They also benefit fromautowiring aliases when theframework bundle is used.

If you are writing or maintaining a library that makes HTTP requests, you candecouple it from any specific HTTP client implementations by coding againsteither Symfony Contracts (recommended) or PSR-18.

Symfony Contracts

The interfaces found in the symfony/http-client-contracts package definethe primary abstractions implemented by the component. Its entry point is theHttpClientInterface. That's theinterface you need to code against when a client is needed:

  1. use Symfony\Contracts\HttpClient\HttpClientInterface;
  2.  
  3. class MyApiLayer
  4. {
  5. private $client;
  6.  
  7. public function __construct(HttpClientInterface $client)
  8. {
  9. $this->client = $client
  10. }
  11.  
  12. // [...]
  13. }

All request options mentioned above (e.g. timeout management) are also definedin the wordings of the interface, so that any compliant implementations (likethis component) is guaranteed to provide them. That's a major difference withthe PSR-18 abstraction, which provides none related to the transport itself.

Another major feature covered by the Symfony Contracts is async/multiplexing,as described in the previous sections.

PSR-18

This component implements the PSR-18 (HTTP Client) specifications via thePsr18Client class, which is an adapterto turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.

To use it, you need the psr/http-client package and a PSR-17 implementation:

  1. # installs the PSR-18 ClientInterface
  2. $ composer require psr/http-client
  3.  
  4. # installs an efficient implementation of response and stream factories
  5. # with autowiring aliases provided by Symfony Flex
  6. $ composer require nyholm/psr7

Now you can make HTTP requests with the PSR-18 client as follows:

  1. use Nyholm\Psr7\Factory\Psr17Factory;
  2. use Symfony\Component\HttpClient\Psr18Client;
  3.  
  4. $psr17Factory = new Psr17Factory();
  5. $psr18Client = new Psr18Client();
  6.  
  7. $url = 'https://symfony.com/versions.json';
  8. $request = $psr17Factory->createRequest('GET', $url);
  9. $response = $psr18Client->sendRequest($request);
  10.  
  11. $content = json_decode($response->getBody()->getContents(), true);

Symfony Framework Integration

When using this component in a full-stack Symfony application, you can configuremultiple clients with different configurations and inject them into your services.

Configuration

Use the framework.http_client key to configure the default HTTP client usedin the application. Check out the fullhttp_client config reference to learn about allthe available config options:

  1. # config/packages/framework.yaml
  2. framework:
  3. # ...
  4. http_client:
  5. max_host_connections: 10
  6. default_options:
  7. max_redirects: 7

If you want to define multiple HTTP clients, use this other expanded configuration:

  1. # config/packages/framework.yaml
  2. framework:
  3. # ...
  4. http_client:
  5. scoped_clients:
  6. crawler.client:
  7. headers: { 'X-Powered-By': 'ACME App' }
  8. http_version: '1.0'
  9. some_api.client:
  10. max_redirects: 5

Injecting the HTTP Client into Services

If your application only needs one HTTP client, you can inject the default oneinto any services by type-hinting a constructor argument with theHttpClientInterface:

  1. use Symfony\Contracts\HttpClient\HttpClientInterface;
  2.  
  3. class SomeService
  4. {
  5. private $client;
  6.  
  7. public function __construct(HttpClientInterface $client)
  8. {
  9. $this->client = $client;
  10. }
  11. }

If you have several clients, you must use any of the methods defined by Symfonyto choose a specific service. Each clienthas a unique service named after its configuration.

Each scoped client also defines a corresponding named autowiring alias.If you use for exampleSymfony\Contracts\HttpClient\HttpClientInterface $myApiClientas the type and name of an argument, autowiring will inject the my_api.clientservice into your autowired classes.

Testing HTTP Clients and Responses

This component includes the MockHttpClient and MockResponse classes touse them in tests that need an HTTP client which doesn't make actual HTTPrequests.

The first way of using MockHttpClient is to pass a list of responses to itsconstructor. These will be yielded in order when requests are made:

  1. use Symfony\Component\HttpClient\MockHttpClient;
  2. use Symfony\Component\HttpClient\Response\MockResponse;
  3.  
  4. $responses = [
  5. new MockResponse($body1, $info1),
  6. new MockResponse($body2, $info2),
  7. ];
  8.  
  9. $client = new MockHttpClient($responses);
  10. // responses are returned in the same order as passed to MockHttpClient
  11. $response1 = $client->request('...'); // returns $responses[0]
  12. $response2 = $client->request('...'); // returns $responses[1]

Another way of using MockHttpClient is to pass a callback that generates theresponses dynamically when it's called:

  1. use Symfony\Component\HttpClient\MockHttpClient;
  2. use Symfony\Component\HttpClient\Response\MockResponse;
  3.  
  4. $callback = function ($method, $url, $options) {
  5. return new MockResponse('...');
  6. };
  7.  
  8. $client = new MockHttpClient($callback);
  9. $response = $client->request('...'); // calls $callback to get the response

The responses provided to the mock client don't have to be instances ofMockResponse. Any class implementing ResponseInterface will work (e.g.$this->createMock(ResponseInterface::class)).

However, using MockResponse allows simulating chunked responses and timeouts:

  1. $body = function () {
  2. yield 'hello';
  3. // empty strings are turned into timeouts so that they are easy to test
  4. yield '';
  5. yield 'world';
  6. };
  7.  
  8. $mockResponse = new MockResponse($body());