Passed
Push — develop ( b729b4...acdfb2 )
by nguereza
42:48
created

ResponseEmitter::emitBodyRange()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 16
rs 9.2222
c 0
b 0
f 0
cc 6
nc 8
nop 3
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant PHP
7
 * Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 * Copyright (c) 2020 Evgeniy Zyubin
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file ResponseEmitter.php
35
 *
36
 *  The default response emitter interface
37
 *
38
 *  @package    Platine\Framework\Http\Emitter
39
 *  @author Platine Developers team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   http://www.iacademy.cf
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Framework\Http\Emitter;
50
51
use InvalidArgumentException;
52
use Platine\Framework\Http\Emitter\Exception\HeadersAlreadySentException;
53
use Platine\Framework\Http\Emitter\Exception\OutputAlreadySentException;
54
use Platine\Http\ResponseInterface;
55
use Platine\Http\StreamInterface;
56
57
/**
58
 * ResponseEmitter
59
 * @package Platine\Framework\Http\Emitter
60
 */
61
class ResponseEmitter implements EmitterInterface
62
{
63
64
    /**
65
     * The response before length
66
     * @var int|null
67
     */
68
    protected ?int $bufferLength = null;
69
70
    /**
71
     * Create new instance
72
     * @param int|null $bufferLength
73
     */
74
    public function __construct(?int $bufferLength = null)
75
    {
76
        if ($bufferLength !== null && $bufferLength < 1) {
77
            throw new InvalidArgumentException(sprintf(
78
                'The response buffer length must be greater than zero; received [%d].',
79
                $bufferLength
80
            ));
81
        }
82
83
        $this->bufferLength = $bufferLength;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function emit(ResponseInterface $response, bool $body = true): void
90
    {
91
        if (headers_sent()) {
92
            throw HeadersAlreadySentException::create();
93
        }
94
95
        if (ob_get_level() > 0 && ob_get_length() > 0) {
96
            throw OutputAlreadySentException::create();
97
        }
98
99
        $this->emitHeaders($response);
100
        $this->emitStatusLine($response);
101
102
        if ($body && $response->getBody()->isReadable()) {
103
            $this->emitBody($response);
104
        }
105
    }
106
107
    /**
108
     * Emit headers
109
     * @param ResponseInterface $response
110
     * @return void
111
     */
112
    protected function emitHeaders(ResponseInterface $response): void
113
    {
114
        foreach ($response->getHeaders() as $name => $values) {
115
            $name = str_replace(
116
                ' ',
117
                '-',
118
                ucwords(strtolower(str_replace('-', ' ', $name)))
119
            );
120
121
            $isFirst = $name !== 'Set-Cookie';
122
            foreach ($values as $value) {
123
                $header = sprintf('%s: %s', $name, $value);
124
                header($header, $isFirst);
125
                $isFirst = false;
126
            }
127
        }
128
    }
129
130
    /**
131
     * Emit status line
132
     * @param ResponseInterface $response
133
     * @return void
134
     */
135
    protected function emitStatusLine(ResponseInterface $response): void
136
    {
137
        $code = $response->getStatusCode();
138
        $text = $response->getReasonPhrase();
139
        $protocolVersion = $response->getProtocolVersion();
140
        $status = $code . (!$text ? '' : ' ' . $text);
141
        $header = sprintf('HTTP/%s %s', $protocolVersion, $status);
142
        header($header, true, $code);
143
    }
144
145
    /**
146
     * Emit body
147
     * @param ResponseInterface $response
148
     * @return void
149
     */
150
    protected function emitBody(ResponseInterface $response): void
151
    {
152
        if ($this->bufferLength === null) {
153
            echo $response->getBody();
154
            return;
155
        }
156
157
        flush();
158
        $body = $response->getBody();
159
        $range = $this->parseContentRange(
160
            $response->getHeaderLine('content-range')
161
        );
162
163
        if (isset($range['unit']) && $range['unit'] === 'bytes') {
164
            $this->emitBodyRange($body, $range['first'], $range['last']);
165
            return;
166
        }
167
168
        if ($body->isSeekable()) {
169
            $body->rewind();
170
        }
171
172
        while (!$body->eof()) {
173
            echo $body->read($this->bufferLength);
174
        }
175
    }
176
177
    /**
178
     * Emit body range
179
     * @param StreamInterface $body
180
     * @param int $first
181
     * @param int $last
182
     * @return void
183
     */
184
    protected function emitBodyRange(StreamInterface $body, int $first, int $last): void
185
    {
186
        $length = $last - $first + 1;
187
        if ($body->isSeekable()) {
188
            $body->seek($first);
189
        }
190
191
        while ($length >= $this->bufferLength && !$body->eof()) {
192
            $contents = $body->read($this->bufferLength);
0 ignored issues
show
Bug introduced by
It seems like $this->bufferLength can also be of type null; however, parameter $length of Platine\Http\StreamInterface::read() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
            $contents = $body->read(/** @scrutinizer ignore-type */ $this->bufferLength);
Loading history...
193
            $length -= strlen($contents);
194
195
            echo $contents;
196
        }
197
198
        if ($length > 0 && !$body->eof()) {
199
            echo $body->read($length);
200
        }
201
    }
202
203
    /**
204
     * Parser content range header
205
     * @param string $header
206
     * @return array<string, mixed>
207
     */
208
    protected function parseContentRange(string $header): array
209
    {
210
        $matches = [];
211
212
        if (
213
            preg_match(
214
                '/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/',
215
                $header,
216
                $matches
217
            )
218
        ) {
219
            return [
220
                'unit' => $matches['unit'],
221
                'first' => (int) $matches['first'],
222
                'last' => (int) $matches['last'],
223
                'length' => ($matches['length'] === '*') ? '*' : (int) $matches['length'],
224
            ];
225
        }
226
227
        return [];
228
    }
229
}
230