├── 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 |
4 |
5 |
Yii PSR Emitter
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/psr-emitter)
10 | [](https://packagist.org/packages/yiisoft/psr-emitter)
11 | [](https://github.com/yiisoft/psr-emitter/actions/workflows/build.yml?query=branch%3Amaster)
12 | [](https://codecov.io/gh/yiisoft/psr-emitter)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/psr-emitter/master)
14 | [](https://github.com/yiisoft/psr-emitter/actions/workflows/static.yml?query=branch%3Amaster)
15 | [](https://shepherd.dev/github/yiisoft/psr-emitter)
16 | [](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 | [](https://opencollective.com/yiisoft)
83 |
84 | ## Follow updates
85 |
86 | [](https://www.yiiframework.com/)
87 | [](https://twitter.com/yiiframework)
88 | [](https://t.me/yii3en)
89 | [](https://www.facebook.com/groups/yiitalk)
90 | [](https://yiiframework.com/go/slack)
91 |
--------------------------------------------------------------------------------