Passed
Push — master ( 4c5f08...24f884 )
by Dawid
02:28
created

Response::isComplete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php declare(strict_types=1);
2
3
namespace Igni\Network\Http;
4
5
use DOMDocument;
6
use Igni\Exception\RuntimeException;
7
use Igni\Network\Exception\InvalidArgumentException;
8
use JsonSerializable;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\StreamInterface;
11
use SimpleXMLElement;
12
use Zend\Diactoros\MessageTrait;
13
14
use function is_array;
15
use function is_string;
16
use function json_encode;
17
18
/**
19
 * PSR-7 implementation of ResponseInterface.
20
 * Utilizes zend/diactoros implementation.
21
 *
22
 * @see ResponseInterface
23
 * @package Igni\Http
24
 */
25
class Response implements ResponseInterface
26
{
27
    use MessageTrait;
28
29
    const HTTP_CONTINUE = 100;
30
    const HTTP_SWITCHING_PROTOCOLS = 101;
31
    const HTTP_PROCESSING = 102;
32
    const HTTP_OK = 200;
33
    const HTTP_CREATED = 201;
34
    const HTTP_ACCEPTED = 202;
35
    const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
36
    const HTTP_NO_CONTENT = 204;
37
    const HTTP_RESET_CONTENT = 205;
38
    const HTTP_PARTIAL_CONTENT = 206;
39
    const HTTP_MULTI_STATUS = 207;
40
    const HTTP_ALREADY_REPORTED = 208;
41
    const HTTP_IM_USED = 226;
42
    const HTTP_MULTIPLE_CHOICES = 300;
43
    const HTTP_MOVED_PERMANENTLY = 301;
44
    const HTTP_FOUND = 302;
45
    const HTTP_SEE_OTHER = 303;
46
    const HTTP_NOT_MODIFIED = 304;
47
    const HTTP_USE_PROXY = 305;
48
    const HTTP_RESERVED = 306;
49
    const HTTP_TEMPORARY_REDIRECT = 307;
50
    const HTTP_PERMANENTLY_REDIRECT = 308;
51
    const HTTP_BAD_REQUEST = 400;
52
    const HTTP_UNAUTHORIZED = 401;
53
    const HTTP_PAYMENT_REQUIRED = 402;
54
    const HTTP_FORBIDDEN = 403;
55
    const HTTP_NOT_FOUND = 404;
56
    const HTTP_METHOD_NOT_ALLOWED = 405;
57
    const HTTP_NOT_ACCEPTABLE = 406;
58
    const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
59
    const HTTP_REQUEST_TIMEOUT = 408;
60
    const HTTP_CONFLICT = 409;
61
    const HTTP_GONE = 410;
62
    const HTTP_LENGTH_REQUIRED = 411;
63
    const HTTP_PRECONDITION_FAILED = 412;
64
    const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
65
    const HTTP_REQUEST_URI_TOO_LONG = 414;
66
    const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
67
    const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
68
    const HTTP_EXPECTATION_FAILED = 417;
69
    const HTTP_I_AM_A_TEAPOT = 418;
70
    const HTTP_MISDIRECTED_REQUEST = 421;
71
    const HTTP_UNPROCESSABLE_ENTITY = 422;
72
    const HTTP_LOCKED = 423;
73
    const HTTP_FAILED_DEPENDENCY = 424;
74
    const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425;
75
    const HTTP_UPGRADE_REQUIRED = 426;
76
    const HTTP_PRECONDITION_REQUIRED = 428;
77
    const HTTP_TOO_MANY_REQUESTS = 429;
78
    const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
79
    const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
80
    const HTTP_INTERNAL_SERVER_ERROR = 500;
81
    const HTTP_NOT_IMPLEMENTED = 501;
82
    const HTTP_BAD_GATEWAY = 502;
83
    const HTTP_SERVICE_UNAVAILABLE = 503;
84
    const HTTP_GATEWAY_TIMEOUT = 504;
85
    const HTTP_VERSION_NOT_SUPPORTED = 505;
86
    const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;
87
    const HTTP_INSUFFICIENT_STORAGE = 507;
88
    const HTTP_LOOP_DETECTED = 508;
89
    const HTTP_NOT_EXTENDED = 510;
90
    const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
91
92
    /**
93
     * Map of standard HTTP status code/reason phrases
94
     *
95
     * @var array
96
     */
97
    private static $phrases = [
98
        // INFORMATIONAL CODES
99
        100 => 'Continue',
100
        101 => 'Switching Protocols',
101
        102 => 'Processing',
102
        // SUCCESS CODES
103
        200 => 'OK',
104
        201 => 'Created',
105
        202 => 'Accepted',
106
        203 => 'Non-Authoritative Information',
107
        204 => 'No Content',
108
        205 => 'Reset Content',
109
        206 => 'Partial Content',
110
        207 => 'Multi-status',
111
        208 => 'Already Reported',
112
        // REDIRECTION CODES
113
        300 => 'Multiple Choices',
114
        301 => 'Moved Permanently',
115
        302 => 'Found',
116
        303 => 'See Other',
117
        304 => 'Not Modified',
118
        305 => 'Use Proxy',
119
        306 => 'Switch Proxy', // Deprecated
120
        307 => 'Temporary Redirect',
121
        // CLIENT ERROR
122
        400 => 'Bad Request',
123
        401 => 'Unauthorized',
124
        402 => 'Payment Required',
125
        403 => 'Forbidden',
126
        404 => 'Not Found',
127
        405 => 'Method Not Allowed',
128
        406 => 'Not Acceptable',
129
        407 => 'Proxy Authentication Required',
130
        408 => 'Request Time-out',
131
        409 => 'Conflict',
132
        410 => 'Gone',
133
        411 => 'Length Required',
134
        412 => 'Precondition Failed',
135
        413 => 'Request Entity Too Large',
136
        414 => 'Request-URI Too Large',
137
        415 => 'Unsupported Media Property',
138
        416 => 'Requested range not satisfiable',
139
        417 => 'Expectation Failed',
140
        418 => 'I\'m a teapot',
141
        422 => 'Unprocessable Entity',
142
        423 => 'Locked',
143
        424 => 'Failed Dependency',
144
        425 => 'Unordered Collection',
145
        426 => 'Upgrade Required',
146
        428 => 'Precondition Required',
147
        429 => 'Too Many Requests',
148
        431 => 'Request Header Fields Too Large',
149
        // SERVER ERROR
150
        500 => 'Internal Server Error',
151
        501 => 'Not Implemented',
152
        502 => 'Bad Gateway',
153
        503 => 'Service Unavailable',
154
        504 => 'Gateway Time-out',
155
        505 => 'HTTP Version not supported',
156
        506 => 'Variant Also Negotiates',
157
        507 => 'Insufficient Storage',
158
        508 => 'Loop Detected',
159
        511 => 'Network Authentication Required',
160
    ];
161
162
    /**
163
     * @var string
164
     */
165
    private $reasonPhrase = '';
166
167
    /**
168
     * @var int
169
     */
170
    private $statusCode;
171
172
    /**
173
     * @var bool
174
     */
175
    private $complete = false;
176
177
    /**
178
     * @param string|resource|StreamInterface $body Stream identifier and/or actual stream resource
179
     * @param int $status Status code for the response, if any.
180
     * @param array $headers Headers for the response, if any.
181
     * @throws \InvalidArgumentException on any invalid element.
182
     */
183 16
    public function __construct($body = '', int $status = self::HTTP_OK, array $headers = [])
184
    {
185 16
        $this->stream = Stream::create($body, 'wb+');
186 16
        $this->statusCode = $status;
187 16
        $this->reasonPhrase = self::$phrases[$this->statusCode];
188 16
        $this->setHeaders($headers);
189 16
    }
190
191
    /**
192
     * Writes content to the response body
193
     *
194
     * @param string $body
195
     * @return $this
196
     */
197 1
    public function write(string $body)
198
    {
199 1
        if ($this->complete) {
200 1
            throw new RuntimeException('Cannot write to the response, response is already completed.');
201
        }
202
203 1
        $this->getBody()->write($body);
204 1
        return $this;
205
    }
206
207
    /**
208
     * Ends and closes response.
209
     *
210
     * @return $this
211
     */
212 1
    public function end()
213
    {
214 1
        if ($this->complete) {
215
            return $this;
216
        }
217
218 1
        $this->complete = true;
219
220 1
        return $this;
221
    }
222
223 1
    public function isComplete(): bool
224
    {
225 1
        return $this->complete;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 13
    public function getStatusCode()
232
    {
233 13
        return $this->statusCode;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function getReasonPhrase()
240
    {
241
        if (!$this->reasonPhrase && isset(self::$phrases[$this->statusCode])) {
242
            $this->reasonPhrase = self::$phrases[$this->statusCode];
243
        }
244
245
        return $this->reasonPhrase;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 1
    public function withStatus($code, $reasonPhrase = '')
252
    {
253 1
        $new = clone $this;
254 1
        $new->statusCode = $code;
255 1
        $new->reasonPhrase = $reasonPhrase;
256 1
        return $new;
257
    }
258
259
    /**
260
     * Factories response instance from json data.
261
     *
262
     * @param array|\JsonSerializable $data
263
     * @param int $status
264
     * @param array $headers
265
     * @return Response
266
     * @throws InvalidArgumentException
267
     */
268 4
    public static function asJson($data, int $status = self::HTTP_OK, array $headers = [])
269
    {
270 4
        if (! $data instanceof JsonSerializable && !is_array($data)) {
0 ignored issues
show
Bug introduced by
The class JsonSerializable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
271 1
            throw new InvalidArgumentException('Invalid $data provided, method expects array or instance of \JsonSerializable.');
272
        }
273
274 3
        $headers['Content-Type'] = 'application/json';
275
276 3
        $body = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
277 3
        return new Response($body, $status, $headers);
278
    }
279
280
    /**
281
     * Factories response instance from text.
282
     *
283
     * @param string $text
284
     * @param int $status
285
     * @param array $headers
286
     * @return Response
287
     */
288 4
    public static function asText(string $text, int $status = self::HTTP_OK, array $headers = []): Response
289
    {
290 4
        $headers['Content-Type'] = 'text/plain';
291 4
        return new Response($text, $status, $headers);
292
    }
293
294
    /**
295
     * Factories response from html text.
296
     *
297
     * @param string $html
298
     * @param int $status
299
     * @param array $headers
300
     * @return Response
301
     */
302 1
    public static function asHtml(string $html, int $status = self::HTTP_OK, array $headers = [])
303
    {
304 1
        $headers['Content-Type'] = 'text/html';
305 1
        return new Response($html, $status, $headers);
306
    }
307
308
    /**
309
     * Factories xml response.
310
     *
311
     * @param SimpleXMLElement|DOMDocument|string $data
312
     * @param int $status
313
     * @param array $headers
314
     * @return Response
315
     * @throws InvalidArgumentException
316
     */
317 4
    public static function asXml($data, int $status = self::HTTP_OK, array $headers = [])
318
    {
319 4
        if ($data instanceof SimpleXMLElement) {
320 1
            $body = $data->asXML();
321 3
        } elseif ($data instanceof DOMDocument) {
322 1
            $body = $data->saveXML();
323 2
        } elseif (is_string($data)) {
324 1
            $body = $data;
325
        } else {
326 1
            throw new InvalidArgumentException('Invalid $data provided, method expects valid string or instance of \SimpleXMLElement, \DOMDocument');
327
        }
328
329 3
        $headers['Content-Type'] = 'text/xml';
330 3
        return new Response($body, $status, $headers);
331
    }
332
333
    /**
334
     * Factories empty response.
335
     *
336
     * @param int $status
337
     * @param array $headers
338
     * @return Response
339
     */
340 5
    public static function empty(int $status = self::HTTP_OK, array $headers = [])
341
    {
342 5
        $headers['Content-Type'] = 'text/plain';
343 5
        return new Response('', $status, $headers);
344
    }
345
}
346