Skip to content

Commit

Permalink
Merge pull request #58 from yiisoft/stream-output
Browse files Browse the repository at this point in the history
Stream output
  • Loading branch information
xepozz authored Mar 3, 2024
2 parents d3ceae2 + 172f226 commit e635091
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 70 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
uses: yiisoft/actions/.github/workflows/phpunit.yml@master
with:
extensions: sockets
ini-values: disable_functions="flush,header,http_response_code,headers_sent,header_remove"
os: >-
['ubuntu-latest', 'windows-latest']
php: >-
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
mutation:
uses: yiisoft/actions/.github/workflows/roave-infection.yml@master
with:
ini-values: disable_functions="flush,header,http_response_code,headers_sent,header_remove"
os: >-
['ubuntu-latest']
php: >-
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 2.2.1 under development

- no changes in this release.
- Enh #58: Support stream output headers (@xepozz)

## 2.2.0 December 25, 2023

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"roave/infection-static-analysis-plugin": "^1.25",
"spatie/phpunit-watcher": "^1.23",
"vimeo/psalm": "^4.30|^5.2",
"xepozz/internal-mocker": "^1.4",
"yiisoft/middleware-dispatcher": "^5.0",
"yiisoft/test-support": "^3.0"
},
Expand Down Expand Up @@ -77,7 +78,7 @@
}
},
"scripts": {
"test": "phpunit --testdox --no-interaction",
"test": "php -ddisable_functions=flush,header,http_response_code,headers_sent,header_remove ./vendor/bin/phpunit",
"test-watch": "phpunit-watcher watch"
}
}
4 changes: 4 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
<directory>./src</directory>
</include>
</coverage>

<extensions>
<extension class="Yiisoft\Yii\Runner\Http\Tests\Support\MockerExtension"/>
</extensions>
</phpunit>
11 changes: 11 additions & 0 deletions src/SapiEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Yiisoft\Yii\Runner\Http\Exception\HeadersHaveBeenSentException;

use function flush;
use function headers_sent;
use function in_array;
use function sprintf;

Expand Down Expand Up @@ -98,6 +99,13 @@ public function emit(ResponseInterface $response, bool $withoutBody = false): vo
}
}

/**
* Sends headers before the body.
* Makes a client possible to recognize the type of the body content if it is sent with a delay,
* for instance, for a streamed response.
*/
flush();

$this->emitBody($response);
}

Expand All @@ -113,6 +121,9 @@ private function emitBody(ResponseInterface $response): void
while (!$body->eof()) {
$output = $body->read($this->bufferSize);
if ($output === '') {
while (ob_get_level() > $level) {
ob_end_flush();
}
continue;
}
echo $output;
Expand Down
6 changes: 3 additions & 3 deletions tests/RequestFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function testInvalidMethodException(): void
$requestFactory->create();
}

