Skip to content

Commit 5b70e5f

Browse files
[HttpClient] add AsyncDecoratorTrait to ease processing responses without breaking async
1 parent 2ed6a0d commit 5b70e5f

12 files changed

+920
-178
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 Symfony\Component\HttpClient\Response\AsyncResponse;
15+
use Symfony\Component\HttpClient\Response\ResponseStream;
16+
use Symfony\Contracts\HttpClient\HttpClientInterface;
17+
use Symfony\Contracts\HttpClient\ResponseInterface;
18+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
19+
20+
/**
21+
* Eases with processing responses while streaming them.
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
trait AsyncDecoratorTrait
26+
{
27+
private $client;
28+
29+
public function __construct(HttpClientInterface $client = null)
30+
{
31+
$this->client = $client ?? HttpClient::create();
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*
37+
* @return AsyncResponse
38+
*/
39+
abstract public function request(string $method, string $url, array $options = []): ResponseInterface;
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function stream($responses, float $timeout = null): ResponseStreamInterface
45+
{
46+
if ($responses instanceof AsyncResponse) {
47+
$responses = [$responses];
48+
} elseif (!is_iterable($responses)) {
49+
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
50+
}
51+
52+
return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::class));
53+
}
54+
}

src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,12 @@ public function getError(): ?string
110110
/**
111111
* @return bool Whether the wrapped error has been thrown or not
112112
*/
113-
public function didThrow(): bool
113+
public function didThrow(bool $didThrow = null): bool
114114
{
115+
if (null !== $didThrow && $this->didThrow !== $didThrow) {
116+
return !$this->didThrow = $didThrow;
117+
}
118+
115119
return $this->didThrow;
116120
}
117121

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
*/
3535
final class AmpResponse implements ResponseInterface
3636
{
37+
use CommonResponseTrait;
3738
use ResponseTrait;
3839

3940
private $multi;
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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\Response;
13+
14+
use Symfony\Component\HttpClient\Chunk\DataChunk;
15+
use Symfony\Component\HttpClient\Chunk\LastChunk;
16+
use Symfony\Contracts\HttpClient\ChunkInterface;
17+
use Symfony\Contracts\HttpClient\HttpClientInterface;
18+
use Symfony\Contracts\HttpClient\ResponseInterface;
19+
20+
/**
21+
* A DTO to work with AsyncResponse.
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
final class AsyncContext
26+
{
27+
private $passthru;
28+
private $client;
29+
private $response;
30+
private $info = [];
31+
private $content;
32+
private $offset;
33+
34+
public function __construct(&$passthru, HttpClientInterface $client, ResponseInterface &$response, array &$info, $content, int $offset)
35+
{
36+
$this->passthru = &$passthru;
37+
$this->client = $client;
38+
$this->response = &$response;
39+
$this->info = &$info;
40+
$this->content = $content;
41+
$this->offset = $offset;
42+
}
43+
44+
/**
45+
* Returns the HTTP status without consuming the response.
46+
*/
47+
public function getStatusCode(): int
48+
{
49+
return $this->response->getInfo('http_code');
50+
}
51+
52+
/**
53+
* Returns the headers without consuming the response.
54+
*/
55+
public function getHeaders(): array
56+
{
57+
$headers = [];
58+
59+
foreach ($this->response->getInfo('response_headers') as $h) {
60+
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([123456789]\d\d)(?: |$)#', $h, $m)) {
61+
$headers = [];
62+
} elseif (2 === \count($m = explode(':', $h, 2))) {
63+
$headers[strtolower($m[0])][] = ltrim($m[1]);
64+
}
65+
}
66+
67+
return $headers;
68+
}
69+
70+
/**
71+
* @return resource|null The PHP stream resource where the content is buffered, if it is
72+
*/
73+
public function getContent()
74+
{
75+
return $this->content;
76+
}
77+
78+
/**
79+
* Creates a new chunk of content.
80+
*/
81+
public function createChunk(string $data): ChunkInterface
82+
{
83+
return new DataChunk($this->offset, $data);
84+
}
85+
86+
/**
87+
* Cancels the request and returns the last chunk to yield.
88+
*/
89+
public function cancel(): ChunkInterface
90+
{
91+
$this->info['canceled'] = true;
92+
$this->info['error'] = 'Response has been canceled.';
93+
$this->response->cancel();
94+
95+
return new LastChunk();
96+
}
97+
98+
/**
99+
* Returns the current info of the response.
100+
*/
101+
public function getInfo(string $type = null)
102+
{
103+
if (null !== $type) {
104+
return $this->info[$type] ?? $this->response->getInfo($type);
105+
}
106+
107+
return $this->info + $this->response->getInfo();
108+
}
109+
110+
/**
111+
* Attaches an info to the response.
112+
*/
113+
public function setInfo(string $type, $value): self
114+
{
115+
if ('canceled' === $type && $value !== $this->info['canceled']) {
116+
throw new \LogicException('You cannot set the "canceled" info directly.');
117+
}
118+
119+
if (null === $value) {
120+
unset($this->info[$type]);
121+
} else {
122+
$this->info[$type] = $value;
123+
}
124+
125+
return $this;
126+
}
127+
128+
/**
129+
* Returns the currently processed response.
130+
*/
131+
public function getResponse(): ResponseInterface
132+
{
133+
return $this->response;
134+
}
135+
136+
/**
137+
* Replaces the currently processed response by doing a new request.
138+
*/
139+
public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface
140+
{
141+
$this->info['previous_info'][] = $this->response->getInfo();
142+
143+
return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options);
144+
}
145+
146+
/**
147+
* Replaces the currently processed response by another one.
148+
*/
149+
public function replaceResponse(ResponseInterface $response): ResponseInterface
150+
{
151+
$this->info['previous_info'][] = $this->response->getInfo();
152+
153+
return $this->response = $response;
154+
}
155+
156+
/**
157+
* Replaces or removes the chunk filter iterator.
158+
*/
159+
public function passthru(callable $passthru = null): void
160+
{
161+
$this->passthru = $passthru;
162+
}
163+
}

0 commit comments

Comments
 (0)