diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index a6722db7d9ad..47b183968482 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -621,6 +621,18 @@ public function sink($to) return $this; } + /** + * Indicate that the response body should be streamed instead of buffered in memory. + * + * @return $this + */ + public function stream() + { + $this->options['stream'] = true; + + return $this; + } + /** * Specify the timeout (in seconds) for the request. * diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index 9d3bd52175e7..6d493a7987dc 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -3,7 +3,9 @@ namespace Illuminate\Http\Client; use ArrayAccess; +use Generator; use GuzzleHttp\Psr7\StreamWrapper; +use GuzzleHttp\Psr7\Utils; use Illuminate\Support\Collection; use Illuminate\Support\Fluent; use Illuminate\Support\Traits\Macroable; @@ -163,6 +165,26 @@ public function resource() return StreamWrapper::getResource($this->response->getBody()); } + /** + * Get an iterator that yields each line of the response body. + * + * @return \Generator + */ + public function lines(): Generator + { + $body = $this->response->getBody(); + + while (! $body->eof()) { + $line = Utils::readLine($body); + + if ($line === '') { + continue; + } + + yield rtrim($line, "\r\n"); + } + } + /** * Get a header from the response. * diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 409a186188cc..6cb29c113675 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -1682,6 +1682,77 @@ public function testSinkWhenStubbedByPath() $this->assertSame(json_encode(['page' => 'foo']), stream_get_contents($resource)); } + public function testStreamMethodSetsGuzzleStreamOption() + { + $request = $this->factory->stream(); + + $this->assertTrue($request->getOptions()['stream']); + } + + public function testStreamMethodIsChainable() + { + $this->factory->fake([ + '*' => $this->factory::response('ok'), + ]); + + $response = $this->factory->stream()->get('http://example.com'); + + $this->assertTrue($response->successful()); + } + + public function testResponseLinesYieldsEachLineOfTheBody() + { + $this->factory->fake([ + '*' => $this->factory::response("first\nsecond\nthird"), + ]); + + $response = $this->factory->stream()->get('http://example.com'); + + $this->assertSame( + ['first', 'second', 'third'], + iterator_to_array($response->lines(), false), + ); + } + + public function testResponseLinesHandlesMixedLineEndings() + { + $this->factory->fake([ + '*' => $this->factory::response("alpha\r\nbeta\ngamma\r\ndelta"), + ]); + + $response = $this->factory->stream()->get('http://example.com'); + + $this->assertSame( + ['alpha', 'beta', 'gamma', 'delta'], + iterator_to_array($response->lines(), false), + ); + } + + public function testResponseLinesPreservesBlankLinesBetweenContent() + { + $this->factory->fake([ + '*' => $this->factory::response("one\n\ntwo\n\nthree\n"), + ]); + + $response = $this->factory->stream()->get('http://example.com'); + + $this->assertSame( + ['one', '', 'two', '', 'three'], + iterator_to_array($response->lines(), false), + ); + } + + public function testResponseLinesReturnsEmptyGeneratorForEmptyBody() + { + $this->factory->fake([ + '*' => $this->factory::response(''), + ]); + + $response = $this->factory->stream()->get('http://example.com'); + + $this->assertSame([], iterator_to_array($response->lines(), false)); + } + public function testCanAssertAgainstOrderOfHttpRequestsWithUrlStrings() { $this->factory->fake();