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

Skip to content

Commit a59e0af

Browse files
[HttpClient] Add $response->toStream() to cast responses to regular PHP streams
1 parent b9b03fe commit a59e0af

File tree

8 files changed

+295
-12
lines changed

8 files changed

+295
-12
lines changed

src/Symfony/Component/HttpClient/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ CHANGELOG
44
4.4.0
55
-----
66

7-
* made `Psr18Client` implement relevant PSR-17 factories
7+
* added `StreamWrapper`
88
* added `HttplugClient`
99
* added support for NTLM authentication
10+
* added `$response->toStream()` to cast responses to regular PHP streams
11+
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
1012

1113
4.3.0
1214
-----

src/Symfony/Component/HttpClient/Psr18Client.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
use Psr\Http\Message\StreamInterface;
2626
use Psr\Http\Message\UriFactoryInterface;
2727
use Psr\Http\Message\UriInterface;
28+
use Symfony\Component\HttpClient\Response\ResponseTrait;
29+
use Symfony\Component\HttpClient\Response\StreamWrapper;
2830
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
2931
use Symfony\Contracts\HttpClient\HttpClientInterface;
3032

@@ -90,7 +92,9 @@ public function sendRequest(RequestInterface $request): ResponseInterface
9092
}
9193
}
9294

93-
return $psrResponse->withBody($this->streamFactory->createStream($response->getContent(false)));
95+
$body = isset(class_uses($response)[ResponseTrait::class]) ? $response->toStream() : StreamWrapper::createResource($response, $this->client);
96+
97+
return $psrResponse->withBody($this->streamFactory->createStreamFromResource($body));
9498
} catch (TransportExceptionInterface $e) {
9599
if ($e instanceof \InvalidArgumentException) {
96100
throw new Psr18RequestException($e, $request);

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,19 @@ public function cancel(): void
178178
$this->close();
179179
}
180180

181+
/**
182+
* Casts the response to a PHP stream resource.
183+
*
184+
* @return resource|null
185+
*/
186+
public function toStream()
187+
{
188+
// Ensure headers arrived
189+
$this->getStatusCode();
190+
191+
return StreamWrapper::createResource($this, null, $this->content, $this->handle && 'stream' === get_resource_type($this->handle) ? $this->handle : null);
192+
}
193+
181194
/**
182195
* Closes the response and all its network handles.
183196
*/
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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\HttpClientInterface;
16+
use Symfony\Contracts\HttpClient\ResponseInterface;
17+
18+
/**
19+
* Allows turning ResponseInterface instances to PHP streams.
20+
*
21+
* @author Nicolas Grekas <[email protected]>
22+
*/
23+
class StreamWrapper
24+
{
25+
/** @var resource */
26+
public $context;
27+
28+
/** @var HttpClientInterface */
29+
private $client;
30+
31+
/** @var ResponseInterface */
32+
private $response;
33+
34+
/** @var resource|null */
35+
private $content;
36+
37+
/** @var resource|null */
38+
private $handle;
39+
40+
private $eof = false;
41+
private $offset = 0;
42+
43+
/**
44+
* Creates a PHP stream resource from a ResponseInterface.
45+
*
46+
* @param resource|null $contentBuffer The seekable resource where the response body is buffered
47+
* @param resource|null $selectHandle The resource handle that should be monitored when
48+
* stream_select() is used on the created stream
49+
*
50+
* @return resource
51+
*/
52+
public static function createResource(ResponseInterface $response, HttpClientInterface $client = null, $contentBuffer = null, $selectHandle = null)
53+
{
54+
if (null === $client && !method_exists($response, 'stream')) {
55+
throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
56+
}
57+
58+
if (false === stream_wrapper_register('symfony', __CLASS__, STREAM_IS_URL)) {
59+
throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
60+
}
61+
62+
try {
63+
$context = [
64+
'client' => $client ?? $response,
65+
'response' => $response,
66+
'content' => $contentBuffer,
67+
'handle' => $selectHandle,
68+
];
69+
70+
return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null;
71+
} finally {
72+
stream_wrapper_unregister('symfony');
73+
}
74+
}
75+
76+
public function stream_open(string $path, string $mode, int $options): bool
77+
{
78+
if ('r' !== $mode) {
79+
if ($options & STREAM_REPORT_ERRORS) {
80+
trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), E_USER_WARNING);
81+
}
82+
83+
return false;
84+
}
85+
86+
$context = stream_context_get_options($this->context)['symfony'] ?? null;
87+
$this->client = $context['client'] ?? null;
88+
$this->response = $context['response'] ?? null;
89+
$this->content = $context['content'] ?? null;
90+
$this->handle = $context['handle'] ?? null;
91+
$this->context = null;
92+
93+
if (null !== $this->client && null !== $this->response) {
94+
return true;
95+
}
96+
97+
if ($options & STREAM_REPORT_ERRORS) {
98+
trigger_error('Missing options "client" or "response" in "symfony" stream context.', E_USER_WARNING);
99+
}
100+
101+
return false;
102+
}
103+
104+
public function stream_read(int $count)
105+
{
106+
if (null !== $this->content) {
107+
// Empty the internal activity list
108+
foreach ($this->client->stream([$this->response], 0) as $chunk) {
109+
try {
110+
$chunk->isTimeout();
111+
} catch (ExceptionInterface $e) {
112+
trigger_error($e->getMessage(), E_USER_WARNING);
113+
114+
return false;
115+
}
116+
}
117+
118+
if (0 !== fseek($this->content, $this->offset)) {
119+
return false;
120+
}
121+
122+
if ('' !== $data = fread($this->content, $count)) {
123+
fseek($this->content, 0, SEEK_END);
124+
$this->offset += \strlen($data);
125+
126+
return $data;
127+
}
128+
}
129+
130+
foreach ($this->client->stream([$this->response]) as $chunk) {
131+
try {
132+
$this->eof = true;
133+
$this->eof = !$chunk->isTimeout();
134+
$this->eof = $chunk->isLast();
135+
136+
if ('' !== $data = $chunk->getContent()) {
137+
$this->offset += \strlen($data);
138+
139+
return $data;
140+
}
141+
} catch (ExceptionInterface $e) {
142+
trigger_error($e->getMessage(), E_USER_WARNING);
143+
144+
return false;
145+
}
146+
}
147+
148+
return '';
149+
}
150+
151+
public function stream_tell(): int
152+
{
153+
return $this->offset;
154+
}
155+
156+
public function stream_eof(): bool
157+
{
158+
return $this->eof;
159+
}
160+
161+
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
162+
{
163+
if (null === $this->content || 0 !== fseek($this->content, 0, SEEK_END)) {
164+
return false;
165+
}
166+
167+
$size = ftell($this->content);
168+
169+
if (SEEK_CUR === $whence) {
170+
$offset += $this->offset;
171+
}
172+
173+
if (SEEK_END === $whence || $size < $offset) {
174+
foreach ($this->client->stream([$this->response]) as $chunk) {
175+
try {
176+
// Chunks are buffered in $this->content already
177+
$size += \strlen($chunk->getContent());
178+
179+
if (SEEK_END !== $whence && $offset <= $size) {
180+
break;
181+
}
182+
} catch (ExceptionInterface $e) {
183+
trigger_error($e->getMessage(), E_USER_WARNING);
184+
185+
return false;
186+
}
187+
}
188+
189+
if (SEEK_END === $whence) {
190+
$offset += $size;
191+
}
192+
}
193+
194+
if (0 <= $offset && $offset <= $size) {
195+
$this->eof = false;
196+
$this->offset = $offset;
197+
198+
return true;
199+
}
200+
201+
return false;
202+
}
203+
204+
public function stream_cast(int $castAs)
205+
{
206+
if (STREAM_CAST_FOR_SELECT === $castAs) {
207+
return $this->handle ?? false;
208+
}
209+
210+
return false;
211+
}
212+
213+
public function stream_stat(): array
214+
{
215+
return [
216+
'dev' => 0,
217+
'ino' => 0,
218+
'mode' => 33060,
219+
'nlink' => 0,
220+
'uid' => 0,
221+
'gid' => 0,
222+
'rdev' => 0,
223+
'size' => (int) ($this->response->getHeaders(false)['content-length'][0] ?? 0),
224+
'atime' => 0,
225+
'mtime' => strtotime($this->response->getHeaders(false)['last-modified'][0] ?? '') ?: 0,
226+
'ctime' => 0,
227+
'blksize' => 0,
228+
'blocks' => 0,
229+
];
230+
}
231+
}

