Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a6dde3d

Browse filesBrowse files
[HttpClient] Add portable HTTP/2 implementation based on Amp's HTTP client
1 parent 392d0b0 commit a6dde3d
Copy full SHA for a6dde3d

File tree

6 files changed

+443
-0
lines changed
Filter options

6 files changed

+443
-0
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"symfony/yaml": "self.version"
100100
},
101101
"require-dev": {
102+
"amphp/http-client": "^4.0",
102103
"cache/integration-tests": "dev-master",
103104
"doctrine/annotations": "~1.0",
104105
"doctrine/cache": "~1.6",
+149Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Amp\Http\Client\Request;
15+
use Amp\Http\Client\HttpClientBuilder;
16+
use Psr\Log\LoggerAwareInterface;
17+
use Psr\Log\LoggerAwareTrait;
18+
use Symfony\Component\HttpClient\Exception\TransportException;
19+
use Symfony\Component\HttpClient\Internal\AmpClientState;
20+
use Symfony\Component\HttpClient\Response\AmpResponse;
21+
use Symfony\Component\HttpClient\Response\ResponseStream;
22+
use Symfony\Contracts\HttpClient\HttpClientInterface;
23+
use Symfony\Contracts\HttpClient\ResponseInterface;
24+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
25+
26+
if (!class_exists(HttpClientBuilder::class)) {
27+
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client".');
28+
}
29+
30+
/**
31+
* A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client.
32+
*
33+
* @author Nicolas Grekas <p@tchwork.com>
34+
*/
35+
final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface
36+
{
37+
use HttpClientTrait;
38+
use LoggerAwareTrait;
39+
40+
private $defaultOptions = self::OPTIONS_DEFAULTS;
41+
42+
/** @var AmpClientState */
43+
private $multi;
44+
45+
/**
46+
* @param array $defaultOptions Default requests' options
47+
* @param int $maxHostConnections The maximum number of connections to open
48+
*
49+
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
50+
*/
51+
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, HttpClientBuilder $builder = null)
52+
{
53+
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
54+
55+
if ($defaultOptions) {
56+
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
57+
}
58+
59+
$this->multi = new AmpClientState($builder, $maxHostConnections);
60+
}
61+
62+
/**
63+
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
64+
*
65+
* {@inheritdoc}
66+
*/
67+
public function request(string $method, string $url, array $options = []): ResponseInterface
68+
{
69+
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
70+
71+
// TODO: handle all options
72+
73+
// TODO: stream the body upload when possible
74+
$options['body'] = self::getBodyAsString($options['body']);
75+
76+
if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) {
77+
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
78+
}
79+
80+
$this->logger && $this->logger->info(sprintf('Request: %s %s', $method, implode('', $url)));
81+
82+
if (!isset($options['normalized_headers']['user-agent'])) {
83+
$options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
84+
}
85+
86+
if (0 < $options['max_duration']) {
87+
$options['timeout'] = min($options['max_duration'], $options['timeout']);
88+
}
89+
90+
$request = new Request(implode('', $url), $method);
91+
92+
if ($options['http_version']) {
93+
switch ((float) $options['http_version']) {
94+
case 1.0: $request->setProtocolVersions(['1.0']); break;
95+
case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
96+
default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
97+
}
98+
}
99+
100+
foreach ($options['headers'] as $v) {
101+
$h = explode(': ', $v, 2);
102+
$request->addHeader($h[0], $h[1]);
103+
}
104+
105+
$request->setTcpConnectTimeout(1000 * $options['timeout']);
106+
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
107+
$request->setTransferTimeout(1000 * $options['max_duration']);
108+
$request->setBody($options['body'] ?? '');
109+
110+
return new AmpResponse($this->multi, $request, $options, $this->logger);
111+
}
112+
113+
/**
114+
* {@inheritdoc}
115+
*/
116+
public function stream($responses, float $timeout = null): ResponseStreamInterface
117+
{
118+
if ($responses instanceof AmpResponse) {
119+
$responses = [$responses];
120+
} elseif (!is_iterable($responses)) {
121+
throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of AmpResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
122+
}
123+
124+
return new ResponseStream(AmpResponse::stream($responses, $timeout));
125+
}
126+
127+
private static function getBodyAsString($body): string
128+
{
129+
if (\is_resource($body)) {
130+
return stream_get_contents($body);
131+
}
132+
133+
if (!$body instanceof \Closure) {
134+
return $body;
135+
}
136+
137+
$result = '';
138+
139+
while ('' !== $data = $body(self::$CHUNK_SIZE)) {
140+
if (!\is_string($data)) {
141+
throw new TransportException(sprintf('Return value of the "body" option callback must be string, %s returned.', \gettype($data)));
142+
}
143+
144+
$result .= $data;
145+
}
146+
147+
return $result;
148+
}
149+
}

‎src/Symfony/Component/HttpClient/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.1.0
5+
-----
6+
7+
* added `AmpHttpClient`, a portable HTTP/2 implementation based on Amp
8+
49
4.4.0
510
-----
611

+64Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Internal;
13+
14+
use Amp\Http\Client\Connection\DefaultConnectionFactory;
15+
use Amp\Http\Client\Connection\LimitedConnectionPool;
16+
use Amp\Http\Client\Connection\UnlimitedConnectionPool;
17+
use Amp\Http\Client\HttpClientBuilder;
18+
use Amp\Socket\DnsConnector;
19+
use Amp\Socket\StaticConnector;
20+
use Amp\Sync\LocalKeyedSemaphore;
21+
22+
/**
23+
* Internal representation of the Amp client's state.
24+
*
25+
* @author Nicolas Grekas <p@tchwork.com>
26+
*
27+
* @internal
28+
*/
29+
final class AmpClientState extends ClientState
30+
{
31+
/** @var HttpClientBuilder[] */
32+
private $clients = [];
33+
private $maxHostConnections = 0;
34+
private $builder;
35+
36+
public function __construct(?HttpClientBuilder $builder, int $maxHostConnections)
37+
{
38+
$this->builder = $builder ?? (new HttpClientBuilder())->retry(0);
39+
$this->maxHostConnections = $maxHostConnections;
40+
}
41+
42+
public function getClient(string $bindTo)
43+
{
44+
if (isset($this->clients[$bindTo])) {
45+
return $this->clients[$bindTo];
46+
}
47+
48+
if ($bindTo) {
49+
$connector = (file_exists($bindTo) ? 'unix://' : 'tcp://').$bindTo;
50+
$connector = new StaticConnector($connector, new DnsConnector());
51+
$connector = new DefaultConnectionFactory($connector);
52+
} else {
53+
$connector = null;
54+
}
55+
56+
$pool = new UnlimitedConnectionPool($connector);
57+
58+
if (0 < $this->maxHostConnections) {
59+
$pool = LimitedConnectionPool::byHost($pool, new LocalKeyedSemaphore($this->maxHostConnections));
60+
}
61+
62+
return $this->clients[$bindTo] = $this->builder->usingPool($pool);
63+
}
64+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.