Passed
Push — master ( 04bcce...6d4f43 )
by Alexander
03:07 queued 42s
created

SapiEmitter::shouldOutputBody()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 10.1359

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 22
ccs 9
cts 15
cp 0.6
rs 8.8333
cc 7
nc 6
nop 1
crap 10.1359
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web;
6
7
use InvalidArgumentException;
8
use Psr\Http\Message\ResponseInterface;
9
use Yiisoft\Http\Status;
10
use Yiisoft\Yii\Web\Exception\HeadersHaveBeenSentException;
11
use function flush;
12
use function in_array;
13
use function sprintf;
14
use function strtolower;
15
16
/**
17
 * SapiEmitter sends a response using PHP Server API
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\Web\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 15
    public function __construct(int $bufferSize = null)
35
    {
36 15
        if ($bufferSize !== null && $bufferSize <= 0) {
37
            throw new InvalidArgumentException('Buffer size must be greater than zero');
38
        }
39 15
        $this->bufferSize = $bufferSize ?? self::DEFAULT_BUFFER_SIZE;
40 15
    }
41
42
    /**
43
     * Respond to the client with headers and body.
44
     *
45
     * @param ResponseInterface $response Response object to send.
46
     * @param bool $withoutBody If body should be ignored.
47
     *
48
     * @throws HeadersHaveBeenSentException
49
     *
50
     * @return bool
51
     */
52 15
    public function emit(ResponseInterface $response, bool $withoutBody = false): bool
53
    {
54 15
        $status = $response->getStatusCode();
55 15
        $withoutBody = $withoutBody || !$this->shouldOutputBody($response);
56 15
        $withoutContentLength = $withoutBody || $response->hasHeader('Transfer-Encoding');
57 15
        if ($withoutContentLength) {
58 9
            $response = $response->withoutHeader('Content-Length');
59
        }
60
61
        // we can't send headers if they are already sent
62 15
        if (headers_sent()) {
63 1
            throw new HeadersHaveBeenSentException();
64
        }
65 14
        header_remove();
66
        // send HTTP Status-Line
67 14
        header(sprintf(
68 14
            'HTTP/%s %d %s',
69 14
            $response->getProtocolVersion(),
70 14
            $status,
71 14
            $response->getReasonPhrase()
72 14
        ), true, $status);
73
        // send headers
74 14
        foreach ($response->getHeaders() as $header => $values) {
75 13
            $replaceFirst = strtolower($header) !== 'set-cookie';
76 13
            foreach ($values as $value) {
77 13
                header("{$header}: {$value}", $replaceFirst);
78 13
                $replaceFirst = false;
79
            }
80
        }
81
82 14
        if (!$withoutBody) {
83 5
            if (!$withoutContentLength && !$response->hasHeader('Content-Length')) {
84 3
                $contentLength = $response->getBody()->getSize();
85 3
                if ($contentLength !== null) {
86 3
                    header("Content-Length: {$contentLength}", true);
87
                }
88
            }
89
90 5
            $this->emitBody($response);
91
        }
92
93 14
        return true;
94
    }
95
96 5
    private function emitBody(ResponseInterface $response): void
97
    {
98 5
        $body = $response->getBody();
99 5
        if ($body->isSeekable()) {
100 5
            $body->rewind();
101
        }
102 5
        while (!$body->eof()) {
103 5
            echo $body->read($this->bufferSize);
104 5
            flush();
105
        }
106 5
    }
107
108 14
    private function shouldOutputBody(ResponseInterface $response): bool
109
    {
110 14
        if (in_array($response->getStatusCode(), self::NO_BODY_RESPONSE_CODES, true)) {
111 6
            return false;
112
        }
113
        // check if body is empty
114 8
        $body = $response->getBody();
115 8
        if (!$body->isReadable()) {
116 1
            return false;
117
        }
118 7
        $size = $body->getSize();
119 7
        if ($size !== null) {
120 7
            return $size > 0;
121
        }
122
        if ($body->isSeekable()) {
123
            $body->rewind();
124
            $byte = $body->read(1);
125
            if ($byte === '' || $body->eof()) {
126
                return false;
127
            }
128
        }
129
        return true;
130
    }
131
}
132