diff --git a/src/Routing/Responder.php b/src/Routing/Responder.php new file mode 100644 index 0000000..5ae9d3d --- /dev/null +++ b/src/Routing/Responder.php @@ -0,0 +1,52 @@ +getStatusLine($response)); + // Headers + foreach ($response->getHeaders() as $key => $headers) { + $replace = true; + foreach ($headers as $header) { + header("$key: $header", $replace); + $replace = false; + } + } + // Body + $body = $response->getBody(); + if ($body->isReadable()) { + $this->outputBody($response->getBody(), $chunkSize); + } + } + + private function getStatusLine(ResponseInterface $response) + { + $protocol = $response->getProtocolVersion(); + $statusCode = $response->getStatusCode(); + $reasonPhrase = $response->getReasonPhrase(); + if ($reasonPhrase) { + return "HTTP/$protocol $statusCode $reasonPhrase"; + } else { + return "HTTP/$protocol $statusCode"; + } + } + + private function outputBody(StreamableInterface $body, $chunkSize) + { + if ($chunkSize > 0) { + $body->rewind(); + while (!$body->eof()) { + print $body->read($chunkSize); + } + } else { + print (string) $body; + } + } +} diff --git a/test/src/HeaderStack.php b/test/src/HeaderStack.php new file mode 100644 index 0000000..c82e714 --- /dev/null +++ b/test/src/HeaderStack.php @@ -0,0 +1,28 @@ +body = $this->prophesize('\Psr\Http\Message\StreamableInterface'); + $this->body->isReadable()->willReturn(false); + $this->response = $this->prophesize('\Psr\Http\Message\ResponseInterface'); + $this->response->getHeaders()->willReturn([]); + $this->response->getProtocolVersion()->willReturn("1.1"); + $this->response->getStatusCode()->willReturn("200"); + $this->response->getReasonPhrase()->willReturn("Ok"); + $this->response->getBody()->willReturn($this->body->reveal()); + } + + public function testSendStatusCodeWithReasonPhrase() + { + $this->response->getStatusCode()->willReturn("200"); + $this->response->getReasonPhrase()->willReturn("Ok"); + + $responder = new Responder(); + $responder->respond($this->response->reveal()); + $this->assertContains("HTTP/1.1 200 Ok", HeaderStack::getHeaders()); + } + + public function testSendStatusCodeWithoutReasonPhrase() + { + $this->response->getStatusCode()->willReturn("999"); + $this->response->getReasonPhrase()->willReturn(null); + + $responder = new Responder(); + $responder->respond($this->response->reveal()); + $this->assertContains("HTTP/1.1 999", HeaderStack::getHeaders()); + } + + /** + * @dataProvider headerProvider + */ + public function testSendsHeaders($header) + { + $this->response->getHeaders()->willReturn([ + "Content-length" => ["2048"], + "X-foo" => ["bar", "baz"], + ]); + + $responder = new Responder(); + $responder->respond($this->response->reveal()); + $this->assertContains($header, HeaderStack::getHeaders()); + } + + public function headerProvider() + { + return [ + ["Content-length: 2048"], + ["X-foo: bar"], + ["X-foo: baz"] + ]; + } + + public function testOutputsBody() + { + $content = "Hello, world!"; + + $this->body->isReadable()->willReturn(true); + $this->body->__toString()->willReturn($content); + + $responder = new Responder(); + + ob_start(); + $responder->respond($this->response->reveal()); + $captured = ob_get_contents(); + ob_end_clean(); + + $this->assertEquals($content, $captured); + } + + public function testOutputsBodyInChunks() + { + $content = "Hello, world!"; + $chunkSize = 3; + $position = 0; + + $this->body->isReadable()->willReturn(true); + $this->body->rewind()->willReturn(true); + $this->body->eof()->willReturn(false); + $this->body->read(Argument::any())->will(function ($args) use ($content, &$position) { + $chunkSize = $args[0]; + $chunk = substr($content, $position, $chunkSize); + $position += $chunkSize; + if ($position >= strlen($content)) { + $this->eof()->willReturn(true); + } + return $chunk; + }); + + $responder = new Responder(); + + ob_start(); + $responder->respond($this->response->reveal(), $chunkSize); + $captured = ob_get_contents(); + ob_end_clean(); + + $this->assertEquals($content, $captured); + } +}