Skip to content

Commit f6e4411

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

File tree

4 files changed

+143
-3
lines changed

4 files changed

+143
-3
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
* @author Nicolas Grekas <[email protected]>
30+
*/
31+
class CachingHttpClient implements HttpClientInterface
32+
{
33+
use HttpClientTrait;
34+
35+
private $client;
36+
private $cache;
37+
private $defaultOptions = self::OPTIONS_DEFAULTS + [
38+
'no_cache' => false, // Set to true to bypass the cache
39+
];
40+
41+
public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [], LoggerInterface $logger = null)
42+
{
43+
if (!class_exists(HttpClientKernel::class)) {
44+
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__));
45+
}
46+
47+
$this->client = $client;
48+
$kernel = new HttpClientKernel($client, $logger);
49+
$this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
50+
51+
unset($defaultOptions['debug']);
52+
unset($defaultOptions['default_ttl']);
53+
unset($defaultOptions['private_headers']);
54+
unset($defaultOptions['allow_reload']);
55+
unset($defaultOptions['allow_revalidate']);
56+
unset($defaultOptions['stale_while_revalidate']);
57+
unset($defaultOptions['stale_if_error']);
58+
59+
if ($defaultOptions) {
60+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
61+
}
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function request(string $method, string $url, array $options = []): ResponseInterface
68+
{
69+
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
70+
$url = implode('', $url);
71+
72+
if ($options['no_cache'] || !empty($options['body']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
73+
return $this->client->request($method, $url, $options);
74+
}
75+
76+
$request = Request::create($url, $method);
77+
$request->attributes->set('http_client_options', $options);
78+
79+
foreach ($options['headers'] as $name => $values) {
80+
if ('cookie' !== $name) {
81+
$request->headers->set($name, $values);
82+
continue;
83+
}
84+
85+
foreach ($values as $cookies) {
86+
foreach (explode('; ', $cookies) as $cookie) {
87+
if ('' !== $cookie) {
88+
$cookie = explode('=', $cookie, 2);
89+
$request->cookies->set($cookie[0], $cookie[1] ?? null);
90+
}
91+
}
92+
}
93+
}
94+
95+
$response = $this->cache->handle($request);
96+
$response = new MockResponse($response->getContent(), [
97+
'http_code' => $response->getStatusCode(),
98+
'raw_headers' => $response->headers->allPreserveCase(),
99+
]);
100+
101+
return MockResponse::fromRequest($method, $url, $options, $response);
102+
}
103+
104+
/**
105+
* {@inheritdoc}
106+
*/
107+
public function stream($responses, float $timeout = null): ResponseStreamInterface
108+
{
109+
if ($responses instanceof ResponseInterface) {
110+
$responses = [$responses];
111+
} elseif (!\is_iterable($responses)) {
112+
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)));
113+
}
114+
115+
$mockResponses = [];
116+
$clientResponses = [];
117+
118+
foreach ($responses as $response) {
119+
if ($response instanceof MockResponse) {
120+
$mockResponses[] = $response;
121+
} else {
122+
$clientResponses[] = $response;
123+
}
124+
}
125+
126+
if (!$mockResponses) {
127+
return $this->client->stream($clientResponses, $timeout);
128+
}
129+
130+
if (!$clientResponses) {
131+
return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
132+
}
133+
134+
return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
135+
yield from MockResponse::stream($mockResponses, $timeout);
136+
yield $this->client->stream($clientResponses, $timeout);
137+
})());
138+
}
139+
}

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;

0 commit comments

Comments
 (0)