Issues (7)

src/SapiEmitter.php (1 issue)

Labels
Severity
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
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ';' on line 31 at column 41
Loading history...
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