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" diff --git a/lib/Message.php b/lib/Message.php index 45bd183..cead5ee 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; @@ -47,6 +47,9 @@ abstract class Message implements MessageInterface { 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); @@ -74,6 +77,9 @@ function getBodyAsString() { if (is_null($body)) { return ''; } + if (is_callable($body)) { + return $this->captureCallbackOutput($body); + } $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) { @@ -311,4 +317,17 @@ function getHttpVersion() { return $this->httpVersion; } + + /** + * Runs given callback and captures data sent to php://output stream. + * + * @param callable $callback + * @return string + */ + private function captureCallbackOutput($callback) + { + ob_start(); + $callback(); + return ob_get_clean(); + } } 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'); diff --git a/tests/HTTP/MessageTest.php b/tests/HTTP/MessageTest.php index cb5aadc..078c113 100644 --- a/tests/HTTP/MessageTest.php +++ b/tests/HTTP/MessageTest.php @@ -42,13 +42,50 @@ function testStringBody() { } + function testCallbackBodyAsString() { + + $body = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($body); + + $string = $message->getBodyAsString(); + + $this->assertSame('foo', $string); + + } + + function testCallbackBodyAsStream() { + + $body = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($body); + + $stream = $message->getBodyAsStream(); + + $this->assertSame('foo', stream_get_contents($stream)); + + } + + function testGetBodyWhenCallback() { + + $callback = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($callback); + + $this->assertSame($callback, $message->getBody()); + + } + /** * It's possible that streams contains more data than the Content-Length. * * 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. @@ -208,11 +245,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') ); @@ -241,6 +278,17 @@ function testHasHeaders() { } + /** + * @param string $content + * @return \Closure Returns a callback printing $content to php://output stream + */ + private function createCallback($content) + { + return function() use ($content) { + echo $content; + }; + } + } 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); + + } + }