Passed
Push — master ( 07783e...f235e2 )
by Alexander
02:34
created

SapiEmitter::shouldOutputBody()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
eloc 14
c 1
b 0
f 1
nc 6
nop 1
dl 0
loc 28
ccs 15
cts 15
cp 1
crap 7
rs 8.8333
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 in_array;
14
use function sprintf;
15
16
/**
17
 * `SapiEmitter` sends a response using standard PHP Server API i.e. with {@see header()} and "echo".
18
 */
19
final class SapiEmitter
20
{
21
    private const NO_BODY_RESPONSE_CODES = [
22
        Status::CONTINUE,
23
        Status::SWITCHING_PROTOCOLS,
24
        Status::PROCESSING,
25
        Status::NO_CONTENT,
26
        Status::RESET_CONTENT,
27
        Status::NOT_MODIFIED,
28
    ];
29
30
    private const DEFAULT_BUFFER_SIZE = 8_388_608; // 8MB
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ';' on line 30 at column 41
Loading history...
31
32
    private int $bufferSize;
33
34
    /**
35
     * @param int|null $bufferSize The size of the buffer in bytes to send the content of the message body.
36
     */
37 27
    public function __construct(int $bufferSize = null)
38
    {
39 27
        if ($bufferSize !== null && $bufferSize < 1) {
40 1
            throw new InvalidArgumentException('Buffer size must be greater than zero.');
41
        }
42
43 26
        $this->bufferSize = $bufferSize ?? self::DEFAULT_BUFFER_SIZE;
44
    }
45
46
    /**
47
     * Responds to the client with headers and body.
48
     *
49
     * @param ResponseInterface $response Response object to send.
50
     * @param bool $withoutBody If body should be ignored.
51
     *
52
     * @throws HeadersHaveBeenSentException
53
     */
54 26
    public function emit(ResponseInterface $response, bool $withoutBody = false): void
55
    {
56 26
        $status = $response->getStatusCode();
57 26
        $withoutBody = $withoutBody || !$this->shouldOutputBody($response);
58 26
        $withoutContentLength = $withoutBody || $response->hasHeader('Transfer-Encoding');
59
60 26
        if ($withoutContentLength) {
61 12
            $response = $response->withoutHeader('Content-Length');
62
        }
63
64
        // We can't send headers if they are already sent.
65 26
        if (headers_sent()) {
66 1
            throw new HeadersHaveBeenSentException();
67
        }
68
69 25
        header_remove();
70
71
        // Send headers.
72 25
        foreach ($response->getHeaders() as $header => $values) {
73 21
            foreach ($values as $value) {
74 21
                header("$header: $value", false);
75
            }
76
        }
77
78
        // Send HTTP Status-Line (must be sent after the headers).
79 25
        header(sprintf(
80 25
            'HTTP/%s %d %s',
81 25
            $response->getProtocolVersion(),
82 25
            $status,
83 25
            $response->getReasonPhrase(),
84 25
        ), true, $status);
85
86 25
        if ($withoutBody) {
87 12
            return;
88
        }
89
90
        // Adds a `Content-Length` header if a body exists, and it has not been added before.
91 13
        if (!$withoutContentLength && !$response->hasHeader('Content-Length')) {
92 11
            $contentLength = $response
93 11
                ->getBody()
94 11
                ->getSize();
95
96 11
            if ($contentLength !== null) {
97 9
                header("Content-Length: $contentLength", true);
98
            }
99
        }
100
101 13
        $this->emitBody($response);
102
    }
103
104 13
    private function emitBody(ResponseInterface $response): void
105
    {
106 13
        $body = $response->getBody();
107
108 13
        if ($body->isSeekable()) {
109 11
            $body->rewind();
110
        }
111
112 13
        $level = ob_get_level();
113 13
        while (!$body->eof()) {
114 12
            $output = $body->read($this->bufferSize);
115 12
            if ($output === '') {
116 1
                continue;
117
            }
118 12
            echo $output;
119
            // flush the output buffer and send echoed messages to the browser
120 12
            while (ob_get_level() > $level) {
121 1
                ob_end_flush();
122
            }
123 12
            flush();
124
        }
125
    }
126
127 25
    private function shouldOutputBody(ResponseInterface $response): bool
128
    {
129 25
        if (in_array($response->getStatusCode(), self::NO_BODY_RESPONSE_CODES, true)) {
130 6
            return false;
131
        }
132
133 19
        $body = $response->getBody();
134
135 19
        if (!$body->isReadable()) {
136 1
            return false;
137
        }
138
139 18
        $size = $body->getSize();
140
141 18
        if ($size !== null) {
142 15
            return $size > 0;
143
        }
144
145 3
        if ($body->isSeekable()) {
146 1
            $body->rewind();
147 1
            $byte = $body->read(1);
148
149 1
            if ($byte === '' || $body->eof()) {
150 1
                return false;
151
            }
152
        }
153
154 2
        return true;
155
    }
156
}
157