Passed
Push — master ( d9ab21...aaf2ee )
by Evgeniy
08:13
created

SapiEmitter::emitBody()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 6
nop 1
dl 0
loc 22
ccs 14
cts 14
cp 1
crap 6
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace HttpSoft\Emitter;
6
7
use HttpSoft\Emitter\Exception\HeadersAlreadySentException;
8
use HttpSoft\Emitter\Exception\OutputAlreadySentException;
9
use InvalidArgumentException;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\StreamInterface;
12
13
use function flush;
14
use function ob_get_length;
15
use function ob_get_level;
16
use function preg_match;
17
use function sprintf;
18
use function strlen;
19
use function str_replace;
20
use function strtolower;
21
use function trim;
22
use function ucwords;
23
24
final class SapiEmitter implements EmitterInterface
25
{
26
    /**
27
     * @var int|null
28
     */
29
    private ?int $bufferLength;
30
31
    /**
32
     * @param int|null $bufferLength
33
     * @throws InvalidArgumentException if buffer length is integer type and less than or one.
34
     */
35 23
    public function __construct(int $bufferLength = null)
36
    {
37 23
        if ($bufferLength !== null && $bufferLength < 1) {
38 1
            throw new InvalidArgumentException(sprintf(
39 1
                'Buffer length for `%s` must be greater than zero; received `%d`.',
40 1
                self::class,
41 1
                $bufferLength
42
            ));
43
        }
44
45 23
        $this->bufferLength = $bufferLength;
46 23
    }
47
48
    /**
49
     * {@inheritDoc}
50
     *
51
     * @throws HeadersAlreadySentException if headers already sent.
52
     * @throws OutputAlreadySentException if output has been emitted previously.
53
     */
54 8
    public function emit(ResponseInterface $response, bool $withoutBody = false): void
55
    {
56 8
        if (headers_sent()) {
57
            throw HeadersAlreadySentException::create();
58
        }
59
60 8
        if (ob_get_level() > 0 && ob_get_length() > 0) {
61
            throw OutputAlreadySentException::create();
62
        }
63
64 8
        $this->emitHeaders($response);
65 8
        $this->emitStatusLine($response);
66
67 8
        if (!$withoutBody && $response->getBody()->isReadable()) {
68 6
            $this->emitBody($response);
69
        }
70 8
    }
71
72
    /**
73
     * Loops through and emits each header as specified to `Psr\Http\Message\MessageInterface::getHeaders()`.
74
     *
75
     * @param ResponseInterface $response
76
     */
77 8
    private function emitHeaders(ResponseInterface $response): void
78
    {
79 8
        foreach ($response->getHeaders() as $name => $values) {
80 6
            $name = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', (string) $name))));
81 6
            $firstReplace = ($name === 'Set-Cookie') ? false : true;
82
83 6
            foreach ($values as $value) {
84 6
                header("{$name}: {$value}", $firstReplace);
85 6
                $firstReplace = false;
86
            }
87
        }
88 8
    }
89
90
    /**
91
     * Emits the response status line.
92
     *
93
     * @param ResponseInterface $response
94
     * @psalm-suppress RedundantCastGivenDocblockType
95
     */
96 8
    private function emitStatusLine(ResponseInterface $response): void
97
    {
98 8
        $statusCode = (int) $response->getStatusCode();
99 8
        $reasonPhrase = trim((string) $response->getReasonPhrase());
100 8
        $protocolVersion = trim((string) $response->getProtocolVersion());
101
102 8
        $status = $statusCode . (!$reasonPhrase ? '' : " {$reasonPhrase}");
103 8
        header("HTTP/{$protocolVersion} {$status}", true, $statusCode);
104 8
    }
105
106
    /**
107
     * Emits the message body.
108
     *
109
     * @param ResponseInterface $response
110
     * @psalm-suppress MixedArgument
111
     */
112 6
    private function emitBody(ResponseInterface $response): void
113
    {
114 6
        if ($this->bufferLength === null) {
115 3
            echo $response->getBody();
116 3
            return;
117
        }
118
119 3
        flush();
120 3
        $body = $response->getBody();
121 3
        $range = $this->parseContentRange($response->getHeaderLine('content-range'));
122
123 3
        if (isset($range['unit']) && $range['unit'] === 'bytes') {
124 1
            $this->emitBodyRange($body, $range['first'], $range['last']);
125 1
            return;
126
        }
127
128 2
        if ($body->isSeekable()) {
129 2
            $body->rewind();
130
        }
131
132 2
        while (!$body->eof()) {
133 2
            echo $body->read($this->bufferLength);
134
        }
135 2
    }
136
137
    /**
138
     * Emits a range of the message body.
139
     *
140
     * @param StreamInterface $body
141
     * @param int $first
142
     * @param int $last
143
     * @psalm-suppress PossiblyNullArgument
144
     */
145 1
    private function emitBodyRange(StreamInterface $body, int $first, int $last): void
146
    {
147 1
        $length = $last - $first + 1;
148
149 1
        if ($body->isSeekable()) {
150 1
            $body->seek($first);
151
        }
152
153 1
        while ($length >= $this->bufferLength && !$body->eof()) {
154 1
            $contents = $body->read($this->bufferLength);
155 1
            $length -= strlen($contents);
156 1
            echo $contents;
157
        }
158
159 1
        if ($length > 0 && !$body->eof()) {
160
            echo $body->read($length);
161
        }
162 1
    }
163
164
    /**
165
     * Parse Content-Range header.
166
     *
167
     * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
168
     * @param string $header
169
     * @return array|null
170
     */
171 3
    private function parseContentRange(string $header): ?array
172
    {
173 3
        if (preg_match('/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/', $header, $matches)) {
174
            return [
175 1
                'unit' => $matches['unit'],
176 1
                'first' => (int) $matches['first'],
177 1
                'last' => (int) $matches['last'],
178 1
                'length' => ($matches['length'] === '*') ? '*' : (int) $matches['length'],
179
            ];
180
        }
181
182 2
        return null;
183
    }
184
}
185