src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use Psr\Log\AbstractLogger;
1515
use Symfony\Component\HttpClient\CurlHttpClient;
1616
use Symfony\Contracts\HttpClient\HttpClientInterface;
17-
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
1817

1918
/**
2019
* @requires extension curl
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\Tests;
13+
14+
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase as BaseHttpClientTestCase;
15+
16+
abstract class HttpClientTestCase extends BaseHttpClientTestCase
17+
{
18+
public function testToStream()
19+
{
20+
$client = $this->getHttpClient(__FUNCTION__);
21+
22+
$response = $client->request('GET', 'http://localhost:8057');
23+
24+
$stream = $response->toStream();
25+
26+
$this->assertSame("{\n \"SER", fread($stream, 10));
27+
$this->assertSame('VER_PROTOCOL', fread($stream, 12));
28+
$this->assertFalse(feof($stream));
29+
$this->assertTrue(rewind($stream));
30+
31+
$this->assertInternalType('array', json_decode(fread($stream, 1024), true));
32+
$this->assertSame('', fread($stream, 1));
33+
$this->assertTrue(feof($stream));
34+
}
35+
}

src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Symfony\Component\HttpClient\Response\MockResponse;
1818
use Symfony\Contracts\HttpClient\HttpClientInterface;
1919
use Symfony\Contracts\HttpClient\ResponseInterface;
20-
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
2120

2221
class MockHttpClientTest extends HttpClientTestCase
2322
{
@@ -31,13 +30,13 @@ protected function getHttpClient(string $testCase): HttpClientInterface
3130
];
3231

3332
$body = '{
34-
"SERVER_PROTOCOL": "HTTP/1.1",
35-
"SERVER_NAME": "127.0.0.1",
36-
"REQUEST_URI": "/",
37-
"REQUEST_METHOD": "GET",
38-
"HTTP_FOO": "baR",
39-
"HTTP_HOST": "localhost:8057"
40-
}';
33+
"SERVER_PROTOCOL": "HTTP/1.1",
34+
"SERVER_NAME": "127.0.0.1",
35+
"REQUEST_URI": "/",
36+
"REQUEST_METHOD": "GET",
37+
"HTTP_FOO": "baR",
38+
"HTTP_HOST": "localhost:8057"
39+
}';
4140

4241
$client = new NativeHttpClient();
4342

@@ -97,6 +96,7 @@ protected function getHttpClient(string $testCase): HttpClientInterface
9796
$responses[] = $mock;
9897
break;
9998

99+
case 'testToStream':
100100
case 'testBadRequestBody':
101101
case 'testOnProgressCancel':
102102
case 'testOnProgressError':

src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Component\HttpClient\NativeHttpClient;
1515
use Symfony\Contracts\HttpClient\HttpClientInterface;
16-
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
1716

1817
class NativeHttpClientTest extends HttpClientTestCase
1918
{

0 commit comments

Comments
 (0)