Skip to content

Commit dae5686

Browse files
fabpotnicolas-grekas
authored andcommitted
[HttpClient] added CachingHttpClient
1 parent 4574f85 commit dae5686

File tree

7 files changed

+161
-3
lines changed

7 files changed

+161
-3
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpClient\Response\MockResponse;
16+
use Symfony\Component\HttpClient\Response\ResponseStream;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
20+
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
21+
use Symfony\Component\HttpKernel\HttpClientKernel;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
use Symfony\Contracts\HttpClient\ResponseInterface;
24+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
25+
26+
/**
27+
* Adds caching on top of an HTTP client.
28+
*
29+
* The implementation buffers responses in memory and doesn't stream directly from the network.
30+
* You can disable/enable this layer by setting option "no_cache" under "extra" to true/false.
31+
* By default, caching is enabled unless the "buffer" option is set to false.
32+
*
33+
* @author Nicolas Grekas <[email protected]>
34+
*/
35+
class CachingHttpClient implements HttpClientInterface
36+
{
37+
use HttpClientTrait;
38+
39+
private $client;
40+
private $cache;
41+
private $defaultOptions = self::OPTIONS_DEFAULTS;
42+
43+
public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [], LoggerInterface $logger = null)
44+
{
45+
if (!class_exists(HttpClientKernel::class)) {
46+
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__));
47+
}
48+
49+
$this->client = $client;
50+
$kernel = new HttpClientKernel($client, $logger);
51+
$this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
52+
53+
unset($defaultOptions['debug']);
54+
unset($defaultOptions['default_ttl']);
55+
unset($defaultOptions['private_headers']);
56+
unset($defaultOptions['allow_reload']);
57+
unset($defaultOptions['allow_revalidate']);
58+
unset($defaultOptions['stale_while_revalidate']);
59+
unset($defaultOptions['stale_if_error']);
60+
61+
if ($defaultOptions) {
62+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
63+
}
64+
}
65+
66+
/**
67+
* {@inheritdoc}
68+
*/
69+
public function request(string $method, string $url, array $options = []): ResponseInterface
70+
{
71+
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
72+
$url = implode('', $url);
73+
$options['extra']['no_cache'] = $options['extra']['no_cache'] ?? !$options['buffer'];
74+
75+
if ($options['extra']['no_cache'] || !empty($options['body']) || !\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['headers'] as $name => $values) {
83+
if ('cookie' !== $name) {
84+
$request->headers->set($name, $values);
85+
continue;
86+
}
87+
88+
foreach ($values as $cookies) {
89+
foreach (explode('; ', $cookies) as $cookie) {
90+
if ('' !== $cookie) {
91+
$cookie = explode('=', $cookie, 2);
92+
$request->cookies->set($cookie[0], $cookie[1] ?? null);
93+
}
94+
}
95+
}
96+
}
97+
98+
$response = $this->cache->handle($request);
99+
$response = new MockResponse($response->getContent(), [
100+
'http_code' => $response->getStatusCode(),
101+
'raw_headers' => $response->headers->allPreserveCase(),
102+
]);
103+
104+
return MockResponse::fromRequest($method, $url, $options, $response);
105+
}
106+
107+
/**
108+
* {@inheritdoc}
109+
*/
110+
public function stream($responses, float $timeout = null): ResponseStreamInterface
111+
{
112+
if ($responses instanceof ResponseInterface) {
113+
$responses = [$responses];
114+
} elseif (!\is_iterable($responses)) {
115+
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)));
116+
}
117+
118+
$mockResponses = [];
119+
$clientResponses = [];
120+
121+
foreach ($responses as $response) {
122+
if ($response instanceof MockResponse) {
123+
$mockResponses[] = $response;
124+
} else {
125+
$clientResponses[] = $response;
126+
}
127+
}
128+
129+
if (!$mockResponses) {
130+
return $this->client->stream($clientResponses, $timeout);
131+
}
132+
133+
if (!$clientResponses) {
134+
return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
135+
}
136+
137+
return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
138+
yield from MockResponse::stream($mockResponses, $timeout);
139+
yield $this->client->stream($clientResponses, $timeout);
140+
})());
141+
}
142+
}

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
147147
$options[$k] = $options[$k] ?? $v;
148148
}
149149

150+
if (isset($defaultOptions['extra'])) {
151+
$options['extra'] += $defaultOptions['extra'];
152+
}
153+
150154
if ($defaultOptions['resolve'] ?? false) {
151155
$options['resolve'] += array_change_key_case($defaultOptions['resolve']);
152156
}

src/Symfony/Component/HttpClient/HttpOptions.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,14 @@ public function capturePeerCertChain(bool $capture)
309309

310310
return $this;
311311
}
312+
313+
/**
314+
* @return $this
315+
*/
316+
public function setExtra(string $name, $value)
317+
{
318+
$this->options['extra'][$name] = $value;
319+
320+
return $this;
321+
}
312322
}

src/Symfony/Component/HttpClient/Response/MockResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function __construct($body = '', array $info = [])
5656
}
5757
}
5858

59-
$info['raw_headers'] = $rawHeaders;
59+
$this->info['raw_headers'] = $rawHeaders;
6060
}
6161

6262
/**

src/Symfony/Component/HttpClient/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"require-dev": {
2727
"nyholm/psr7": "^1.0",
2828
"psr/http-client": "^1.0",
29-
"symfony/process": "~4.2"
29+
"symfony/http-kernel": "^4.3",
30+
"symfony/process": "^4.2"
3031
},
3132
"autoload": {
3233
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },

src/Symfony/Component/HttpKernel/RealHttpKernel.php renamed to src/Symfony/Component/HttpKernel/HttpClientKernel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @author Fabien Potencier <[email protected]>
2929
*/
30-
final class RealHttpKernel implements HttpKernelInterface
30+
final class HttpClientKernel implements HttpKernelInterface
3131
{
3232
private $client;
3333
private $logger;

src/Symfony/Contracts/HttpClient/HttpClientInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ interface HttpClientInterface
6464
'ciphers' => null,
6565
'peer_fingerprint' => null,
6666
'capture_peer_cert_chain' => false,
67+
'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options
6768
];
6869

6970
/**

0 commit comments

Comments
 (0)