Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 764043e

Browse files
[HttpClient] Add $response->toStream() to cast responses to regular PHP streams
1 parent 835f6b0 commit 764043e

File tree

8 files changed

+238
-6
lines changed

8 files changed

+238
-6
lines changed

src/Symfony/Component/HttpClient/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ CHANGELOG
66

77
* made `Psr18Client` implement relevant PSR-17 factories
88
* added `HttplugClient`
9+
* added `$response->toStream()` to cast responses to regular PHP streams
10+
* made `Psr18Client` and `HttplugClient` stream their response body
911

1012
4.3.0
1113
-----

src/Symfony/Component/HttpClient/Psr18Client.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public function sendRequest(RequestInterface $request): ResponseInterface
9292
}
9393
}
9494

95-
return $psrResponse->withBody($this->streamFactory->createStream($response->getContent(false)));
95+
return $psrResponse->withBody($this->streamFactory->createStreamFromResource($response->toStream()));
9696
} catch (TransportExceptionInterface $e) {
9797
if ($e instanceof \InvalidArgumentException) {
9898
throw new Psr18RequestException($e, $request);

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ final class CurlResponse implements ResponseInterface
2727
use ResponseTrait;
2828

2929
private static $performing = false;
30-
private $multi;
3130
private $debugBuffer;
3231

3332
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class MockResponse implements ResponseInterface
4444
*/
4545
public function __construct($body = '', array $info = [])
4646
{
47+
$this->multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
4748
$this->body = is_iterable($body) ? $body : (string) $body;
4849
$this->info = $info + $this->info;
4950

@@ -83,6 +84,7 @@ public function getInfo(string $type = null)
8384
*/
8485
protected function close(): void
8586
{
87+
unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]);
8688
$this->body = [];
8789
}
8890

@@ -133,10 +135,8 @@ protected static function schedule(self $response, array &$runningResponses): vo
133135
throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
134136
}
135137

136-
$multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
137-
138138
if (!isset($runningResponses[0])) {
139-
$runningResponses[0] = [$multi, []];
139+
$runningResponses[0] = [$response->multi, []];
140140
}
141141

142142
$runningResponses[0][1][$response->id] = $response;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ final class NativeResponse implements ResponseInterface
3333
private $remaining;
3434
private $buffer;
3535
private $inflate;
36-
private $multi;
3736
private $debugBuffer;
3837

