Response   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 194
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 91
c 3
b 0
f 0
dl 0
loc 194
ccs 34
cts 34
cp 1
rs 10
wmc 17

7 Methods

Rating   Name   Duplication   Size   Complexity  
A validateStatusCode() 0 8 4
A validateReasonPhrase() 0 12 4
A setStatus() 0 11 2
A getStatusCode() 0 3 1
A __construct() 0 16 4
A getReasonPhrase() 0 3 1
A withStatus() 0 6 1
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message;
13
14
use Fig\Http\Message\StatusCodeInterface;
15
use Psr\Http\Message\ResponseInterface;
16
use Psr\Http\Message\StreamInterface;
17
use Sunrise\Http\Message\Exception\InvalidArgumentException;
18
19
use function is_int;
20
use function is_string;
21
use function preg_match;
22
23
class Response extends Message implements ResponseInterface, StatusCodeInterface
24
{
25
    /**
26
     * List of Reason Phrases
27
     *
28
     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
29
     *
30
     * @var array<int<100, 599>, non-empty-string>
31
     */
32
    public const REASON_PHRASES = [
33
        // 1xx
34
        100 => 'Continue',
35
        101 => 'Switching Protocols',
36
        102 => 'Processing',
37
        103 => 'Early Hints',
38
        // 2xx
39
        200 => 'OK',
40
        201 => 'Created',
41
        202 => 'Accepted',
42
        203 => 'Non-Authoritative Information',
43
        204 => 'No Content',
44
        205 => 'Reset Content',
45
        206 => 'Partial Content',
46
        207 => 'Multi-Status',
47
        208 => 'Already Reported',
48
        226 => 'IM Used',
49
        // 3xx
50
        300 => 'Multiple Choices',
51
        301 => 'Moved Permanently',
52
        302 => 'Found',
53
        303 => 'See Other',
54
        304 => 'Not Modified',
55
        305 => 'Use Proxy',
56
        307 => 'Temporary Redirect',
57
        308 => 'Permanent Redirect',
58
        // 4xx
59
        400 => 'Bad Request',
60
        401 => 'Unauthorized',
61
        402 => 'Payment Required',
62
        403 => 'Forbidden',
63
        404 => 'Not Found',
64
        405 => 'Method Not Allowed',
65
        406 => 'Not Acceptable',
66
        407 => 'Proxy Authentication Required',
67
        408 => 'Request Timeout',
68
        409 => 'Conflict',
69
        410 => 'Gone',
70
        411 => 'Length Required',
71
        412 => 'Precondition Failed',
72
        413 => 'Payload Too Large',
73
        414 => 'URI Too Long',
74
        415 => 'Unsupported Media Type',
75
        416 => 'Range Not Satisfiable',
76
        417 => 'Expectation Failed',
77
        421 => 'Misdirected Request',
78
        422 => 'Unprocessable Entity',
79
        423 => 'Locked',
80
        424 => 'Failed Dependency',
81
        425 => 'Too Early',
82
        426 => 'Upgrade Required',
83
        428 => 'Precondition Required',
84
        429 => 'Too Many Requests',
85
        431 => 'Request Header Fields Too Large',
86
        451 => 'Unavailable For Legal Reasons',
87
        // 5xx
88
        500 => 'Internal Server Error',
89
        501 => 'Not Implemented',
90
        502 => 'Bad Gateway',
91
        503 => 'Service Unavailable',
92
        504 => 'Gateway Timeout',
93
        505 => 'HTTP Version Not Supported',
94
        506 => 'Variant Also Negotiates',
95
        507 => 'Insufficient Storage',
96
        508 => 'Loop Detected',
97
        510 => 'Not Extended',
98
        511 => 'Network Authentication Required',
99
    ];
100
101
    private int $statusCode = self::STATUS_OK;
102
    private string $reasonPhrase = self::REASON_PHRASES[self::STATUS_OK];
103
104
    /**
105
     * @param array<string, string|string[]>|null $headers
106
     *
107
     * @throws InvalidArgumentException
108
     */
109 169
    public function __construct(
110
        ?int $statusCode = null,
111
        ?string $reasonPhrase = null,
112
        ?array $headers = null,
113
        ?StreamInterface $body = null
114
    ) {
115 169
        if ($statusCode !== null) {
116 27
            $this->setStatus($statusCode, $reasonPhrase ?? '');
117
        }
118
119 163
        if ($headers !== null) {
120 16
            $this->setHeaders($headers);
121
        }
122
123 153
        if ($body !== null) {
124 1
            $this->setBody($body);
125
        }
126
    }
127
128
    /**
129
     * @inheritDoc
130
     */
131 20
    public function getStatusCode(): int
132
    {
133 20
        return $this->statusCode;
134
    }
135
136
    /**
137
     * @inheritDoc
138
     */
139 19
    public function getReasonPhrase(): string
140
    {
141 19
        return $this->reasonPhrase;
142
    }
143
144
    /**
145
     * @inheritDoc
146
     */
147 18
    public function withStatus($code, $reasonPhrase = ''): ResponseInterface
148
    {
149 18
        $clone = clone $this;
150 18
        $clone->setStatus($code, $reasonPhrase);
151
152 6
        return $clone;
153
    }
154
155
    /**
156
     * Sets the given status code to the response
157
     *
158
     * @param int $statusCode
159
     * @param string $reasonPhrase
160
     *
161
     * @throws InvalidArgumentException
162
     */
163 45
    final protected function setStatus($statusCode, $reasonPhrase): void
164
    {
165 45
        $this->validateStatusCode($statusCode);
166 32
        $this->validateReasonPhrase($reasonPhrase);
167
168 27
        if ($reasonPhrase === '') {
169 21
            $reasonPhrase = self::REASON_PHRASES[$statusCode] ?? 'Unknown Status Code';
170
        }
171
172 27
        $this->statusCode = $statusCode;
173 27
        $this->reasonPhrase = $reasonPhrase;
174
    }
175
176
    /**
177
     * Validates the given status code
178
     *
179
     * @link https://tools.ietf.org/html/rfc7230#section-3.1.2
180
     *
181
     * @param mixed $statusCode
182
     *
183
     * @throws InvalidArgumentException
184
     */
185 45
    private function validateStatusCode($statusCode): void
186
    {
187 45
        if (!is_int($statusCode)) {
188 5
            throw new InvalidArgumentException('HTTP status code must be an integer');
189
        }
190
191 40
        if (!($statusCode >= 100 && $statusCode <= 599)) {
192 8
            throw new InvalidArgumentException('Invalid HTTP status code');
193
        }
194
    }
195
196
    /**
197
     * Validates the given reason phrase
198
     *
199
     * @link https://tools.ietf.org/html/rfc7230#section-3.1.2
200
     *
201
     * @param mixed $reasonPhrase
202
     *
203
     * @throws InvalidArgumentException
204
     */
205 32
    private function validateReasonPhrase($reasonPhrase): void
206
    {
207 32
        if ($reasonPhrase === '') {
208 21
            return;
209
        }
210
211 11
        if (!is_string($reasonPhrase)) {
212 2
            throw new InvalidArgumentException('HTTP reason phrase must be a string');
213
        }
214
215 9
        if (!preg_match(HeaderInterface::RFC7230_FIELD_VALUE_REGEX, $reasonPhrase)) {
216 3
            throw new InvalidArgumentException('Invalid HTTP reason phrase');
217
        }
218
    }
219
}
220