8000 [HttpClient] Add support for amphp/http-client v5 · symfony/symfony@a5fb61c · GitHub
[go: up one dir, main page]

Skip to content

Commit a5fb61c

Browse files
[HttpClient] Add support for amphp/http-client v5
1 parent aaa2b8c commit a5fb61c

18 files changed

+1120
-42
lines changed

.github/patch-types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/'):
4747
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberIntersectionWithTrait.php'):
4848
case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'):
49+
case false !== strpos($file, '/src/Symfony/Component/HttpClient/Internal/'):
4950
case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Answer.php'):
5051
case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Number.php'):
5152
case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Suit.php'):

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,8 @@
122122
"symfony/yaml": "self.version"
123123
},
124124
"require-dev": {
125-
"amphp/amp": "^2.5",
126-
"amphp/http-client": "^4.2.1",
127-
"amphp/http-tunnel": "^1.0",
125+
"amphp/http-client": "^4.2.1|^5.0",
126+
"amphp/http-tunnel": "^1.0|^2.0",
128127
"async-aws/ses": "^1.0",
129128
"async-aws/sqs": "^1.0|^2.0",
130129
"async-aws/sns": "^1.0",
@@ -151,6 +150,7 @@
151150
"psr/http-client": "^1.0",
152151
"psr/simple-cache": "^1.0|^2.0|^3.0",
153152
"seld/jsonlint": "^1.10",
153+
"symfony/amphp-http-client-meta": "^1.0|^2.0",
154154
"symfony/mercure-bundle": "^0.3",
155155
"symfony/phpunit-bridge": "^6.4|^7.0",
156156
"symfony/runtime": "self.version",
@@ -162,6 +162,7 @@
162162
},
163163
"conflict": {
164164
"ext-psr": "<1.1|>=2",
165+
"amphp/amp": "<2.5",
165166
"async-aws/core": "<1.5",
166167
"doctrine/collections": "<1.8",
167168
"doctrine/dbal": "<3.6",

src/Symfony/Component/HttpClient/AmpHttpClient.php

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@
1212
namespace Symfony\Component\HttpClient;
1313

1414
use Amp\CancelledException;
15+
use Amp\DeferredFuture;
1516
use Amp\Http\Client\DelegateHttpClient;
1617
use Amp\Http\Client\InterceptedHttpClient;
1718
use Amp\Http\Client\PooledHttpClient;
1819
use Amp\Http\Client\Request;
20+
use Amp\Http\HttpMessage;
1921
use Amp\Http\Tunnel\Http1TunnelConnector;
20-
use Amp\Promise;
2122
use Psr\Log\LoggerAwareInterface;
2223
use Psr\Log\LoggerAwareTrait;
2324
use Symfony\Component\HttpClient\Exception\TransportException;
24-
use Symfony\Component\HttpClient\Internal\AmpClientState;
25-
use Symfony\Component\HttpClient\Response\AmpResponse;
25+
use Symfony\Component\HttpClient\Internal\AmpClientStateV4;
26+
use Symfony\Component\HttpClient\Internal\AmpClientStateV5;
27+
use Symfony\Component\HttpClient\Response\AmpResponseV4;
28+
use Symfony\Component\HttpClient\Response\AmpResponseV5;
2629
use Symfony\Component\HttpClient\Response\ResponseStream;
2730
use Symfony\Contracts\HttpClient\HttpClientInterface;
2831
use Symfony\Contracts\HttpClient\ResponseInterface;
@@ -33,8 +36,8 @@
3336
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:^4.2.1".');
3437
}
3538

