| 1 |
<?php |
| 2 |
|
| 3 |
/* |
| 4 |
* This file is part of the Symfony package. |
| 5 |
* |
| 6 |
* (c) Fabien Potencier <[email protected]> |
| 7 |
* |
| 8 |
* For the full copyright and license information, please view the LICENSE |
| 9 |
* file that was distributed with this source code. |
| 10 |
*/ |
| 11 |
|
| 12 |
namespace Symfony\Component\HttpClient; |
| 13 |
|
| 14 |
use Symfony\Component\HttpClient\Response\MockResponse; |
| 15 |
use Symfony\Component\HttpClient\Response\ResponseStream; |
| 16 |
use Symfony\Component\HttpFoundation\Request; |
| 17 |
use Symfony\Component\HttpKernel\HttpCache\HttpCache; |
| 18 |
use Symfony\Component\HttpKernel\HttpCache\StoreInterface; |
| 19 |
use Symfony\Component\HttpKernel\HttpClientKernel; |
| 20 |
use Symfony\Contracts\HttpClient\HttpClientInterface; |
| 21 |
use Symfony\Contracts\HttpClient\ResponseInterface; |
| 22 |
use Symfony\Contracts\HttpClient\ResponseStreamInterface; |
| 23 |
use Symfony\Contracts\Service\ResetInterface; |
| 24 |
|
| 25 |
/** |
| 26 |
* Adds caching on top of an HTTP client. |
| 27 |
* |
| 28 |
* The implementation buffers responses in memory and doesn't stream directly from the network. |
| 29 |
* You can disable/enable this layer by setting option "no_cache" under "extra" to true/false. |
| 30 |
* By default, caching is enabled unless the "buffer" option is set to false. |
| 31 |
* |
| 32 |
* @author Nicolas Grekas <[email protected]> |
| 33 |
*/ |
| 34 |
class CachingHttpClient implements HttpClientInterface, ResetInterface |
| 35 |
{ |
| 36 |
use HttpClientTrait; |
| 37 |
|
| 38 |
private $client; |
| 39 |
private $cache; |
| 40 |
private $defaultOptions = self::OPTIONS_DEFAULTS; |
| 41 |
|
| 42 |
public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = []) |
| 43 |
{ |
| 44 |
if (!class_exists(HttpClientKernel::class)) { |
| 45 |
throw new \LogicException(sprintf('Using "%s" requires that the HttpKernel component version 4.3 or higher is installed, try running "composer require symfony/http-kernel:^5.4".', __CLASS__)); |
| 46 |
} |
| 47 |
|
| 48 |
$this->client = $client; |
| 49 |
$kernel = new HttpClientKernel($client); |
| 50 |
$this->cache = new HttpCache($kernel, $store, null, $defaultOptions); |
| 51 |
|
| 52 |
unset($defaultOptions['debug']); |
| 53 |
unset($defaultOptions['default_ttl']); |
| 54 |
unset($defaultOptions['private_headers']); |
| 55 |
unset($defaultOptions['allow_reload']); |
| 56 |
unset($defaultOptions['allow_revalidate']); |
| 57 |
unset($defaultOptions['stale_while_revalidate']); |
| 58 |
unset($defaultOptions['stale_if_error']); |
| 59 |
unset($defaultOptions['trace_level']); |
| 60 |
unset($defaultOptions['trace_header']); |
| 61 |
|
| 62 |
if ($defaultOptions) { |
| 63 |
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions); |
| 64 |
} |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* {@inheritdoc} |
| 69 |
*/ |
| 70 |
public function request(string $method, string $url, array $options = []): ResponseInterface |
| 71 |
{ |
| 72 |
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true); |
| 73 |
$url = implode('', $url); |
| 74 |
|
| 75 |
if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) { |
| 76 |
return $this->client->request($method, $url, $options); |
| 77 |
} |
| 78 |
|
| 79 |
$request = Request::create($url, $method); |
| 80 |
$request->attributes->set('http_client_options', $options); |
| 81 |
|
| 82 |
foreach ($options['normalized_headers'] as $name => $values) { |
| 83 |
if ('cookie' !== $name) { |
| 84 |
foreach ($values as $value) { |
| 85 |
$request->headers->set($name, substr($value, 2 + \strlen($name)), false); |
| 86 |
} |
| 87 |
|
| 88 |
continue; |
| 89 |
} |
| 90 |
|
| 91 |
foreach ($values as $cookies) { |
| 92 |
foreach (explode('; ', substr($cookies, \strlen('Cookie: '))) as $cookie) { |
| 93 |
if ('' !== $cookie) { |
| 94 |
$cookie = explode('=', $cookie, 2); |
| 95 |
$request->cookies->set($cookie[0], $cookie[1] ?? ''); |
| 96 |
} |
| 97 |
} |
| 98 |
} |
| 99 |
} |
| 100 |
|
| 101 |
$response = $this->cache->handle($request); |
| 102 |
$response = new MockResponse($response->getContent(), [ |
| 103 |
'http_code' => $response->getStatusCode(), |
| 104 |
'response_headers' => $response->headers->allPreserveCase(), |
| 105 |
]); |
| 106 |
|
| 107 |
return MockResponse::fromRequest($method, $url, $options, $response); |
| 108 |
} |
| 109 |
|
| 110 |
/** |
| 111 |
* {@inheritdoc} |
| 112 |
*/ |
| 113 |
public function stream($responses, float $timeout = null): ResponseStreamInterface |
| 114 |
{ |
| 115 |
if ($responses instanceof ResponseInterface) { |
| 116 |
$responses = [$responses]; |
| 117 |
} elseif (!is_iterable($responses)) { |
| 118 |
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of ResponseInterface objects, "%s" given.', __METHOD__, get_debug_type($responses))); |
| 119 |
} |
| 120 |
|
| 121 |
$mockResponses = []; |
| 122 |
$clientResponses = []; |
| 123 |
|
| 124 |
foreach ($responses as $response) { |
| 125 |
if ($response instanceof MockResponse) { |
| 126 |
$mockResponses[] = $response; |
| 127 |
} else { |
| 128 |
$clientResponses[] = $response; |
| 129 |
} |
| 130 |
} |
| 131 |
|
| 132 |
if (!$mockResponses) { |
| 133 |
return $this->client->stream($clientResponses, $timeout); |
| 134 |
} |
| 135 |
|
| 136 |
if (!$clientResponses) { |
| 137 |
return new ResponseStream(MockResponse::stream($mockResponses, $timeout)); |
| 138 |
} |
| 139 |
|
| 140 |
return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) { |
| 141 |
yield from MockResponse::stream($mockResponses, $timeout); |
| 142 |
yield $this->client->stream($clientResponses, $timeout); |
| 143 |
})()); |
| 144 |
} |
| 145 |
|
| 146 |
public function reset() |
| 147 |
{ |
| 148 |
if ($this->client instanceof ResetInterface) { |
| 149 |
$this->client->reset(); |
| 150 |
} |
| 151 |
} |
| 152 |
} |
| 153 |
|