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

Commit 3dff33f

Browse files
committed
Merge branch 'feature/111' into develop
Close #111 Fixes #107
2 parents f5177ff + 6fc0b43 commit 3dff33f

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ All notable changes to this project will be documented in this file, in reverse
1616
- `filterHeader()`
1717
The `SapiEmitter` implementation has been updated to remove those methods and
1818
instead compose the trait.
19+
- [#111](https://github.com/zendframework/zend-diactoros/pull/111) adds
20+
a new emitter implementation, `SapiStreamEmitter`; this emitter type will
21+
loop through the stream instead of emitting it in one go, and supports content
22+
ranges.
1923

2024
### Deprecated
2125

src/Response/SapiStreamEmitter.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
6+
* @copyright Copyright (c) 2015 Oscar Otero (http://oscarotero.com) / Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
8+
*/
9+
10+
namespace Zend\Diactoros\Response;
11+
12+
use Psr\Http\Message\ResponseInterface;
13+
use RuntimeException;
14+
15+
class SapiStreamEmitter implements EmitterInterface
16+
{
17+
use SapiEmitterTrait;
18+
19+
/**
20+
* Emits a response for a PHP SAPI environment.
21+
*
22+
* Emits the status line and headers via the header() function, and the
23+
* body content via the output buffer.
24+
*
25+
* @param ResponseInterface $response
26+
* @param int $maxBufferLength Maximum output buffering size for each iteration
27+
*/
28+
public function emit(ResponseInterface $response, $maxBufferLength = 8192)
29+
{
30+
if (headers_sent()) {
31+
throw new RuntimeException('Unable to emit response; headers already sent');
32+
}
33+
34+
$response = $this->injectContentLength($response);
35+
36+
$this->emitStatusLine($response);
37+
$this->emitHeaders($response);
38+
$this->flush();
39+
40+
$range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
41+
42+
if (is_array($range)) {
43+
$this->emitBodyRange($range, $response, $maxBufferLength);
44+
return;
45+
}
46+
47+
$this->emitBody($response, $maxBufferLength);
48+
}
49+
50+
/**
51+
* Emit the message body.
52+
*
53+
* @param ResponseInterface $response
54+
* @param int $maxBufferLength
55+
*/
56+
private function emitBody(ResponseInterface $response, $maxBufferLength)
57+
{
58+
$body = $response->getBody();
59+
$body->rewind();
60+
61+
while (! $body->eof()) {
62+
echo $body->read($maxBufferLength);
63+
}
64+
}
65+
66+
/**
67+
* Emit a range of the message body.
68+
*
69+
* @param array $range
70+
* @param ResponseInterface $response
71+
* @param int $maxBufferLength
72+
*/
73+
private function emitBodyRange(array $range, ResponseInterface $response, $maxBufferLength)
74+
{
75+
list($unit, $first, $last, $lenght) = $range;
76+
77+
++$last; //zero-based position
78+
$body = $response->getBody();
79+
$body->seek($first);
80+
$pos = $first;
81+
82+
while (! $body->eof() && $pos < $last) {
83+
if (($pos + $maxBufferLength) > $last) {
84+
echo $body->read($last - $pos);
85+
break;
86+
}
87+
88+
echo $body->read($maxBufferLength);
89+
$pos = $body->tell();
90+
}
91+
}
92+
93+
/**
94+
* Parse content-range header
95+
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
96+
*
97+
* @param string $header
98+
* @return false|array [unit, first, last, length]; returns false if no
99+
* content range or an invalid content range is provided
100+
*/
101+
private function parseContentRange($header)
102+
{
103+
if (preg_match('/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/', $header, $matches)) {
104+
return [
105+
$matches['unit'],
106+
(int) $matches['first'],
107+
(int) $matches['last'],
108+
$matches['length'] === '*' ? '*' : (int) $matches['length'],
109+
];
110+
}
111+
return false;
112+
}
113+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
6+
* @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
8+
*/
9+
10+
namespace ZendTest\Diactoros\Response;
11+
12+
use PHPUnit_Framework_TestCase as TestCase;
13+
use Psr\Http\Message\ResponseInterface;
14+
use Psr\Http\Message\StreamInterface;
15+
use Zend\Diactoros\Response;
16+
use Zend\Diactoros\Response\SapiStreamEmitter;
17+
use ZendTest\Diactoros\TestAsset\HeaderStack;
18+
19+
class SapiStreamEmitterTest extends SapiEmitterTest
20+
{
21+
public function setUp()
22+
{
23+
HeaderStack::reset();
24+
$this->emitter = new SapiStreamEmitter();
25+
}
26+
27+
public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
28+
{
29+
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
30+
$stream->__toString()->willReturn('Content!');
31+
$stream->isSeekable()->willReturn(false);
32+
$stream->eof()->willReturn(true);
33+
$stream->rewind()->willReturn(true);
34+
$stream->getSize()->willReturn(null);
35+
$response = (new Response())
36+
->withStatus(200)
37+
->withBody($stream->reveal());
38+
39+
ob_start();
40+
$this->emitter->emit($response);
41+
ob_end_clean();
42+
foreach (HeaderStack::stack() as $header) {
43+
$this->assertNotContains('Content-Length:', $header);
44+
}
45+
}
46+
47+
public function contentRangeProvider()
48+
{
49+
return [
50+
['bytes 0-2/*', 'Hello world', 'Hel'],
51+
['bytes 3-6/*', 'Hello world', 'lo w'],
52+
];
53+
}
54+
55+
/**
56+
* @dataProvider contentRangeProvider
57+
*/
58+
public function testContentRange($header, $body, $expected)
59+
{
60+
$response = (new Response())
61+
->withHeader('Content-Range', $header);
62+
63+
$response->getBody()->write($body);
64+
65+
ob_start();
66+
$this->emitter->emit($response);
67+
$this->assertEquals($expected, ob_get_clean());
68+
}
69+
}

0 commit comments

Comments
 (0)