3938
/**
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Response;
13+
14+
/**
15+
* @author Nicolas Grekas <[email protected]>
16+
*
17+
* @internal
18+
*/
19+
final class NativeStreamWrapper extends StreamWrapper
20+
{
21+
public function stream_cast(int $castAs)
22+
{
23+
if (STREAM_CAST_FOR_SELECT === $castAs) {
24+
return \Closure::bind(function () { return $this->handle ?? false; }, $this->response, $this->response)();
25+
}
26+
27+
return false;
28+
}
29+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ trait ResponseTrait
5757
private $finalInfo;
5858
private $offset = 0;
5959
private $jsonData;
60+
private $multi;
6061

6162
/**
6263
* {@inheritdoc}
@@ -178,6 +179,22 @@ public function cancel(): void
178179
$this->close();
179180
}
180181

182+
/**
183+
* Casts the response to a PHP stream resource.
184+
*
185+
* @return resource|null
186+
*/
187+
public function toStream()
188+
{
189+
stream_wrapper_register('symfony', $this instanceof NativeResponse ? NativeStreamWrapper::class : StreamWrapper::class, STREAM_IS_URL);
190+
191+
try {
192+
return fopen('symfony://'.$this->getInfo('url'), 'r', false, stream_context_create(['symfony' => ['response' => $this]])) ?: null;
193+
} finally {
194+
stream_wrapper_unregister('symfony');
195+
}
196+
}
197+
181198
/**
182199
* Closes the response and all its network handles.
183200
*/
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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\Response;
13+
14+
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
15+
use Symfony\Contracts\HttpClient\ResponseInterface;
16+
17+
/**
18+
* @author Nicolas Grekas <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
class StreamWrapper
23+
{
24+
/** @var resource */
25+
public $context;
26+
27+
/** @var ResponseInterface */
28+
protected $response;
29+
30+
private $content;
31+
private $offset = 0;
32+
33+
public function stream_open(string $path, string $mode, int $options): bool
34+
{
35+
if ('r' !== $mode) {
36+
if ($options & STREAM_REPORT_ERRORS) {
37+
trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), E_USER_WARNING);
38+
}
39+
40+
return false;
41+
}
42+
43+
$this->response = stream_context_get_options($this->context)['symfony']['response'] ?? null;
44+
$this->context = null;
45+
46+
if ($this->response instanceof NativeResponse || $this->response instanceof CurlResponse) {
47+
$this->content = \Closure::bind(function () { return $this->content; }, $this->response, $this->response)();
48+
49+
return true;
50+
}
51+
52+
if ($options & STREAM_REPORT_ERRORS) {
53+
trigger_error('Invalid or missing "symfony.response" context option.', E_USER_WARNING);
54+
}
55+
56+
return false;
57+
}
58+
59+
public function stream_read(int $count)
60+
{
61+
$responseClass = \get_class($this->response);
62+
63+
if (null !== $this->content) {
64+
// Empty activity list
65+
foreach ($responseClass::stream([$this->response], 0) as $chunk) {
66+
$chunk->isTimeout();
67+
}
68+
69+
fseek($this->content, $this->offset);
70+
71+
if ('' !== $data = fread($this->content, $count)) {
72+
fseek($this->content, 0, SEEK_END);
73+
$this->offset += \strlen($data);
74+
75+
return $data;
76+
}
77+
}
78+
79+
foreach ($responseClass::stream([$this->response]) as $chunk) {
80+
try {
81+
if ('' !== $data = $chunk->getContent()) {
82+
$this->offset += \strlen($data);
83+
84+
return $data;
85+
}
86+
} catch (ExceptionInterface $e) {
87+
trigger_error($e->getMessage(), E_USER_WARNING);
88+
89+
return false;
90+
}
91+
}
92+
93+
return '';
94+
}
95+
96+
public function stream_tell(): int
97+
{
98+
return $this->offset;
99+
}
100+
101+
public function stream_eof(): bool
102+
{
103+
if (null !== $this->content) {
104+
fseek($this->content, $this->offset);
105+
$eof = '' === fread($this->content, 1);
106+
fseek($this->content, 0, SEEK_END);
107+
108+
if (!$eof) {
109+
return false;
110+
}
111+
}
112+
113+
$isClosed = function () {
114+
return !isset($this->multi->openHandles[$this->id]) && !isset($this->multi->handlesActivity[$this->id]) && !isset($this->buffer);
115+
};
116+
117+
return \Closure::bind($isClosed, $this->response, $this->response)();
118+
}
119+
120+
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
121+
{
122+
if (null === $this->content) {
123+
return false;
124+
}
125+
126+
if (SEEK_CUR) {
127+
$offset += $this->offset;
128+
}
129+
130+
fseek($this->content, 0, SEEK_END);
131+
$size = ftell($this->content);
132+
133+
if (SEEK_END === $whence || $size < $offset) {
134+
$responseClass = \get_class($this->response);
135+
foreach ($responseClass::stream([$this->response]) as $chunk) {
136+
try {
137+
// Chunks are buffered in $this->content already
138+
$chunk->getContent();
139+
} catch (ExceptionInterface $e) {
140+
trigger_error($e->getMessage(), E_USER_WARNING);
141+
142+
return false;
143+
}
144+
}
145+
146+
fseek($this->content, 0, SEEK_END);
147+
$size = ftell($this->content);
148+
149+
if (SEEK_END === $whence) {
150+
$offset += $size;
151+
}
152+
}
153+
154+
if (0 <= $offset && $offset <= $size) {
155+
$this->offset = $offset;
156+
157+
return true;
158+
}
159+
160+
return false;
161+
}
162+
163+
public function stream_cast(int $castAs)
164+
{
165+
return false;
166+
}
167+
168+
public function stream_stat(): array
169+
{
170+
return [
171+
'dev' => 0,
172+
'ino' => 0,
173+
'mode' => 33060,
174+
'nlink' => 0,
175+
'uid' => 0,
176+
'gid' => 0,
177+
'rdev' => 0,
178+
'size' => (int) ($this->response->getHeaders(false)['content-length'][0] ?? 0),
179+
'atime' => 0,
180+
'mtime' => strtotime($this->response->getHeaders(false)['last-modified'][0] ?? '') ?: 0,
181+
'ctime' => 0,
182+
'blksize' => 0,
183+
'blocks' => 0,
184+
];
185+
}
186+
}

0 commit comments

Comments
 (0)