ResponseEmitter::emitStatusLine()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
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 class
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   https://www.platine-php.com
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
 * @class ResponseEmitter
59
 * @package Platine\Framework\Http\Emitter
60
 */
61
class ResponseEmitter implements EmitterInterface
62
{
63
    /**
64
     * The response buffer length
65
     * @var int|null
66
     */
67
    protected ?int $bufferLength = null;
68
69
    /**
70
     * Create new instance
71
     * @param int|null $bufferLength
72
     */
73
    public function __construct(?int $bufferLength = null)
74
    {
75
        if ($bufferLength !== null && $bufferLength < 1) {
76
            throw new InvalidArgumentException(sprintf(
77
                'The response buffer length must be greater than zero; received [%d].',
78
                $bufferLength
79
            ));
80
        }
81
82
        $this->bufferLength = $bufferLength;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function emit(ResponseInterface $response, bool $withBody = true): void
89
    {
90
        if (headers_sent()) {
91
            throw HeadersAlreadySentException::create();
92
        }
93
94
        if (ob_get_level() > 0 && ob_get_length() > 0) {
95
            throw OutputAlreadySentException::create();
96
        }
97
98
        $this->emitHeaders($response);
99
        $this->emitStatusLine($response);
100
101
        if ($withBody && $response->getBody()->isReadable()) {
102
            $this->emitBody($response);
103
        }
104
    }
105
106
    /**
107
     * Emit headers
108
     * @param ResponseInterface $response
109
     * @return void
110
     */
111
    protected function emitHeaders(ResponseInterface $response): void
112
    {
113
        foreach ($response->getHeaders() as $name => $values) {
114
            $name = str_replace(
115
                ' ',
116
                '-',
117
                ucwords(strtolower(str_replace('-', ' ', $name)))
118
            );
119
120
            $isFirst = $name !== 'Set-Cookie';
121
            foreach ($values as $value) {
122
                $header = sprintf('%s: %s', $name, $value);
123
                header($header, $isFirst);
124
                $isFirst = false;
125
            }
126
        }
127
    }
128
129
    /**
130
     * Emit status line
131
     * @param ResponseInterface $response
132
     * @return void
133
     */
134
    protected function emitStatusLine(ResponseInterface $response): void
135
    {
136
        $code = $response->getStatusCode();
137
        $text = $response->getReasonPhrase();
138
        $protocolVersion = $response->getProtocolVersion();
139
        $status = $code . (empty($text) ? '' : ' ' . $text);
140
        $header = sprintf('HTTP/%s %s', $protocolVersion, $status);
141
        header($header, true, $code);
142
    }
143
144
    /**
145
     * Emit body
146
     * @param ResponseInterface $response
147
     * @return void
148
     */
149
    protected function emitBody(ResponseInterface $response): void
150
    {
151
        if ($this->bufferLength === null) {
152
            echo $response->getBody();
153
            return;
154
        }
155
156
        flush();
157
        $body = $response->getBody();
158
        $range = $this->parseContentRange(
159
            $response->getHeaderLine('content-range')
160
        );
161
162
        if (isset($range['unit']) && $range['unit'] === 'bytes') {
163
            $this->emitBodyRange($body, $range['first'], $range['last']);
164
            return;
165
        }
166
167
        if ($body->isSeekable()) {
168
            $body->rewind();
169
        }
170
171
        while ($body->eof() === false) {
172
            echo $body->read($this->bufferLength);
173
        }
174
    }
175
176
    /**
177
     * Emit body range
178
     * @param StreamInterface $body
179
     * @param int $first
180
     * @param int $last
181
     * @return void
182
     */
183
    protected function emitBodyRange(StreamInterface $body, int $first, int $last): void
184
    {
185
        $length = $last - $first + 1;
186
        if ($body->isSeekable()) {
187
            $body->seek($first);
188
        }
189
190
        while ($length >= $this->bufferLength && !$body->eof()) {
191
            $contents = $body->read((int) $this->bufferLength);
192
            $length -= strlen($contents);
193
194
            echo $contents;
195
        }
196
197
        if ($length > 0 && $body->eof() === false) {
198
            echo $body->read($length);
199
        }
200
    }
201
202
    /**
203
     * Parser content range header
204
     * @param string $header
205
     * @return array<string, mixed>
206
     */
207
    protected function parseContentRange(string $header): array
208
    {
209
        $matches = [];
210
        if (
211
            preg_match(
212
                '/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/',
213
                $header,
214
                $matches
215
            )
216
        ) {
217
            return [
218
                'unit' => $matches['unit'],
219
                'first' => (int) $matches['first'],
220
                'last' => (int) $matches['last'],
221
                'length' => ($matches['length'] === '*') ? '*' : (int) $matches['length'],
222
            ];
223
        }
224
225
        return [];
226
    }
227
}
228