Passed
Push — master ( 29fb57...ab4d12 )
by
unknown
28s
created

Response::setErrorCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: harry
5
 * Date: 2/14/18
6
 * Time: 11:58 AM
7
 */
8
9
namespace PhpRestfulApiResponse;
10
11
use League\Fractal\Manager;
12
use League\Fractal\Pagination\Cursor;
13
use League\Fractal\Resource\Collection;
14
use League\Fractal\Resource\Item;
15
use League\Fractal\TransformerAbstract;
16
use PhpRestfulApiResponse\Contracts\PhpRestfulApiResponse;
17
use Zend\Diactoros\MessageTrait;
18
use InvalidArgumentException;
19
20
class Response implements PhpRestfulApiResponse
21
{
22
    use MessageTrait;
23
24
    const MIN_STATUS_CODE_VALUE = 100;
25
    const MAX_STATUS_CODE_VALUE = 599;
26
27
    /**
28
     * Map of standard HTTP status code/reason phrases
29
     *
30
     * @var array
31
     */
32
    private $phrases = [
33
        // INFORMATIONAL CODES
34
        100 => 'Continue',
35
        101 => 'Switching Protocols',
36
        102 => 'Processing',
37
        103 => 'Early Hints',
38
        // SUCCESS CODES
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
        // REDIRECTION CODES
50
        300 => 'Multiple Choices',
51
        301 => 'Moved Permanently',
52
        302 => 'Found',
53
        303 => 'See Other',
54
        304 => 'Not Modified',
55
        305 => 'Use Proxy',
56
        306 => 'Switch Proxy', // Deprecated to 306 => '(Unused)'
57
        307 => 'Temporary Redirect',
58
        308 => 'Permanent Redirect',
59
        // CLIENT ERROR
60
        400 => 'Bad Request',
61
        401 => 'Unauthorized',
62
        402 => 'Payment Required',
63
        403 => 'Forbidden',
64
        404 => 'Not Found',
65
        405 => 'Method Not Allowed',
66
        406 => 'Not Acceptable',
67
        407 => 'Proxy Authentication Required',
68
        408 => 'Request Timeout',
69
        409 => 'Conflict',
70
        410 => 'Gone',
71
        411 => 'Length Required',
72
        412 => 'Precondition Failed',
73
        413 => 'Payload Too Large',
74
        414 => 'URI Too Long',
75
        415 => 'Unsupported Media Type',
76
        416 => 'Range Not Satisfiable',
77
        417 => 'Expectation Failed',
78
        418 => 'I\'m a teapot',
79
        421 => 'Misdirected Request',
80
        422 => 'Unprocessable Entity',
81
        423 => 'Locked',
82
        424 => 'Failed Dependency',
83
        425 => 'Unordered Collection',
84
        426 => 'Upgrade Required',
85
        428 => 'Precondition Required',
86
        429 => 'Too Many Requests',
87
        431 => 'Request Header Fields Too Large',
88
        444 => 'Connection Closed Without Response',
89
        451 => 'Unavailable For Legal Reasons',
90
        // SERVER ERROR
91
        499 => 'Client Closed Request',
92
        500 => 'Internal Server Error',
93
        501 => 'Not Implemented',
94
        502 => 'Bad Gateway',
95
        503 => 'Service Unavailable',
96
        504 => 'Gateway Timeout',
97
        505 => 'HTTP Version Not Supported',
98
        506 => 'Variant Also Negotiates',
99
        507 => 'Insufficient Storage',
100
        508 => 'Loop Detected',
101
        510 => 'Not Extended',
102
        511 => 'Network Authentication Required',
103
        599 => 'Network Connect Timeout Error',
104
    ];
105
106
    /**
107
     * @var string
108
     */
109
    private $reasonPhrase = '';
110
111
    /**
112
     * @var int
113
     */
114
    private $statusCode;
115
116
    /**
117
     * @var int|string
118
     */
119
    private $errorCode;
120
121
    /**
122
     * Response constructor.
123
     * @param string $body
124
     * @param int $status
125
     * @param int $errorCode
126
     * @param array $headers
127
     */
128 27
    public function __construct($body = 'php://memory', int $status = 200, $errorCode = null, array $headers = [])
129
    {
130 27
        $this->setStatusCode($status);
131 27
        $this->setErrorCode($errorCode);
132 27
        $this->stream = $this->getStream($body, 'wb+');
133 27
        $this->setHeaders($headers);
134 27
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 24
    public function getStatusCode()
140
    {
141 24
        return $this->statusCode;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147 19
    public function getReasonPhrase()
148
    {
149 19
        if (! $this->reasonPhrase
150 19
            && isset($this->phrases[$this->statusCode])
151
        ) {
152 19
            $this->reasonPhrase = $this->phrases[$this->statusCode];
153
        }
154
155 19
        return $this->reasonPhrase;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 1
    public function withStatus($code, $reasonPhrase = '')
162
    {
163 1
        $new = clone $this;
164 1
        $new->setStatusCode($code);
165 1
        $new->reasonPhrase = $reasonPhrase;
166 1
        return $new;
167
    }
168
169
    /**
170
     * @param array|null $data
171
     * @param $code
172
     * @param array $headers
173
     * @return Response
174
     */
175 3
    public function withArray($data, $code = 200, array $headers = [])
176
    {
177 3
        $new = clone $this;
178 3
        $new->setStatusCode($code);
179 3
        $new->getBody()->write(json_encode($data));
180 3
        $new = $new->withHeader('Content-Type', 'application/json');
181 3
        $new->headers = array_merge($new->headers, $headers);
182 3
        return $new;
183
    }
184
185
    /**
186
     * @param $data
187
     * @param TransformerAbstract|callable $transformer
188
     * @param int $code
189
     * @param null $resourceKey
190
     * @param array $meta
191
     * @param array $headers
192
     * @return Response
193
     */
194 1
    public function withItem($data, $transformer, $code = 200, $resourceKey = null, $meta = [], array $headers = [])
195
    {
196 1
        $resource = new Item($data, $transformer, $resourceKey);
197
198 1
        foreach ($meta as $metaKey => $metaValue) {
199
            $resource->setMetaValue($metaKey, $metaValue);
200
        }
201
202 1
        $manager = new Manager();
203
204 1
        $rootScope = $manager->createData($resource);
205
206 1
        return $this->withArray($rootScope->toArray(), $code, $headers);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->withArray(...ay(), $code, $headers); (PhpRestfulApiResponse\Response) is incompatible with the return type declared by the interface PhpRestfulApiResponse\Co...ulApiResponse::withItem of type Zend\Diactoros\Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
207
    }
208
209
    /**
210
     * @param $data
211
     * @param TransformerAbstract|callable $transformer
212
     * @param int $code
213
     * @param null $resourceKey
214
     * @param Cursor|null $cursor
215
     * @param array $meta
216
     * @param array $headers
217
     * @return Response
218
     */
219 1
    public function withCollection($data, $transformer, $code = 200, $resourceKey = null, Cursor $cursor = null, $meta = [], array $headers = [])
220
    {
221 1
        $resource = new Collection($data, $transformer, $resourceKey);
222
223 1
        foreach ($meta as $metaKey => $metaValue) {
224
            $resource->setMetaValue($metaKey, $metaValue);
225
        }
226
227 1
        if (!is_null($cursor)) {
228
            $resource->setCursor($cursor);
229
        }
230
231 1
        $manager = new Manager();
232
233 1
        $rootScope = $manager->createData($resource);
234
235 1
        return $this->withArray($rootScope->toArray(), $code, $headers);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->withArray(...ay(), $code, $headers); (PhpRestfulApiResponse\Response) is incompatible with the return type declared by the interface PhpRestfulApiResponse\Co...esponse::withCollection of type Zend\Diactoros\Response.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
236
    }
237
238
    /**
239
     * Response for errors
240
     *
241
     * @param string|array $message
242
     * @param int $statusCode
243
     * @param int|string $errorCode
244
     * @param array  $headers
245
     * @return mixed
246
     */
247 19
    public function withError($message, int $statusCode, $errorCode = null, array $headers = [])
248
    {
249 19
        $new = clone $this;
250 19
        $new->setStatusCode($statusCode);
251 19
        $new->setErrorCode($errorCode);
252 19
        $new->getBody()->write(
253 19
            json_encode(
254
                [
255 19
                    'error' => array_filter([
256 19
                        'http_code' => $new->statusCode,
257 19
                        'code' => $errorCode,
258 19
                        'phrase' => $new->getReasonPhrase(),
259 19
                        'message' => $message
260
                    ])
261
                ]
262
            )
263
        );
264 19
        $new = $new->withHeader('Content-Type', 'application/json');
265 19
        $new->headers = array_merge($new->headers, $headers);
266 19
        return $new;
267
    }
268
269
    /**
270
     * Generates a response with a 403 HTTP header and a given message.
271
     *
272
     * @param string $message
273
     * @param int|string $errorCode
274
     * @param array  $headers
275
     * @return mixed
276
     */
277 2
    public function errorForbidden(string $message = '', $errorCode = null, array $headers = [])
278
    {
279 2
        return $this->withError($message, 403, $errorCode, $headers);
280
    }
281
282
    /**
283
     * Generates a response with a 500 HTTP header and a given message.
284
     *
285
     * @param string $message
286
     * @param int|string $errorCode
287
     * @param array $headers
288
     * @return mixed
289
     */
290 2
    public function errorInternalError(string $message = '', $errorCode = null, array $headers = [])
291
    {
292 2
        return $this->withError($message, 500, $errorCode, $headers);
293
    }
294
295
    /**
296
     * Generates a response with a 404 HTTP header and a given message.
297
     *
298
     * @param string $message
299
     * @param int|string $errorCode
300
     * @param array  $headers
301
     * @return mixed
302
     */
303 2
    public function errorNotFound(string $message = '', $errorCode = null, array $headers = [])
304
    {
305 2
        return $this->withError($message, 404, $errorCode, $headers);
306
    }
307
308
    /**
309
     * Generates a response with a 401 HTTP header and a given message.
310
     *
311
     * @param string $message
312
     * @param int|string $errorCode
313
     * @param array $headers
314
     * @return mixed
315
     */
316 2
    public function errorUnauthorized(string $message = '', $errorCode = null, array $headers = [])
317
    {
318 2
        return $this->withError($message, 401, $errorCode, $headers);
319
    }
320
321
    /**
322
     * Generates a response with a 400 HTTP header and a given message.
323
     *
324
     * @param array $message
325
     * @param int|array $errorCode
326
     * @param array $headers
327
     * @return mixed
328
     */
329 1
    public function errorWrongArgs(array $message, $errorCode = null, array $headers = [])
330
    {
331 1
        return $this->withError($message, 400, $errorCode, $headers);
0 ignored issues
show
Bug introduced by
It seems like $errorCode defined by parameter $errorCode on line 329 can also be of type array; however, PhpRestfulApiResponse\Response::withError() does only seem to accept integer|string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
332
    }
333
334
    /**
335
     * Generates a response with a 410 HTTP header and a given message.
336
     *
337
     * @param string $message
338
     * @param int|string $errorCode
339
     * @param array $headers
340
     * @return mixed
341
     */
342 2
    public function errorGone(string $message = '', $errorCode = null, array $headers = [])
343
    {
344 2
        return $this->withError($message, 410, $errorCode, $headers);
345
    }
346
347
    /**
348
     * Generates a response with a 405 HTTP header and a given message.
349
     *
350
     * @param string $message
351
     * @param int|string $errorCode
352
     * @param array $headers
353
     * @return mixed
354
     */
355 2
    public function errorMethodNotAllowed(string $message = '', $errorCode = null, array $headers = [])
356
    {
357 2
        return $this->withError($message, 405, $errorCode, $headers);
358
    }
359
360
    /**
361
     * Generates a Response with a 431 HTTP header and a given message.
362
     *
363
     * @param string $message
364
     * @param int|string $errorCode
365
     * @param array $headers
366
     * @return mixed
367
     */
368 2
    public function errorUnwillingToProcess(string $message = '', $errorCode = null, array $headers = [])
369
    {
370 2
        return $this->withError($message, 431, $errorCode, $headers);
371
    }
372
373
    /**
374
     * Generates a Response with a 422 HTTP header and a given message.
375
     *
376
     * @param string $message
377
     * @param int|string $errorCode
378
     * @param array $headers
379
     * @return mixed
380
     */
381 2
    public function errorUnprocessable(string $message = '', $errorCode = null, array $headers = [])
382
    {
383 2
        return $this->withError($message, 422, $errorCode, $headers);
384
    }
385
386
    /**
387
     * @return int|string
388
     */
389 20
    public function getErrorCode()
390
    {
391 20
        return $this->errorCode;
392
    }
393
394
    /**
395
     * @param $errorCode
396
     * @return $this
397
     */
398 27
    public function setErrorCode($errorCode)
399
    {
400 27
        $this->errorCode = $errorCode;
401 27
    }
402
403
    /**
404
     * Set a valid status code.
405
     *
406
     * @param int $statusCode
407
     * @throws InvalidArgumentException on an invalid status code.
408
     */
409 27
    private function setStatusCode(int $statusCode)
410
    {
411 27
        if ($statusCode < static::MIN_STATUS_CODE_VALUE
412 27
            || $statusCode > static::MAX_STATUS_CODE_VALUE
413
        ) {
414 2
            throw new InvalidArgumentException(sprintf(
415 2
                'Invalid status code "%s"; must be an integer between %d and %d, inclusive',
416 2
                (is_scalar($statusCode) ? $statusCode : gettype($statusCode)),
417 2
                static::MIN_STATUS_CODE_VALUE,
418 2
                static::MAX_STATUS_CODE_VALUE
419
            ));
420
        }
421 27
        $this->statusCode = $statusCode;
422
    }
423
}