36-
if (!interface_exists(Promise::class)) {
37-
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the installed "amphp/http-client" is not compatible with this version of "symfony/http-client". Try downgrading "amphp/http-client" to "^4.2.1".');
39+
if (\PHP_VERSION_ID < 80400 && is_subclass_of(Request::class, HttpMessage::class)) {
40+
throw new \LogicException('Using "Symfony\Component\HttpClient\AmpHttpClient" with amphp/http-client >= 5 requires PHP >= 8.4. Try running "composer require amphp/http-client:^4.2.1" or upgrade to PHP >= 8.4.');
3841
}
3942

4043
/**
@@ -53,7 +56,7 @@ final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface,
5356

5457
private array $defaultOptions = self::OPTIONS_DEFAULTS;
5558
private static array $emptyDefaults = self::OPTIONS_DEFAULTS;
56-
private AmpClientState $multi;
59+
private AmpClientStateV4|AmpClientStateV5 $multi;
5760

5861
/**
5962
* @param array $defaultOptions Default requests' options
@@ -72,7 +75,11 @@ public function __construct(array $defaultOptions = [], ?callable $clientConfigu
7275
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
7376
}
7477

75-
$this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
78+
if (is_subclass_of(Request::class, HttpMessage::class)) {
79+
$this->multi = new AmpClientStateV5($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
80+
} else {
81+
$this->multi = new AmpClientStateV4($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
82+
}
7683
}
7784

7885
/**
@@ -132,9 +139,10 @@ public function request(string $method, string $url, array $options = []): Respo
132139
$request->addHeader($h[0], $h[1]);
133140
}
134141

135-
$request->setTcpConnectTimeout(1000 * $options['timeout']);
136-
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
137-
$request->setTransferTimeout(1000 * $options['max_duration']);
142+
$coef = $request instanceof HttpMessage ? 1 : 1000;
143+
$request->setTcpConnectTimeout($coef * $options['timeout']);
144+
$request->setTlsHandshakeTimeout($coef * $options['timeout']);
145+
$request->setTransferTimeout($coef * $options['max_duration']);
138146
if (method_exists($request, 'setInactivityTimeout')) {
139147
$request->setInactivityTimeout(0);
140148
}
@@ -145,25 +153,37 @@ public function request(string $method, string $url, array $options = []): Respo
145153
$request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
146154
}
147155

148-
return new AmpResponse($this->multi, $request, $options, $this->logger);
156+
if ($request instanceof HttpMessage) {
157+
return new AmpResponseV5($this->multi, $request, $options, $this->logger);
158+
}
159+
160+
return new AmpResponseV4($this->multi, $request, $options, $this->logger);
149161
}
150162

151163
public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface
152164
{
153-
if ($responses instanceof AmpResponse) {
165+
if ($responses instanceof AmpResponseV4 || $responses instanceof AmpResponseV5) {
154166
$responses = [$responses];
155167
}
156168

157-
return new ResponseStream(AmpResponse::stream($responses, $timeout));
169+
if ($this->multi instanceof AmpClientStateV5) {
170+
return new ResponseStream(AmpResponseV5::stream($responses, $timeout));
171+
}
172+
173+
return new ResponseStream(AmpResponseV4::stream($responses, $timeout));
158174
}
159175

160176
public function reset(): void
161177
{
162178
$this->multi->dnsCache = [];
163179

164-
foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
180+
foreach ($this->multi->pushedResponses as $pushedResponses) {
165181
foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
166-
$pushDeferred->fail(new CancelledException());
182+
if ($pushDeferred instanceof DeferredFuture) {
183+
$pushDeferred->error(new CancelledException());
184+
} else {
185+
$pushDeferred->fail(new CancelledException());
186+
}
167187

168188
$this->logger?->debug(\sprintf('Unused pushed response: "%s"', $pushedUrl));
169189
}

src/Symfony/Component/HttpClient/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add support for amphp/http-client v5 on PHP 8.4+
8+
49
7.1
510
---
611

src/Symfony/Component/HttpClient/HttpClient.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
namespace Symfony\Component\HttpClient;
1313

14-
use Amp\Http\Client\Connection\ConnectionLimitingPool;
15-
use Amp\Promise;
14+
use Amp\Http\Client\Request as AmpRequest;
15+
use Amp\Http\HttpMessage;
1616
use Symfony\Contracts\HttpClient\HttpClientInterface;
1717

1818
/**
@@ -31,7 +31,7 @@ final class HttpClient
3131
*/
3232
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
3333
{
34-
if ($amp = class_exists(ConnectionLimitingPool::class) && interface_exists(Promise::class)) {
34+
if ($amp = class_exists(AmpRequest::class) && (\PHP_VERSION_ID >= 80400 || is_subclass_of(AmpRequest::class, HttpMessage::class))) {
3535
if (!\extension_loaded('curl')) {
3636
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
3737
}

src/Symfony/Component/HttpClient/Internal/AmpBody.php renamed to src/Symfony/Component/HttpClient/Internal/AmpBodyV4.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*
2424
* @internal
2525
*/
26-
class AmpBody implements RequestBody, InputStream
26+
class AmpBodyV4 implements RequestBody, InputStream
2727
{
2828
private ResourceInputStream|\Closure|string $body;
2929
private array $info;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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\ByteStream\ReadableBuffer;
15+
use Amp\ByteStream\ReadableIterableStream;
16+
use Amp\ByteStream\ReadableResourceStream;
17+
use Amp\ByteStream\ReadableStream;
18+
use Amp\Cancellation;
19+
use Amp\Http\Client\HttpContent;
20+
use Symfony\Component\HttpClient\Exception\TransportException;
21+
22+
/**
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*
25+
* @internal
26+
*/
27+
class AmpBodyV5 implements HttpContent, ReadableStream, \IteratorAggregate
28+
{
29+
private ReadableStream $body;
30+
private ?string $content;
31+
private array $info;
32+
private ?int $offset = 0;
33+
private int $length = -1;
34+
private ?int $uploaded = null;
35+
36+
/**
37+
* @param \Closure|resource|string $body
38+
*/
39+
public function __construct(
40+
$body,
41+
&$info,
42+
private \Closure $onProgress,
43+
) {
44+
$this->info = &$info;
45+
46+
if (\is_resource($body)) {
47+
$this->offset = ftell($body);
48+
$this->length = fstat($body)['size'];
49+
$this->body = new ReadableResourceStream($body);
50+
} elseif (\is_string($body)) {
51+
$this->length = \strlen($body);
52+
$this->body = new ReadableBuffer($body);
53+
$this->content = $body;
54+
} else {
55+
$this->body = new ReadableIterableStream((static function () use ($body) {
56+
while ('' !== $data = ($body)(16372)) {
57+
if (!\is_string($data)) {
58+
throw new TransportException(\sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
59+
}
60+
61+
yield $data;
62+
}
63+
})());
64+
}
65+
}
66+
67+
public function getContent(): ReadableStream
68+
{
69+
if (null !== $this->uploaded) {
70+
$this->uploaded = null;
71+
72+
if (\is_string($this->body)) {
73+
$this->offset = 0;
74+
} elseif ($this->body instanceof ReadableResourceStream) {
75+
fseek($this->body->getResource(), $this->offset);
76+
}
77+
}
78+
79+
return $this;
80+
}
81+
82+
public function getContentType(): ?string
83+
{
84+
return null;
85+
}
86+
87+
public function getContentLength(): ?int
88+
{
89+
return 0 <= $this->length ? $this->length - $this->offset : null;
90+
}
91+
92+
public function read(?Cancellation $cancellation = null): ?string
93+
{
94+
$this->info['size_upload'] += $this->uploaded;
95+
$this->uploaded = 0;
96+
($this->onProgress)();
97+
98+
if (null !== $data = $this->body->read($cancellation)) {
99+
$this->uploaded = \strlen($data);
100+
} else {
101+
$this->info['upload_content_length'] = $this->info['size_upload'];
102+
}
103+
104+
return $data;
105+
}
106+
107+
public function isReadable(): bool
108+
{
109+
return $this->body->isReadable();
110+
}
111+
112+
public function close(): void
113+
{
114+
$this->body->close();
115+
}
116+
117+
public function isClosed(): bool
118+
{
119+
return $this->body->isClosed();
120+
}
121+
122+
public function onClose(\Closure $onClose): void
123+
{
124+
$this->body->onClose($onClose);
125+
}
126+
127+
public function getIterator(): \Traversable
128+
{
129+
return $this->body;
130+
}
131+
132+
public static function rewind(HttpContent $body): HttpContent
133+
{
134+
if (!$body instanceof self) {
135+
return $body;
136+
}
137+
138+
$body->uploaded = null;
139+
140+
if ($body->body instanceof ReadableResourceStream && !$body->body->isClosed()) {
141+
fseek($body->body->getResource(), $body->offset);
142+
}
143+
144+
if ($body->body instanceof ReadableBuffer) {
145+
return new $body($body->content, $body->info, $body->onProgress);
146+
}
147+
148+
return $body;
149+
}
150+
}

src/Symfony/Component/HttpClient/Internal/AmpClientState.php renamed to src/Symfony/Component/HttpClient/Internal/AmpClientStateV4.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
*
4040
* @internal
4141
*/
42-
final class AmpClientState extends ClientState
42+
final class AmpClientStateV4 extends ClientState
4343
{
4444
public array $dnsCache = [];
4545
public int $responseCount = 0;
@@ -90,7 +90,7 @@ public function request(array $options, Request $request, CancellationToken $can
9090
$info['peer_certificate_chain'] = [];
9191
}
9292

93-
$request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
93+
$request->addEventListener(new AmpListenerV4($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
9494
$request->setPushHandler(fn ($request, $response): Promise => $this->handlePush($request, $response, $options));
9595

9696
($request->hasHeader('content-length') ? new Success((int) $request->getHeader('content-length')) : $request->getBody()->getBodyLength())
@@ -157,7 +157,7 @@ public function connect(string $uri, ?ConnectContext $context = null, ?Cancellat
157157
return $result;
158158
}
159159
};
160-
$connector->connector = new DnsConnector(new AmpResolver($this->dnsCache));
160+
$connector->connector = new DnsConnector(new AmpResolverV4($this->dnsCache));
161161

162162
$context = (new ConnectContext())
163163
->withTcpNoDelay()

0 commit comments

Comments
 (0)
0