Passed
Pull Request — master (#50)
by Dmitriy
02:23
created

SapiEmitter::emitBody()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0113

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 5
eloc 12
c 3
b 0
f 1
nc 8
nop 1
dl 0
loc 20
ccs 12
cts 13
cp 0.9231
crap 5.0113
rs 9.5555
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
The constant Yiisoft\Yii\Runner\Http\8_388_608 was not found. Maybe you did not declare it correctly or list all dependencies?
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 25
    public function __construct(int $bufferSize = null)
38
    {
39 25
        if ($bufferSize !== null && $bufferSize < 1) {
40 1
            throw new InvalidArgumentException('Buffer size must be greater than zero.');
41
        }
42
43 24
        $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 24
    public function emit(ResponseInterface $response, bool $withoutBody = false): void
55
    {
56 24
        $status = $response->getStatusCode();
57 24
        $withoutBody = $withoutBody || !$this->shouldOutputBody($response);
58 24
        $withoutContentLength = $withoutBody || $response->hasHeader('Transfer-Encoding');
59
60 24
        if ($withoutContentLength) {
61 11
            $response = $response->withoutHeader('Content-Length');
62
        }
63
64
        // We can't send headers if they are already sent.
65 24
        if (headers_sent()) {
66 1
            throw new HeadersHaveBeenSentException();
67
        }
68
69 23
        header_remove();
70
71
        // Send headers.
72 23
        foreach ($response->getHeaders() as $header => $values) {
73 19
            foreach ($values as $value) {
74 19
                header("$header: $value", false);
75
            }
76
        }
77
78
        // Send HTTP Status-Line (must be sent after the headers).
79 23
        header(sprintf(
80 23
            'HTTP/%s %d %s',
81 23
            $response->getProtocolVersion(),
82 23
            $status,
83 23
            $response->getReasonPhrase(),
84 23
        ), true, $status);
85
86 23
        if ($withoutBody) {
87 11
            return;
88
        }
89
90
        // Adds a `Content-Length` header if a body exists, and it has not been added before.
91 12
        if (!$withoutContentLength && !$response->hasHeader('Content-Length')) {
92 10
            $contentLength = $response
93 10
                ->getBody()
94 10
                ->getSize();
95
96 10
            if ($contentLength !== null) {
97 9
                header("Content-Length: $contentLength", true);
98
            }
99
        }
100
101 12
        $this->emitBody($response);
102
    }
103
104 12
    private function emitBody(ResponseInterface $response): void
105
    {
106 12
        $body = $response->getBody();
107
108 12
        if ($body->isSeekable()) {
109 11
            $body->rewind();
110
        }
111
112 12
        $level = ob_get_level();
113 12
        while (!$body->eof()) {
114 11
            $output = $body->read($this->bufferSize);
115 11
            if ($output === '') {
116 1
                continue;
117
            }
118 11
            echo $output;
119
            // flush the output buffer and send echoed messages to the browser
120 11
            while (ob_get_level() > $level) {
121
                ob_end_flush();
122
            }
123 11
            flush();
124
        }
125
    }
126
127 23
    private function shouldOutputBody(ResponseInterface $response): bool
128
    {
129 23
        if (in_array($response->getStatusCode(), self::NO_BODY_RESPONSE_CODES, true)) {
130 6
            return false;
131
        }
132
133 17
        $body = $response->getBody();
134
135 17
        if (!$body->isReadable()) {
136 1
            return false;
137
        }
138
139 16
        $size = $body->getSize();
140
141 16
        if ($size !== null) {
142 14
            return $size > 0;
143
        }
144
145 2
        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 1
        return true;
155
    }
156
}
157