Skip to content

Commit 9981b83

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

File tree

3 files changed

+141
-2
lines changed

3 files changed

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

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)