diff --git a/src/main/php/web/Response.class.php b/src/main/php/web/Response.class.php index d23776d9..a369eeab 100755 --- a/src/main/php/web/Response.class.php +++ b/src/main/php/web/Response.class.php @@ -1,6 +1,8 @@ transmit(...)` instead! * @param io.streams.InputStream $in * @param string $mediaType * @param int $size If omitted, uses chunked transfer encoding @@ -188,11 +191,43 @@ public function transfer($in, $mediaType= 'application/octet-stream', $size= nul } } + /** + * Transmits a given source to the output asynchronously. + * + * @param io.Channel|io.streams.InputStream $source + * @param string $mediaType + * @param int $size If omitted, uses chunked transfer encoding + * @return iterable + * @throws lang.IllegalArgumentException + */ + public function transmit($source, $mediaType= 'application/octet-stream', $size= null) { + if ($source instanceof InputStream) { + $in= $source; + } else if ($source instanceof Channel) { + $in= $source->in(); + } else { + throw new IllegalArgumentException('Expected either a channel or an input stream, have '.typeof($source)); + } + + $this->headers['Content-Type']= [$mediaType]; + $out= $this->stream($size); + try { + while ($in->available()) { + $out->write($in->read()); + yield; + } + } finally { + $out->close(); + $in->close(); + } + } + /** * Sends some content * * @param string $content * @param string $mediaType + * @return void */ public function send($content, $mediaType= 'text/html') { $this->headers['Content-Type']= [$mediaType]; diff --git a/src/test/php/web/unittest/ResponseTest.class.php b/src/test/php/web/unittest/ResponseTest.class.php index a78baf4f..7ffae488 100755 --- a/src/test/php/web/unittest/ResponseTest.class.php +++ b/src/test/php/web/unittest/ResponseTest.class.php @@ -1,12 +1,14 @@ transmit(new MemoryInputStream('

Test

'), 'text/html', 13) as $_) { } + + $this->assertResponse( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 13\r\n\r\n". + "

Test

", + $res + ); + } + + #[Test] + public function transmit_stream_chunked() { + $res= new Response(new TestOutput()); + foreach ($res->transmit(new MemoryInputStream('

Test

'), 'text/html') as $_) { } + + $this->assertResponse( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n". + "d\r\n

Test

\r\n0\r\n\r\n", + $res + ); + } + + #[Test] + public function transmit_stream_buffered() { + $res= new Response((new TestOutput())->using(Buffered::class)); + foreach ($res->transmit(new MemoryInputStream('

Test

'), 'text/html') as $_) { } + + $this->assertResponse( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 13\r\n\r\n". + "

Test

", + $res + ); + } + + #[Test] + public function transmit_channel() { + $res= new Response(new TestOutput()); + $channel= new class() implements Channel { + public function in() { return new MemoryInputStream('

Test

'); } + public function out() { /* Not implemented */ } + }; + foreach ($res->transmit($channel, 'text/html', 13) as $_) { } + + $this->assertResponse( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 13\r\n\r\n". + "

Test

", + $res + ); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function transmit_null() { + $res= new Response(new TestOutput()); + foreach ($res->transmit(null) as $_) { } + } + #[Test] public function cookies_and_headers_are_merged() { $res= new Response(new TestOutput());