├── CHANGELOG.md ├── composer-require-checker.json ├── src ├── EmitterInterface.php ├── FakeEmitter.php ├── HeadersHaveBeenSentException.php ├── EmitterMiddleware.php └── SapiEmitter.php ├── rector.php ├── LICENSE.md ├── composer.json └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii PSR Emitter Change Log 2 | 3 | ## 1.0.2 under development 4 | 5 | - no changes in this release. 6 | 7 | ## 1.0.1 December 20, 2025 8 | 9 | - Enh #7: Add PHP 8.5 support (@vjik) 10 | 11 | ## 1.0.0 May 21, 2025 12 | 13 | - Initial release. 14 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "Psr\\Http\\Server\\MiddlewareInterface", 4 | "Psr\\Http\\Server\\RequestHandlerInterface" 5 | ], 6 | "php-core-extensions": [ 7 | "Core", 8 | "date", 9 | "json", 10 | "hash", 11 | "pcre", 12 | "Phar", 13 | "Reflection", 14 | "SPL", 15 | "random", 16 | "standard" 17 | ], 18 | "scan-files": [] 19 | } 20 | -------------------------------------------------------------------------------- /src/EmitterInterface.php: -------------------------------------------------------------------------------- 1 | response = $response; 19 | } 20 | 21 | /** 22 | * @return ResponseInterface|null The last emitted response or `null` if no response has been emitted. 23 | */ 24 | public function getLastResponse(): ?ResponseInterface 25 | { 26 | return $this->response; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HeadersHaveBeenSentException.php: -------------------------------------------------------------------------------- 1 | withPaths([ 14 | __DIR__ . '/src', 15 | __DIR__ . '/tests', 16 | ]) 17 | ->withPhpSets(php81: true) 18 | ->withRules([ 19 | InlineConstructorDefaultToPropertyRector::class, 20 | ]) 21 | ->withSkip([ 22 | ClosureToArrowFunctionRector::class, 23 | ReadOnlyPropertyRector::class, 24 | NullToStrictStringFuncCallArgRector::class, 25 | ArrowFunctionDelegatingCallToFirstClassCallableRector::class => [ 26 | 'tests/Support/InternalMocker/MockerExtension.php', 27 | ], 28 | ]); 29 | -------------------------------------------------------------------------------- /src/EmitterMiddleware.php: -------------------------------------------------------------------------------- 1 | handle($request); 31 | $this->emitter->emit($response); 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/psr-emitter", 3 | "type": "library", 4 | "description": "PSR-7 HTTP response emitter", 5 | "keywords": [ 6 | "psr-7", 7 | "emitter", 8 | "http" 9 | ], 10 | "homepage": "https://www.yiiframework.com/", 11 | "license": "BSD-3-Clause", 12 | "support": { 13 | "issues": "https://github.com/yiisoft/psr-emitter/issues?state=open", 14 | "source": "https://github.com/yiisoft/psr-emitter", 15 | "forum": "https://www.yiiframework.com/forum/", 16 | "wiki": "https://www.yiiframework.com/wiki/", 17 | "irc": "ircs://irc.libera.chat:6697/yii", 18 | "chat": "https://t.me/yii3en" 19 | }, 20 | "funding": [ 21 | { 22 | "type": "opencollective", 23 | "url": "https://opencollective.com/yiisoft" 24 | }, 25 | { 26 | "type": "github", 27 | "url": "https://github.com/sponsors/yiisoft" 28 | } 29 | ], 30 | "require": { 31 | "php": "8.1 - 8.5", 32 | "psr/http-message": "^2.0", 33 | "yiisoft/friendly-exception": "^1.1" 34 | }, 35 | "require-dev": { 36 | "httpsoft/http-message": "^1.1.6", 37 | "maglnet/composer-require-checker": "^4.7.1", 38 | "phpunit/phpunit": "^10.5.46", 39 | "psr/http-server-middleware": "^1.0.2", 40 | "rector/rector": "^2.0.16", 41 | "roave/infection-static-analysis-plugin": "^1.35", 42 | "spatie/phpunit-watcher": "^1.24", 43 | "vimeo/psalm": "^5.26.1 || ^6.10.3", 44 | "xepozz/internal-mocker": "^1.4.1" 45 | }, 46 | "suggest": { 47 | "psr/http-server-middleware": "To use emitter as middleware." 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "Yiisoft\\PsrEmitter\\": "src" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "psr-4": { 56 | "Yiisoft\\PsrEmitter\\Tests\\": "tests" 57 | } 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "infection/extension-installer": true, 63 | "composer/package-versions-deprecated": true 64 | } 65 | }, 66 | "scripts": { 67 | "test": "php -ddisable_functions=headers_sent,header,header_remove,flush ./vendor/bin/phpunit --testdox", 68 | "test-watch": "phpunit-watcher watch" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/SapiEmitter.php: -------------------------------------------------------------------------------- 1 | bufferSize = $bufferSize ?? self::DEFAULT_BUFFER_SIZE; 38 | } 39 | 40 | public function emit(ResponseInterface $response): void 41 | { 42 | $this->emitHeaders($response); 43 | 44 | /** 45 | * Sends headers before the body. 46 | * Makes a client possible to recognize the type of the body content if it is sent with a delay, 47 | * for instance, for a streamed response. 48 | */ 49 | flush(); 50 | 51 | $this->emitBody($response); 52 | } 53 | 54 | /** 55 | * @throws HeadersHaveBeenSentException If headers have already been sent. 56 | */ 57 | private function emitHeaders(ResponseInterface $response): void 58 | { 59 | // We can't send headers if they are already sent 60 | if (headers_sent()) { 61 | throw new HeadersHaveBeenSentException(); 62 | } 63 | 64 | header_remove(); 65 | 66 | $headers = $response->getHeaders(); 67 | 68 | // Send headers 69 | foreach ($headers as $header => $values) { 70 | foreach ($values as $value) { 71 | header("$header: $value", false); 72 | } 73 | } 74 | 75 | // Send HTTP Status-Line (must be sent after the headers) 76 | $status = $response->getStatusCode(); 77 | header( 78 | sprintf( 79 | 'HTTP/%s %d %s', 80 | $response->getProtocolVersion(), 81 | $status, 82 | $response->getReasonPhrase(), 83 | ), 84 | true, 85 | $status 86 | ); 87 | } 88 | 89 | private function emitBody(ResponseInterface $response): void 90 | { 91 | $level = ob_get_level(); 92 | $body = $response->getBody(); 93 | if (!$body->isReadable()) { 94 | return; 95 | } 96 | 97 | if ($body->isSeekable()) { 98 | $body->rewind(); 99 | } 100 | 101 | $size = $body->getSize(); 102 | if ($size !== null && $size <= $this->bufferSize) { 103 | $this->emitContent($body->getContents(), $level); 104 | return; 105 | } 106 | 107 | while (!$body->eof()) { 108 | $this->emitContent($body->read($this->bufferSize), $level); 109 | } 110 | } 111 | 112 | private function emitContent(string $content, int $level): void 113 | { 114 | if ($content === '') { 115 | while (ob_get_level() > $level) { 116 | ob_end_flush(); 117 | } 118 | return; 119 | } 120 | 121 | echo $content; 122 | 123 | // flush the output buffer and send echoed messages to the browser 124 | while (ob_get_level() > $level) { 125 | ob_end_flush(); 126 | } 127 | flush(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii PSR Emitter

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/psr-emitter/v)](https://packagist.org/packages/yiisoft/psr-emitter) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/psr-emitter/downloads)](https://packagist.org/packages/yiisoft/psr-emitter) 11 | [![Build status](https://github.com/yiisoft/psr-emitter/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/yiisoft/psr-emitter/actions/workflows/build.yml?query=branch%3Amaster) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/psr-emitter/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/psr-emitter) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fpsr-emitter%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/psr-emitter/master) 14 | [![Static analysis](https://github.com/yiisoft/psr-emitter/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/yiisoft/psr-emitter/actions/workflows/static.yml?query=branch%3Amaster) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/psr-emitter/coverage.svg)](https://shepherd.dev/github/yiisoft/psr-emitter) 16 | [![psalm-level](https://shepherd.dev/github/yiisoft/psr-emitter/level.svg)](https://shepherd.dev/github/yiisoft/psr-emitter) 17 | 18 | The package provides `EmitterInterface` that is responsible for sending PSR-7 HTTP responses as well as several implementations of the interface: 19 | 20 | - `SapiEmitter` - sends a response using standard PHP Server API; 21 | - `FakeEmiiter` - a fake emitter that does nothing, except for capturing response (useful for testing purposes). 22 | 23 | Additionally, the package provides `EmitterMiddleware` PSR-15 middleware that can be used in an application to send 24 | a response by any `EmitterInterface` implementation. 25 | 26 | ## Requirements 27 | 28 | - PHP 8.1 - 8.5. 29 | 30 | ## Installation 31 | 32 | The package could be installed with [Composer](https://getcomposer.org): 33 | 34 | ```shell 35 | composer require yiisoft/psr-emitter 36 | ``` 37 | 38 | ## General usage 39 | 40 | Create emitter instance and call `emit()` method with a PSR-7 response: 41 | 42 | ```php 43 | use Psr\Http\Message\ResponseInterface; 44 | use Yiisoft\PsrEmitter\SapiEmitter; 45 | 46 | /** @var Response $response */ 47 | 48 | $emitter = new SapiEmitter(); 49 | $emitter->emit($response); 50 | ``` 51 | 52 | You can customize the buffer size (by default, 8MB) for large response bodies: 53 | 54 | ```php 55 | use Psr\Http\Message\ResponseInterface; 56 | use Yiisoft\PsrEmitter\SapiEmitter; 57 | 58 | /** @var Response $response */ 59 | 60 | $emitter = new SapiEmitter(4_194_304); // Buffer size is 4MB 61 | 62 | // Response content will be sent in chunks of 4MB 63 | $emitter->emit($response); 64 | ``` 65 | 66 | ## Documentation 67 | 68 | - [Internals](docs/internals.md) 69 | 70 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place 71 | for that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 72 | 73 | ## License 74 | 75 | The Yii PSR Emitter is free software. It is released under the terms of the BSD License. 76 | Please see [`LICENSE`](./LICENSE.md) for more information. 77 | 78 | Maintained by [Yii Software](https://www.yiiframework.com/). 79 | 80 | ## Support the project 81 | 82 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 83 | 84 | ## Follow updates 85 | 86 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 87 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 88 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 89 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 90 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 91 | --------------------------------------------------------------------------------