diff --git a/src/Symfony/Component/HttpClient/CachingHttpClient.php b/src/Symfony/Component/HttpClient/CachingHttpClient.php new file mode 100644 index 0000000000..30f370078f --- /dev/null +++ b/src/Symfony/Component/HttpClient/CachingHttpClient.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient; + +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpClient\Response\ResponseStream; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpCache\HttpCache; +use Symfony\Component\HttpKernel\HttpCache\StoreInterface; +use Symfony\Component\HttpKernel\HttpClientKernel; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; + +/** + * Adds caching on top of an HTTP client. + * + * The implementation buffers responses in memory and doesn't stream directly from the network. + * You can disable/enable this layer by setting option "no_cache" under "extra" to true/false. + * By default, caching is enabled unless the "buffer" option is set to false. + * + * @author Nicolas Grekas
+ */
+class CachingHttpClient implements HttpClientInterface
+{
+ use HttpClientTrait;
+
+ private $client;
+ private $cache;
+ private $defaultOptions = self::OPTIONS_DEFAULTS;
+
+ public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [], LoggerInterface $logger = null)
+ {
+ if (!class_exists(HttpClientKernel::class)) {
+ 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:^4.3".', __CLASS__));
+ }
+
+ $this->client = $client;
+ $kernel = new HttpClientKernel($client, $logger);
+ $this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
+
+ unset($defaultOptions['debug']);
+ unset($defaultOptions['default_ttl']);
+ unset($defaultOptions['private_headers']);
+ unset($defaultOptions['allow_reload']);
+ unset($defaultOptions['allow_revalidate']);
+ unset($defaultOptions['stale_while_revalidate']);
+ unset($defaultOptions['stale_if_error']);
+
+ if ($defaultOptions) {
+ [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
+ $url = implode('', $url);
+ $options['extra']['no_cache'] = $options['extra']['no_cache'] ?? !$options['buffer'];
+
+ if ($options['extra']['no_cache'] || !empty($options['body']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
+ return $this->client->request($method, $url, $options);
+ }
+
+ $request = Request::create($url, $method);
+ $request->attributes->set('http_client_options', $options);
+
+ foreach ($options['headers'] as $name => $values) {
+ if ('cookie' !== $name) {
+ $request->headers->set($name, $values);
+ continue;
+ }
+
+ foreach ($values as $cookies) {
+ foreach (explode('; ', $cookies) as $cookie) {
+ if ('' !== $cookie) {
+ $cookie = explode('=', $cookie, 2);
+ $request->cookies->set($cookie[0], $cookie[1] ?? null);
+ }
+ }
+ }
+ }
+
+ $response = $this->cache->handle($request);
+ $response = new MockResponse($response->getContent(), [
+ 'http_code' => $response->getStatusCode(),
+ 'raw_headers' => $response->headers->allPreserveCase(),
+ ]);
+
+ return MockResponse::fromRequest($method, $url, $options, $response);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof ResponseInterface) {
+ $responses = [$responses];
+ } elseif (!\is_iterable($responses)) {
+ throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of ResponseInterface objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
+ }
+
+ $mockResponses = [];
+ $clientResponses = [];
+
+ foreach ($responses as $response) {
+ if ($response instanceof MockResponse) {
+ $mockResponses[] = $response;
+ } else {
+ $clientResponses[] = $response;
+ }
+ }
+
+ if (!$mockResponses) {
+ return $this->client->stream($clientResponses, $timeout);
+ }
+
+ if (!$clientResponses) {
+ return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
+ }
+
+ return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
+ yield from MockResponse::stream($mockResponses, $timeout);
+ yield $this->client->stream($clientResponses, $timeout);
+ })());
+ }
+}
diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php
index 04178ae656..9a45d420b9 100644
--- a/src/Symfony/Component/HttpClient/HttpClientTrait.php
+++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php
@@ -147,6 +147,10 @@ trait HttpClientTrait
$options[$k] = $options[$k] ?? $v;
}
+ if (isset($defaultOptions['extra'])) {
+ $options['extra'] += $defaultOptions['extra'];
+ }
+
if ($defaultOptions['resolve'] ?? false) {
$options['resolve'] += array_change_key_case($defaultOptions['resolve']);
}
diff --git a/src/Symfony/Component/HttpClient/HttpOptions.php b/src/Symfony/Component/HttpClient/HttpOptions.php
index 2a29fc66fb..85b55f0d08 100644
--- a/src/Symfony/Component/HttpClient/HttpOptions.php
+++ b/src/Symfony/Component/HttpClient/HttpOptions.php
@@ -309,4 +309,14 @@ class HttpOptions
return $this;
}
+
+ /**
+ * @return $this
+ */
+ public function setExtra(string $name, $value)
+ {
+ $this->options['extra'][$name] = $value;
+
+ return $this;
+ }
}
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index 979385626d..803b424d3d 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -26,7 +26,8 @@
"require-dev": {
"nyholm/psr7": "^1.0",
"psr/http-client": "^1.0",
- "symfony/process": "~4.2"
+ "symfony/http-kernel": "^4.3",
+ "symfony/process": "^4.2"
},
"autoload": {
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
diff --git a/src/Symfony/Component/HttpKernel/RealHttpKernel.php b/src/Symfony/Component/HttpKernel/HttpClientKernel.php
similarity index 98%
rename from src/Symfony/Component/HttpKernel/RealHttpKernel.php
rename to src/Symfony/Component/HttpKernel/HttpClientKernel.php
index d78fda1f8d..29a6a97cef 100644
--- a/src/Symfony/Component/HttpKernel/RealHttpKernel.php
+++ b/src/Symfony/Component/HttpKernel/HttpClientKernel.php
@@ -27,7 +27,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*
* @author Fabien Potencier