public function bodyDataProvider(): array
public static function bodyDataProvider(): array
{
return [
'string' => ['content', 'content'],
Expand All @@ -105,7 +105,7 @@ public function testBody(string $expected, ?string $body): void
$this->assertSame($expected, (string) $request->getBody());
}

public function hostParsingDataProvider(): array
public static function hostParsingDataProvider(): array
{
return [
'host' => [
Expand Down Expand Up @@ -387,7 +387,7 @@ public function testHostParsingFromGlobals(array $serverParams, array $expectPar
$this->assertSame($expectParams['query'], $request->getUri()->getQuery());
}

public function dataPostInParsedBody(): array
public static function dataPostInParsedBody(): array
{
return [
[
Expand Down
62 changes: 55 additions & 7 deletions tests/SapiEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Yiisoft\Yii\Runner\Http\Tests;

include 'Support/Emitter/httpFunctionMocks.php';

use HttpSoft\Message\Response;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
Expand All @@ -32,7 +30,7 @@ public static function tearDownAfterClass(): void
HTTPFunctions::reset();
}

public function bufferSizeProvider(): array
public static function bufferSizeProvider(): array
{
return [[null], [1], [100], [1000]];
}
Expand All @@ -56,7 +54,7 @@ public function testEmit(?int $bufferSize): void
$this->expectOutputString($body);
}

public function noBodyResponseCodeProvider(): array
public static function noBodyResponseCodeProvider(): array
{
return [[100], [101], [102], [204], [205], [304]];
}
Expand Down Expand Up @@ -261,15 +259,30 @@ public function testObLevel(): void
$this->assertSame($expectedLevel, $actualLevel);
}

public function testExtraObLevel(): void
public static function dataExtraObLevel(): iterable
{
yield 'empty response' => [
'',
1,
];
yield 'some response' => [
'Example body',
2,
];
}

/**
* @dataProvider dataExtraObLevel
*/
public function testExtraObLevel(string $responseBody, int $expectedFlushes): void
{
$expectedLevel = ob_get_level();
$stream = $this->createMock(StreamInterface::class);
$stream->method('read')->willReturnCallback(static function () {
$stream->method('read')->willReturnCallback(static function () use ($responseBody) {
ob_start();
ob_start();
ob_start();
return '-';
return $responseBody;
});
$stream->method('isReadable')->willReturn(true);
$stream->method('eof')->willReturnOnConsecutiveCalls(false, true);
Expand All @@ -283,6 +296,41 @@ public function testExtraObLevel(): void

$actualLevel = ob_get_level();
$this->assertSame($expectedLevel, $actualLevel);
$this->assertSame($expectedFlushes, HTTPFunctions::getFlushTimes());
}

public function testFlushWithBody(): void
{
$stream = $this->createMock(StreamInterface::class);
$stream->method('read')->willReturnCallback(static fn() => '-');
$stream->method('isReadable')->willReturn(true);
$stream->method('eof')->willReturnOnConsecutiveCalls(false, true);
$response = $this->createResponse(Status::OK, ['X-Test' => 1])
->withBody($stream);

$this
->createEmitter()
->emit($response);

$this->assertSame(['X-Test: 1'], HTTPFunctions::getHeader('X-Test'));
$this->assertSame(2, HTTPFunctions::getFlushTimes());
}

public function testFlushWithoutBody(): void
{
$stream = $this->createMock(StreamInterface::class);
$stream->method('isReadable')->willReturn(true);
$stream->method('eof')->willReturnOnConsecutiveCalls(true);
$response = $this->createResponse(Status::OK, ['X-Test' => 1])
->withBody($stream)
;

$this
->createEmitter()
->emit($response);

$this->assertSame(['X-Test: 1'], HTTPFunctions::getHeader('X-Test'));
$this->assertSame(1, HTTPFunctions::getFlushTimes());
}

private function createEmitter(?int $bufferSize = null): SapiEmitter
Expand Down
6 changes: 3 additions & 3 deletions tests/ServerRequestFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function testInvalidMethodException(): void
->createFromParameters([]);
}

public function bodyDataProvider(): array
public static function bodyDataProvider(): array
{
$content = 'content';
$resource = fopen('php://memory', 'wb+');
Expand Down Expand Up @@ -119,7 +119,7 @@ public function testBody(mixed $body, string $expected): void
$this->assertSame($expected, (string) $request->getBody());
}

public function invalidBodyDataProvider(): array
public static function invalidBodyDataProvider(): array
{
return [
'int' => [1],
Expand Down Expand Up @@ -150,7 +150,7 @@ public function testInvalidBodyException(mixed $body): void
->createFromParameters($server, [], [], [], [], [], $body);
}

public function hostParsingDataProvider(): array
public static function hostParsingDataProvider(): array
{
return [
'host' => [
Expand Down
25 changes: 21 additions & 4 deletions tests/Support/Emitter/HTTPFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class HTTPFunctions
private static string $headersSentFile = '';
private static int $headersSentLine = 0;
private static string $rawHttpHeader = '';
private static int $flushedTimes = 0;

/**
* Reset state
Expand All @@ -35,6 +36,7 @@ public static function reset(): void
self::$headersSentFile = '';
self::$headersSentLine = 0;
self::$rawHttpHeader = '';
self::$flushedTimes = 0;
}

/**
Expand All @@ -60,7 +62,7 @@ public static function headers_sent(&$file = null, &$line = null): bool
/**
* Send a raw HTTP header
*/
public static function header(string $string, bool $replace = true, ?int $http_response_code = null): void
public static function header(string $string, bool $replace = true, ?int $http_response_code = 0): void
{
if (!str_starts_with($string, 'HTTP/')) {
$header = strtolower(explode(':', $string, 2)[0]);
Expand All @@ -71,7 +73,7 @@ public static function header(string $string, bool $replace = true, ?int $http_r
} else {
self::$rawHttpHeader = $string;
}
if ($http_response_code !== null) {
if ($http_response_code !== 0) {
self::$responseCode = $http_response_code;
}
}
Expand Down Expand Up @@ -107,9 +109,9 @@ public static function headers_list(): array
/**
* Get or Set the HTTP response code
*/
public static function http_response_code(?int $response_code = null): int
public static function http_response_code(?int $response_code = 0): int
{
if ($response_code !== null) {
if ($response_code !== 0) {
self::$responseCode = $response_code;
}
return self::$responseCode;
Expand All @@ -127,4 +129,19 @@ public static function rawHttpHeader(): string
{
return self::$rawHttpHeader;
}

public static function getHeader(string $header): array
{
return self::$headers[strtolower($header)] ?? [];
}

public static function flush(): void
{
self::$flushedTimes++;
}

public static function getFlushTimes(): int
{
return self::$flushedTimes;
}
}
2 changes: 0 additions & 2 deletions tests/Support/Emitter/HTTPFunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Yiisoft\Yii\Runner\Http\Tests\Support\Emitter;

include 'httpFunctionMocks.php';

use PHPUnit\Framework\TestCase;
use Yiisoft\Http\Status;

Expand Down
49 changes: 0 additions & 49 deletions tests/Support/Emitter/httpFunctionMocks.php

This file was deleted.

Loading

0 comments on commit e635091

Please sign in to comment.