Passed
Pull Request — master (#50)
by Dmitriy
14:22 queued 02:01
created

SapiEmitter   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 136
Duplicated Lines 0 %

Test Coverage

Coverage 98.31%

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 64
c 3
b 0
f 1
dl 0
loc 136
ccs 58
cts 59
cp 0.9831
rs 10
wmc 26

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 3
B emit() 0 48 11
A emitBody() 0 20 5
B shouldOutputBody() 0 28 7
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 26
    public function __construct(int $bufferSize = null)
38
    {
39 26
        if ($bufferSize !== null && $bufferSize < 1) {
40 1
            throw new InvalidArgumentException('Buffer size must be greater than zero.');
41
        }
42
43 25
        $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 25
    public function emit(ResponseInterface $response, bool $withoutBody = false): void
55
    {
56 25
        $status = $response->getStatusCode();
57 25
        $withoutBody = $withoutBody || !$this->shouldOutputBody($response);
58 25
        $withoutContentLength = $withoutBody || $response->hasHeader('Transfer-Encoding');
59
60 25
        if ($withoutContentLength) {
61 12
            $response = $response->withoutHeader('Content-Length');
62
        }
63
64
        // We can't send headers if they are already sent.
65 25
        if (headers_sent()) {
66 1
            throw new HeadersHaveBeenSentException();
67
        }
68
69 24
        header_remove();
70
71
        // Send headers.
72 24
        foreach ($response->getHeaders() as $header => $values) {
73 20
            foreach ($values as $value) {
74 20
                header("$header: $value", false);
75
            }
76
        }
77
78
        // Send HTTP Status-Line (must be sent after the headers).
79 24
        header(sprintf(
80 24
            'HTTP/%s %d %s',
81 24
            $response->getProtocolVersion(),
82 24
            $status,
83 24
            $response->getReasonPhrase(),
84 24
        ), true, $status);
85
86 24
        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 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 24
    private function shouldOutputBody(ResponseInterface $response): bool
128
    {
129 24
        if (in_array($response->getStatusCode(), self::NO_BODY_RESPONSE_CODES, true)) {
130 6
            return false;
131
        }
132
133 18
        $body = $response->getBody();
134
135 18
        if (!$body->isReadable()) {
136 1
            return false;
137
        }
138
139 17
        $size = $body->getSize();
140
141 17
        if ($size !== null) {
142 15
            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