1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Yii\Runner\Http; |
||
6 | |||
7 | use InvalidArgumentException; |
||
8 | use Psr\Http\Message\ResponseInterface; |
||
9 | use Yiisoft\Http\Status; |
||
10 | use Yiisoft\Yii\Runner\Http\Exception\HeadersHaveBeenSentException; |
||
11 | |||
12 | use function flush; |
||
13 | use function headers_sent; |
||
14 | use function in_array; |
||
15 | use function sprintf; |
||
16 | |||
17 | /** |
||
18 | * `SapiEmitter` sends a response using standard PHP Server API i.e. with {@see header()} and "echo". |
||
19 | */ |
||
20 | final class SapiEmitter |
||
21 | { |
||
22 | private const NO_BODY_RESPONSE_CODES = [ |
||
23 | Status::CONTINUE, |
||
24 | Status::SWITCHING_PROTOCOLS, |
||
25 | Status::PROCESSING, |
||
26 | Status::NO_CONTENT, |
||
27 | Status::RESET_CONTENT, |
||
28 | Status::NOT_MODIFIED, |
||
29 | ]; |
||
30 | |||
31 | private const DEFAULT_BUFFER_SIZE = 8_388_608; // 8MB |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
32 | |||
33 | private int $bufferSize; |
||
34 | |||
35 | /** |
||
36 | * @param int|null $bufferSize The size of the buffer in bytes to send the content of the message body. |
||
37 | 27 | */ |
|
38 | public function __construct(int $bufferSize = null) |
||
39 | 27 | { |
|
40 | 1 | if ($bufferSize !== null && $bufferSize < 1) { |
|
41 | throw new InvalidArgumentException('Buffer size must be greater than zero.'); |
||
42 | } |
||
43 | 26 | ||
44 | $this->bufferSize = $bufferSize ?? self::DEFAULT_BUFFER_SIZE; |
||
45 | } |
||
46 | |||
47 | /** |
||
48 | * Responds to the client with headers and body. |
||
49 | * |
||
50 | * @param ResponseInterface $response Response object to send. |
||
51 | * @param bool $withoutBody If body should be ignored. |
||
52 | * |
||
53 | * @throws HeadersHaveBeenSentException |
||
54 | 26 | */ |
|
55 | public function emit(ResponseInterface $response, bool $withoutBody = false): void |
||
56 | 26 | { |
|
57 | 26 | $status = $response->getStatusCode(); |
|
58 | 26 | $withoutBody = $withoutBody || !$this->shouldOutputBody($response); |
|
59 | $withoutContentLength = $withoutBody || $response->hasHeader('Transfer-Encoding'); |
||
60 | 26 | ||
61 | 12 | if ($withoutContentLength) { |
|
62 | $response = $response->withoutHeader('Content-Length'); |
||
63 | } |
||
64 | |||
65 | 26 | // We can't send headers if they are already sent. |
|
66 | 1 | if (headers_sent()) { |
|
67 | throw new HeadersHaveBeenSentException(); |
||
68 | } |
||
69 | 25 | ||
70 | header_remove(); |
||
71 | |||
72 | 25 | // Send headers. |
|
73 | 21 | foreach ($response->getHeaders() as $header => $values) { |
|
74 | 21 | foreach ($values as $value) { |
|
75 | header("$header: $value", false); |
||
76 | } |
||
77 | } |
||
78 | |||
79 | 25 | // Send HTTP Status-Line (must be sent after the headers). |
|
80 | 25 | header(sprintf( |
|
81 | 25 | 'HTTP/%s %d %s', |
|
82 | 25 | $response->getProtocolVersion(), |
|
83 | 25 | $status, |
|
84 | 25 | $response->getReasonPhrase(), |
|
85 | ), true, $status); |
||
86 | 25 | ||
87 | 12 | if ($withoutBody) { |
|
88 | return; |
||
89 | } |
||
90 | |||
91 | 13 | // Adds a `Content-Length` header if a body exists, and it has not been added before. |
|
92 | 11 | if (!$withoutContentLength && !$response->hasHeader('Content-Length')) { |
|
93 | 11 | $contentLength = $response |
|
94 | 11 | ->getBody() |
|
95 | ->getSize(); |
||
96 | 11 | ||
97 | 9 | if ($contentLength !== null) { |
|
98 | header("Content-Length: $contentLength", true); |
||
99 | } |
||
100 | } |
||
101 | 13 | ||
102 | /** |
||
103 | * Sends headers before the body. |
||
104 | 13 | * Makes a client possible to recognize the type of the body content if it is sent with a delay, |
|
105 | * for instance, for a streamed response. |
||
106 | 13 | */ |
|
107 | flush(); |
||
108 | 13 | ||
109 | 11 | $this->emitBody($response); |
|
110 | } |
||
111 | |||
112 | 13 | private function emitBody(ResponseInterface $response): void |
|
113 | 13 | { |
|
114 | 12 | $body = $response->getBody(); |
|
115 | 12 | ||
116 | 1 | if ($body->isSeekable()) { |
|
117 | $body->rewind(); |
||
118 | 12 | } |
|
119 | |||
120 | 12 | $level = ob_get_level(); |
|
121 | 1 | ||
122 | $size = $body->getSize(); |
||
123 | 12 | if ($size !== null && $size <= $this->bufferSize) { |
|
124 | $this->emitContent($body->getContents(), $level); |
||
125 | return; |
||
126 | } |
||
127 | 25 | ||
128 | while (!$body->eof()) { |
||
129 | 25 | $this->emitContent($body->read($this->bufferSize), $level); |
|
130 | 6 | } |
|
131 | } |
||
132 | |||
133 | 19 | private function emitContent(string $content, int $level): void |
|
134 | { |
||
135 | 19 | if ($content === '') { |
|
136 | 1 | while (ob_get_level() > $level) { |
|
137 | ob_end_flush(); |
||
138 | } |
||
139 | 18 | return; |
|
140 | } |
||
141 | 18 | ||
142 | 15 | echo $content; |
|
143 | |||
144 | // flush the output buffer and send echoed messages to the browser |
||
145 | 3 | while (ob_get_level() > $level) { |
|
146 | 1 | ob_end_flush(); |
|
147 | 1 | } |
|
148 | flush(); |
||
149 | 1 | } |
|
150 | 1 | ||
151 | private function shouldOutputBody(ResponseInterface $response): bool |
||
152 | { |
||
153 | if (in_array($response->getStatusCode(), self::NO_BODY_RESPONSE_CODES, true)) { |
||
154 | 2 | return false; |
|
155 | } |
||
156 | |||
157 | $body = $response->getBody(); |
||
158 | |||
159 | if (!$body->isReadable()) { |
||
160 | return false; |
||
161 | } |
||
162 | |||
163 | $size = $body->getSize(); |
||
164 | |||
165 | if ($size !== null) { |
||
166 | return $size > 0; |
||
167 | } |
||
168 | |||
169 | if ($body->isSeekable()) { |
||
170 | $body->rewind(); |
||
171 | $byte = $body->read(1); |
||
172 | |||
173 | if ($byte === '' || $body->eof()) { |
||
174 | return false; |
||
175 | } |
||
176 | } |
||
177 | |||
178 | return true; |
||
179 | } |
||
180 | } |
||
181 |