diff --git a/composer.json b/composer.json index 1e49117..d6b0e1f 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "LaminasTest\\HttpHandlerRunner\\": "test/" }, "files": [ - "test/TestAsset/SapiResponse.php" + "test/TestAsset/HeadersSent.php" ] }, "scripts": { diff --git a/composer.lock b/composer.lock index 994d723..8848243 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6a9664d9468fd40f4cdb94191bee506a", + "content-hash": "e5e5f9dd44cdb31c6a7eb317aac77e30", "packages": [ { "name": "laminas/laminas-diactoros", diff --git a/src/Emitter/SapiEmitterTrait.php b/src/Emitter/SapiEmitterTrait.php index 75693c2..73d4d88 100644 --- a/src/Emitter/SapiEmitterTrait.php +++ b/src/Emitter/SapiEmitterTrait.php @@ -11,6 +11,7 @@ use function function_exists; use function header; use function headers_sent; +use function is_int; use function is_string; use function ob_get_length; use function ob_get_level; @@ -30,8 +31,11 @@ trait SapiEmitterTrait */ private function assertNoPreviousOutput(): void { - if ($this->headersSent()) { - throw EmitterException::forHeadersSent(); + $filename = null; + $line = null; + if ($this->headersSent($filename, $line)) { + assert(is_string($filename) && is_int($line)); + throw EmitterException::forHeadersSent($filename, $line); } if (ob_get_level() > 0 && ob_get_length() > 0) { @@ -99,14 +103,14 @@ private function filterHeader(string $header): string return ucwords($header, '-'); } - private function headersSent(): bool + private function headersSent(?string &$filename = null, ?int &$line = null): bool { if (function_exists('Laminas\HttpHandlerRunner\Emitter\headers_sent')) { // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName - return \Laminas\HttpHandlerRunner\Emitter\headers_sent(); + return \Laminas\HttpHandlerRunner\Emitter\headers_sent($filename, $line); } - return headers_sent(); + return headers_sent($filename, $line); } private function header(string $headerName, bool $replace, int $statusCode): void diff --git a/src/Exception/EmitterException.php b/src/Exception/EmitterException.php index a2f6a9c..79a2f27 100644 --- a/src/Exception/EmitterException.php +++ b/src/Exception/EmitterException.php @@ -6,11 +6,13 @@ use RuntimeException; +use function sprintf; + class EmitterException extends RuntimeException implements ExceptionInterface { - public static function forHeadersSent(): self + public static function forHeadersSent(string $filename, int $line): self { - return new self('Unable to emit response; headers already sent'); + return new self(sprintf('Unable to emit response; headers already sent in %s:%d', $filename, $line)); } public static function forOutputSent(): self diff --git a/test/Emitter/AbstractEmitterTest.php b/test/Emitter/AbstractEmitterTest.php index ec17b3c..a2ff943 100644 --- a/test/Emitter/AbstractEmitterTest.php +++ b/test/Emitter/AbstractEmitterTest.php @@ -6,13 +6,17 @@ use Laminas\Diactoros\Response; use Laminas\HttpHandlerRunner\Emitter\EmitterInterface; +use Laminas\HttpHandlerRunner\Emitter\HeadersSent; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; +use Laminas\HttpHandlerRunner\Exception\EmitterException; use LaminasTest\HttpHandlerRunner\TestAsset\HeaderStack; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use function ob_end_clean; use function ob_start; +use function sprintf; abstract class AbstractEmitterTest extends TestCase { @@ -22,12 +26,14 @@ abstract class AbstractEmitterTest extends TestCase public function setUp(): void { HeaderStack::reset(); + HeadersSent::reset(); $this->emitter = new SapiEmitter(); } public function tearDown(): void { HeaderStack::reset(); + HeadersSent::reset(); } public function testEmitsResponseHeaders(): void @@ -108,4 +114,20 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown(): voi self::assertStringNotContainsString('Content-Length:', $header['header']); } } + + public function testWillThrowEmitterExceptionWhenHeadersAreAlreadySent(): void + { + $sentInLine = __LINE__; + HeadersSent::markSent(__FILE__, $sentInLine); + + $this->expectException(EmitterException::class); + $this->expectExceptionMessage( + sprintf( + 'Unable to emit response; headers already sent in %s:%d', + __FILE__, + $sentInLine + ) + ); + $this->emitter->emit($this->createMock(ResponseInterface::class)); + } } diff --git a/test/TestAsset/HeadersSent.php b/test/TestAsset/HeadersSent.php new file mode 100644 index 0000000..a99b9f7 --- /dev/null +++ b/test/TestAsset/HeadersSent.php @@ -0,0 +1,58 @@ + $headerName, + 'replace' => $replace, + 'status_code' => $httpResponseCode, + ] + ); +} diff --git a/test/TestAsset/SapiResponse.php b/test/TestAsset/SapiResponse.php deleted file mode 100644 index 9245f22..0000000 --- a/test/TestAsset/SapiResponse.php +++ /dev/null @@ -1,30 +0,0 @@ - $headerName, - 'replace' => $replace, - 'status_code' => $httpResponseCode, - ] - ); -}