From 676ca6eceb66c065eee4bfa6e660463c9635d347 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 29 Sep 2016 00:01:59 +0200 Subject: [PATCH 1/7] Message: support body to be a callback outputting the body into php://output (cherry picked from commit 0e79b792b116fd3968cc534e9523166e9334424c) --- lib/Message.php | 18 ++++++++++++------ lib/MessageInterface.php | 6 +++--- lib/Sapi.php | 5 +++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/Message.php b/lib/Message.php index 45bd183..a2a0aba 100644 --- a/lib/Message.php +++ b/lib/Message.php @@ -16,9 +16,9 @@ abstract class Message implements MessageInterface { /** * Request body * - * This should be a stream resource + * This should be a stream resource, string or a callback writing the body to php://output * - * @var resource + * @var resource|string|callable */ protected $body; @@ -53,6 +53,9 @@ function getBodyAsStream() { rewind($stream); return $stream; } + if (is_callable($body)) { + throw new \UnexpectedValueException('Callback to stream not supported'); + } return $body; } @@ -74,6 +77,9 @@ function getBodyAsString() { if (is_null($body)) { return ''; } + if (is_callable($body)) { + throw new \UnexpectedValueException('Callback to string not supported'); + } $contentLength = $this->getHeader('Content-Length'); if (is_int($contentLength) || ctype_digit($contentLength)) { return stream_get_contents($body, $contentLength); @@ -85,9 +91,9 @@ function getBodyAsString() { /** * Returns the message body, as it's internal representation. * - * This could be either a string or a stream. + * This could be either a string, a stream or a callback writing the body to php://output. * - * @return resource|string + * @return resource|string|callable */ function getBody() { @@ -96,9 +102,9 @@ function getBody() { } /** - * Replaces the body resource with a new stream or string. + * Replaces the body resource with a new stream, string or a callback writing the body to php://output. * - * @param resource|string $body + * @param resource|string|callable $body */ function setBody($body) { diff --git a/lib/MessageInterface.php b/lib/MessageInterface.php index df55beb..5c8877f 100644 --- a/lib/MessageInterface.php +++ b/lib/MessageInterface.php @@ -35,16 +35,16 @@ function getBodyAsString(); /** * Returns the message body, as it's internal representation. * - * This could be either a string or a stream. + * This could be either a string, a stream or a callback writing the body to php://output * - * @return resource|string + * @return resource|string|callable */ function getBody(); /** * Updates the body resource with a new stream. * - * @param resource|string $body + * @param resource|string|callable $body * @return void */ function setBody($body); diff --git a/lib/Sapi.php b/lib/Sapi.php index 054380e..f56cad3 100644 --- a/lib/Sapi.php +++ b/lib/Sapi.php @@ -71,6 +71,11 @@ static function sendResponse(ResponseInterface $response) { $body = $response->getBody(); if (is_null($body)) return; + if (is_callable($body)) { + $body(); + return; + } + $contentLength = $response->getHeader('Content-Length'); if ($contentLength !== null) { $output = fopen('php://output', 'wb'); From 46ce35e1c9b3daf3bc2fc4d02324fabda3d6b957 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 29 Sep 2016 00:23:44 +0200 Subject: [PATCH 2/7] Test coverage for callback as a body (cherry picked from commit 161dd98682e8b39e804a00c5054d39bf02fd8b69) --- tests/HTTP/MessageTest.php | 48 ++++++++++++++++++++++++++++++++++++++ tests/HTTP/SapiTest.php | 21 +++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/tests/HTTP/MessageTest.php b/tests/HTTP/MessageTest.php index cb5aadc..27312f0 100644 --- a/tests/HTTP/MessageTest.php +++ b/tests/HTTP/MessageTest.php @@ -42,6 +42,45 @@ function testStringBody() { } + /** + * @expectedException \UnexpectedValueException + */ + function testCallbackBodyAsString() { + + $body = $this->createCallback(); + + $message = new MessageMock(); + $message->setBody($body); + + $message->getBodyAsString(); + + } + + /** + * @expectedException \UnexpectedValueException + */ + function testCallbackBodyAsStream() { + + $body = $this->createCallback(); + + $message = new MessageMock(); + $message->setBody($body); + + $message->getBodyAsStream(); + + } + + function testGetBodyWhenCallback() { + + $body = $this->createCallback(); + + $message = new MessageMock(); + $message->setBody($body); + + $this->assertEquals($body, $message->getBody()); + + } + /** * It's possible that streams contains more data than the Content-Length. * @@ -241,6 +280,15 @@ function testHasHeaders() { } + private function createCallback() + { + return function() { + $fd = fopen('php://output', 'r+'); + fwrite($fd, 'foo'); + fclose($fd); + }; + } + } class MessageMock extends Message { } diff --git a/tests/HTTP/SapiTest.php b/tests/HTTP/SapiTest.php index 158ce21..935b8f6 100644 --- a/tests/HTTP/SapiTest.php +++ b/tests/HTTP/SapiTest.php @@ -164,4 +164,25 @@ function testSendLimitedByContentLengthStream() { } + /** + * @runInSeparateProcess + * @depends testSend + */ + function testSendWorksWithCallbackAsBody() { + $response = new Response(200, [], function() { + $fd = fopen('php://output', 'r+'); + fwrite($fd, 'foo'); + fclose($fd); + }); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + + $this->assertEquals('foo', $result); + + } + } From 2311a006232398bc7179fdee2e0b8893ee76e9d2 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 29 Sep 2016 09:35:07 +0200 Subject: [PATCH 3/7] Fix typo and use single-quotes for consistency (cherry picked from commit a849fd60d62548a8d3453ed40aee42e3ab3e9824) --- tests/HTTP/MessageTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/HTTP/MessageTest.php b/tests/HTTP/MessageTest.php index 27312f0..8f9c77d 100644 --- a/tests/HTTP/MessageTest.php +++ b/tests/HTTP/MessageTest.php @@ -87,7 +87,7 @@ function testGetBodyWhenCallback() { * The request object should make sure to never emit more than * Content-Length, if Content-Length is set. * - * This is in particular useful when respoding to range requests with + * This is in particular useful when responding to range requests with * streams that represent files on the filesystem, as it's possible to just * seek the stream to a certain point, set the content-length and let the * request object do the rest. @@ -247,11 +247,11 @@ function testMultipleHeaders() { $message->addHeader('A', '2'); $this->assertEquals( - "1,2", + '1,2', $message->getHeader('A') ); $this->assertEquals( - "1,2", + '1,2', $message->getHeader('a') ); From 4a81ba5cd3d21e254bcf153aa133e93113ffef13 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 6 Oct 2016 14:08:48 +0200 Subject: [PATCH 4/7] Message: implement getBodyAsString() and getBodyAsStream() for the case when is a callback function (cherry picked from commit a95cb499848494f6ae4ba906038b99ba816b1112) --- lib/Message.php | 29 +++++++++++++++++++++++++---- tests/HTTP/MessageTest.php | 34 ++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/Message.php b/lib/Message.php index a2a0aba..027c7ed 100644 --- a/lib/Message.php +++ b/lib/Message.php @@ -42,20 +42,21 @@ abstract class Message implements MessageInterface { * Note that the stream may not be rewindable, and therefore may only be * read once. * + * @throws \RuntimeException * @return resource */ function getBodyAsStream() { $body = $this->getBody(); + if (is_callable($body)) { + $body = $this->captureCallbackOutput($body); + } if (is_string($body) || is_null($body)) { $stream = fopen('php://temp', 'r+'); fwrite($stream, $body); rewind($stream); return $stream; } - if (is_callable($body)) { - throw new \UnexpectedValueException('Callback to stream not supported'); - } return $body; } @@ -66,6 +67,7 @@ function getBodyAsStream() { * Note that because the underlying data may be based on a stream, this * method could only work correctly the first time. * + * @throws \RuntimeException * @return string */ function getBodyAsString() { @@ -78,7 +80,7 @@ function getBodyAsString() { return ''; } if (is_callable($body)) { - throw new \UnexpectedValueException('Callback to string not supported'); + return $this->captureCallbackOutput($body); } $contentLength = $this->getHeader('Content-Length'); if (is_int($contentLength) || ctype_digit($contentLength)) { @@ -317,4 +319,23 @@ function getHttpVersion() { return $this->httpVersion; } + + /** + * Runs given callback and captures data sent to php://output stream. + * + * @param callable $callback + * @throws \RuntimeException when ob_start() fails to start output buffer + * @return string + */ + private function captureCallbackOutput($callback) + { + $success = ob_start(); + if ($success === false) { + throw new \RuntimeException('Cannot start output buffering'); + } + $callback(); + $content = ob_get_contents(); + ob_end_clean(); + return $content; + } } diff --git a/tests/HTTP/MessageTest.php b/tests/HTTP/MessageTest.php index 8f9c77d..04899b6 100644 --- a/tests/HTTP/MessageTest.php +++ b/tests/HTTP/MessageTest.php @@ -42,42 +42,40 @@ function testStringBody() { } - /** - * @expectedException \UnexpectedValueException - */ function testCallbackBodyAsString() { - $body = $this->createCallback(); + $body = $this->createCallback('foo'); $message = new MessageMock(); $message->setBody($body); - $message->getBodyAsString(); + $string = $message->getBodyAsString(); + + $this->assertSame('foo', $string); } - /** - * @expectedException \UnexpectedValueException - */ function testCallbackBodyAsStream() { - $body = $this->createCallback(); + $body = $this->createCallback('foo'); $message = new MessageMock(); $message->setBody($body); - $message->getBodyAsStream(); + $stream = $message->getBodyAsStream(); + + $this->assertSame('foo', stream_get_contents($stream)); } function testGetBodyWhenCallback() { - $body = $this->createCallback(); + $callback = $this->createCallback('foo'); $message = new MessageMock(); - $message->setBody($body); + $message->setBody($callback); - $this->assertEquals($body, $message->getBody()); + $this->assertSame($callback, $message->getBody()); } @@ -280,11 +278,15 @@ function testHasHeaders() { } - private function createCallback() + /** + * @param string $content + * @return \Closure Returns a callback printing $content to php://output stream + */ + private function createCallback($content) { - return function() { + return function() use ($content) { $fd = fopen('php://output', 'r+'); - fwrite($fd, 'foo'); + fwrite($fd, $content); fclose($fd); }; } From f390fc8584deef6f86b6ee7f4dc74fca7abeb94d Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Thu, 6 Oct 2016 18:02:20 +0200 Subject: [PATCH 5/7] Message: replace ob_get_contents() & ob_end_clean() by ob_get_clean() (cherry picked from commit 78c34ba70e9c1e44d040709bfa23eca5eba33bbb) --- lib/Message.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Message.php b/lib/Message.php index 027c7ed..7a8c005 100644 --- a/lib/Message.php +++ b/lib/Message.php @@ -334,8 +334,6 @@ private function captureCallbackOutput($callback) throw new \RuntimeException('Cannot start output buffering'); } $callback(); - $content = ob_get_contents(); - ob_end_clean(); - return $content; + return ob_get_clean(); } } From 26fef7feb4a3bd2c102e91c70b0e1d1c917f6ad6 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Fri, 7 Oct 2016 00:18:14 +0200 Subject: [PATCH 6/7] Message: simplification: don't check return value of ob_start() as it doesn't fail in common cases; MessageTest: use echo rather then complicated fwrite to php://output (cherry picked from commit f065bd538857acdc7bbcad9afc2d6d78007196a2) --- lib/Message.php | 8 +------- tests/HTTP/MessageTest.php | 4 +--- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/Message.php b/lib/Message.php index 7a8c005..cead5ee 100644 --- a/lib/Message.php +++ b/lib/Message.php @@ -42,7 +42,6 @@ abstract class Message implements MessageInterface { * Note that the stream may not be rewindable, and therefore may only be * read once. * - * @throws \RuntimeException * @return resource */ function getBodyAsStream() { @@ -67,7 +66,6 @@ function getBodyAsStream() { * Note that because the underlying data may be based on a stream, this * method could only work correctly the first time. * - * @throws \RuntimeException * @return string */ function getBodyAsString() { @@ -324,15 +322,11 @@ function getHttpVersion() { * Runs given callback and captures data sent to php://output stream. * * @param callable $callback - * @throws \RuntimeException when ob_start() fails to start output buffer * @return string */ private function captureCallbackOutput($callback) { - $success = ob_start(); - if ($success === false) { - throw new \RuntimeException('Cannot start output buffering'); - } + ob_start(); $callback(); return ob_get_clean(); } diff --git a/tests/HTTP/MessageTest.php b/tests/HTTP/MessageTest.php index 04899b6..078c113 100644 --- a/tests/HTTP/MessageTest.php +++ b/tests/HTTP/MessageTest.php @@ -285,9 +285,7 @@ function testHasHeaders() { private function createCallback($content) { return function() use ($content) { - $fd = fopen('php://output', 'r+'); - fwrite($fd, $content); - fclose($fd); + echo $content; }; } From ce733bbbac7848c131e7347512a1162e764fdbe5 Mon Sep 17 00:00:00 2001 From: Petr Kotek Date: Mon, 17 Oct 2016 00:21:12 +0200 Subject: [PATCH 7/7] update dependencies (cherry picked from commit 4b6fef9d1af3801c84195ff11bf5be951e23b602) --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 507d5d2..46a93c1 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,11 @@ "ext-mbstring" : "*", "ext-ctype" : "*", "sabre/event" : ">=1.0.0,<4.0.0", - "sabre/uri" : "~1.0" + "sabre/uri" : "^1.0.1" }, "require-dev" : { "phpunit/phpunit" : "~4.3", - "sabre/cs" : "~0.0.1" + "sabre/cs" : "~0.0.8" }, "suggest" : { "ext-curl" : " to make http requests with the Client class"