Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Commit

Permalink
Merge branch 'hotfix/251a' into develop
Browse files Browse the repository at this point in the history
Close #270
Fixes #251
Fixes #216
  • Loading branch information
weierophinney committed Sep 13, 2017
2 parents 5f0c491 + a22b273 commit 0d5eb1f
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 140 deletions.
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@ All notable changes to this project will be documented in this file, in reverse

### Changed

- Nothing.
- [#270](https://github.com/zendframework/zend-diactoros/pull/270) changes the
behavior of `Zend\Diactoros\Server`: it no longer creates an output buffer.

- [#270](https://github.com/zendframework/zend-diactoros/pull/270) changes the
behavior of the two SAPI emitters in two backwards-incompatible ways:

- They no longer auto-inject a `Content-Length` header. If you need this
functionality, zendframework/zend-expressive-helpers 4.1+ provides it via
`Zend\Expressive\Helper\ContentLengthMiddleware`.

- They no longer flush the output buffer. Instead, if headers have been sent,
or the output buffer exists and has a non-zero length, the emitters raise an
exception, as mixed PSR-7/output buffer content creates a blocking issue.
If you are emitting content via `echo`, `print`, `var_dump`, etc., or not
catching PHP errors or exceptions, you will need to either fix your
application to always work with a PSR-7 response, or provide your own
emitters that allow mixed output mechanisms.

### Deprecated

Expand Down
53 changes: 49 additions & 4 deletions doc/book/emitting-responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
If you are using a non-SAPI PHP implementation and wish to use the `Server` class, or if you do not
want to use the `Server` implementation but want to emit a response, this package provides an
interface, `Zend\Diactoros\Response\EmitterInterface`, defining a method `emit()` for emitting the
response. A single implementation is currently available, `Zend\Diactoros\Response\SapiEmitter`,
which will use the native PHP functions `header()` and `echo` in order to emit the response. If you
are using a non-SAPI implementation, you will need to create your own `EmitterInterface`
implementation.
response.

Diactoros provides two implementations currently, both for working with
traditional Server API (SAPI) implementations: `Zend\Diactoros\Response\SapiEmitter`
and `Zend\Diactoros\Response\SapiStreamEmitter`. Each uses the native `header()`
PHP function to emit headers, and `echo()` to emit the response body.

If you are using a non-SAPI implementation, you will need to create your own
`EmitterInterface` implementation.

For example, the `SapiEmitter` implementation of the `EmitterInterface` can be used thus:

Expand All @@ -16,3 +21,43 @@ $response->getBody()->write("some content\n");
$emitter = new Zend\Diactoros\Response\SapiEmitter();
$emitter->emit($response);
```

## Emitting ranges of streamed files

The `SapiStreamEmitter` is useful when you want to emit a `Content-Range`. As an
example, to stream a range of bytes from a file to a client, the client can pass
the following header:

```http
Range: bytes=1024-2047
```

Your application would then populate the response with a `Content-Range` header:

```php
$range = $request->getHeaderLine('range');
$range = str_replace('=', ' ', $range);

$body = new Stream($pathToFile);
$size = $body->getSize();
$range .= '/' . $size;

$response = new Response($body);
$response = $response->withHeader('Content-Range', $range);
```

> Note: you will likely want to ensure the range specified falls within the
> content size of the streamed body!
The `SapiStreamEmitter` detects the `Content-Range` header and emits only the
bytes specified.

```php
$emitter = new SapiStreamEmitter();
$emitter->emit($response);
```

The `SapiStreamEmitter` may be used in place of the `SapiEmitter`, even when not
sending files. However, unlike the `SapiEmitter`, it will emit a chunk of
content at a time instead of the full content at once, which could lead to
performance overhead. The default chunk size is 8192 bytes.
10 changes: 2 additions & 8 deletions src/Response/SapiEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,13 @@ class SapiEmitter implements EmitterInterface
* body content via the output buffer.
*
* @param ResponseInterface $response
* @param null|int $maxBufferLevel Maximum output buffering level to unwrap.
*/
public function emit(ResponseInterface $response, $maxBufferLevel = null)
public function emit(ResponseInterface $response)
{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}

$response = $this->injectContentLength($response);
$this->assertNoPreviousOutput();

$this->emitStatusLine($response);
$this->emitHeaders($response);
$this->flush($maxBufferLevel);
$this->emitBody($response);
}

Expand Down
41 changes: 13 additions & 28 deletions src/Response/SapiEmitterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,28 @@
namespace Zend\Diactoros\Response;

use Psr\Http\Message\ResponseInterface;
use RuntimeException;

trait SapiEmitterTrait
{
/**
* Inject the Content-Length header if is not already present.
* Checks to see if content has previously been sent.
*
* @param ResponseInterface $response
* @return ResponseInterface
* If either headers have been sent or the output buffer contains content,
* raises an exception.
*
* @throws RuntimeException if headers have already been sent.
* @throws RuntimeException if output is present in the output buffer.
*/
private function injectContentLength(ResponseInterface $response)
private function assertNoPreviousOutput()
{
if (! $response->hasHeader('Content-Length')) {
// PSR-7 indicates int OR null for the stream size; for null values,
// we will not auto-inject the Content-Length.
if (null !== $response->getBody()->getSize()) {
return $response->withHeader('Content-Length', (string) $response->getBody()->getSize());
}
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}

return $response;
if (ob_get_level() > 0 && ob_get_length() > 0) {
throw new RuntimeException('Output has been emitted previously; cannot emit response');
}
}

/**
Expand Down Expand Up @@ -77,23 +79,6 @@ private function emitHeaders(ResponseInterface $response)
}
}

/**
* Loops through the output buffer, flushing each, before emitting
* the response.
*
* @param int|null $maxBufferLevel Flush up to this buffer level.
*/
private function flush($maxBufferLevel = null)
{
if (null === $maxBufferLevel) {
$maxBufferLevel = ob_get_level();
}

while (ob_get_level() > $maxBufferLevel) {
ob_end_flush();
}
}

/**
* Filter a header name to wordcase
*
Expand Down
11 changes: 2 additions & 9 deletions src/Response/SapiStreamEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,13 @@ class SapiStreamEmitter implements EmitterInterface
* body content via the output buffer.
*
* @param ResponseInterface $response
* @param null|int $maxBufferLevel Maximum output buffering level to unwrap.
* @param int $maxBufferLength Maximum output buffering size for each iteration
*/
public function emit(ResponseInterface $response, $maxBufferLevel = null, $maxBufferLength = 8192)
public function emit(ResponseInterface $response, $maxBufferLength = 8192)
{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}

$response = $this->injectContentLength($response);

$this->assertNoPreviousOutput();
$this->emitStatusLine($response);
$this->emitHeaders($response);
$this->flush($maxBufferLevel);

$range = $this->parseContentRange($response->getHeaderLine('Content-Range'));

Expand Down
10 changes: 2 additions & 8 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,24 +150,18 @@ public static function createServerFromRequest(
* If provided a $finalHandler, that callable will be used for
* incomplete requests.
*
* Output buffering is enabled prior to invoking the attached
* callback; any output buffered will be sent prior to any
* response body content.
*
* @param null|callable $finalHandler
*/
public function listen(callable $finalHandler = null)
{
$callback = $this->callback;

ob_start();
$bufferLevel = ob_get_level();

$response = $callback($this->request, $this->response, $finalHandler);
if (! $response instanceof ResponseInterface) {
$response = $this->response;
}
$this->getEmitter()->emit($response, $bufferLevel);

$this->getEmitter()->emit($response);
}

/**
Expand Down
1 change: 0 additions & 1 deletion test/Response/AbstractEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public function testEmitsResponseHeaders()
ob_end_clean();
$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
$this->assertContains('Content-Length: 8', HeaderStack::stack());
}

public function testEmitsMessageBody()
Expand Down
22 changes: 0 additions & 22 deletions test/Response/SapiEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,4 @@

class SapiEmitterTest extends AbstractEmitterTest
{
public function testEmitsBufferLevel()
{
ob_start();
echo "level" . ob_get_level() . " "; // 2
ob_start();
echo "level" . ob_get_level() . " "; // 3
ob_start();
echo "level" . ob_get_level() . " "; // 4
$response = (new Response())
->withStatus(200)
->withAddedHeader('Content-Type', 'text/plain');
$response->getBody()->write('Content!');
ob_start();
$this->emitter->emit($response);
$this->assertEquals('Content!', ob_get_contents());
ob_end_clean();
$this->assertEquals('level4 ', ob_get_contents(), 'current buffer level string must remains after emit');
ob_end_clean();
$this->emitter->emit($response, 2);
$this->assertEquals('level2 level3 Content!', ob_get_contents(), 'must buffer until specified level');
ob_end_clean();
}
}
6 changes: 3 additions & 3 deletions test/Response/SapiStreamEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ function ($bufferLength) use (& $peakBufferLength) {
->withBody($stream->reveal());

ob_start();
$this->emitter->emit($response, null, $maxBufferLength);
$this->emitter->emit($response, $maxBufferLength);
$emittedContents = ob_get_clean();

if ($seekable) {
Expand Down Expand Up @@ -351,7 +351,7 @@ function ($bufferLength) use (& $peakBufferLength) {
->withBody($stream->reveal());

ob_start();
$this->emitter->emit($response, null, $maxBufferLength);
$this->emitter->emit($response, $maxBufferLength);
$emittedContents = ob_get_clean();

$stream->rewind()->shouldNotBeCalled();
Expand Down Expand Up @@ -497,7 +497,7 @@ function () use (& $closureTrackMemoryUsage) {

gc_disable();

$this->emitter->emit($response, null, $maxBufferLength);
$this->emitter->emit($response, $maxBufferLength);

ob_end_flush();

Expand Down
49 changes: 0 additions & 49 deletions test/ServerIntegrationTest.php

This file was deleted.

7 changes: 0 additions & 7 deletions test/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ public function testEmitterSetter()

$this->expectOutputString('');
$server->listen();
ob_end_flush();
}

public function testCreateServerWillCreateDefaultInstancesForRequestAndResponse()
Expand Down Expand Up @@ -153,7 +152,6 @@ public function testListenInvokesCallbackAndSendsResponse()

$this->expectOutputString('FOOBAR');
$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand All @@ -179,7 +177,6 @@ public function testListenEmitsStatusHeaderWithoutReasonPhraseIfNoReasonPhrase()

$this->expectOutputString('FOOBAR');
$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 299', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand All @@ -204,7 +201,6 @@ public function testEnsurePercentCharactersDoNotResultInOutputError()

$this->expectOutputString('100%');
$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand Down Expand Up @@ -233,7 +229,6 @@ public function testEmitsHeadersWithMultipleValuesMultipleTimes()
$server = Server::createServer($callback, $server, [], [], [], []);

$server->listen();
ob_end_flush();

$this->assertContains('HTTP/1.1 200 OK', HeaderStack::stack());
$this->assertContains('Content-Type: text/plain', HeaderStack::stack());
Expand All @@ -256,7 +251,6 @@ public function testEmitsHeadersWithMultipleValuesMultipleTimes()
*/
public function testHeaderOrderIsHonoredWhenEmitted($stack)
{
array_pop($stack); // ignore "Content-Length" automatically set by the response emitter
$header = array_pop($stack);
$this->assertContains(
'Set-Cookie: bar=baz; expires=Wed, 8 Oct 2014 10:30; path=/foo/bar; domain=example.com',
Expand Down Expand Up @@ -311,7 +305,6 @@ public function testListenPassesCallableArgumentToCallback()
$this->response
);
$server->listen($final);
ob_end_flush();
$this->assertTrue($invoked);
}
}

0 comments on commit 0d5eb1f

Please sign in to comment.