Response   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 4

Test Coverage

Coverage 90.74%

Importance

Changes 0
Metric Value
wmc 21
lcom 3
cbo 4
dl 0
loc 321
ccs 49
cts 54
cp 0.9074
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A write() 0 9 2
A end() 0 10 2
A isComplete() 0 4 1
A getStatusCode() 0 4 1
A getReasonPhrase() 0 8 3
A withStatus() 0 7 1
A asJson() 0 11 3
A asText() 0 5 1
A asHtml() 0 5 1
A asXml() 0 15 4
A empty() 0 5 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+');
0 ignored issues
show
Documentation Bug introduced by
It seems like \Igni\Network\Http\Stream::create($body, 'wb+') of type object<self> is incompatible with the declared type object<Psr\Http\Message\StreamInterface> of property $stream.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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);
0 ignored issues
show
Security Bug introduced by
It seems like $body defined by $data->asXML() on line 320 can also be of type false; however, Igni\Network\Http\Response::__construct() does only seem to accept string|resource|object<P...essage\StreamInterface>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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