Response   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 91.84%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 7
dl 0
loc 432
ccs 90
cts 98
cp 0.9184
rs 9.92
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A getStatusCode() 0 4 1
A getReasonPhrase() 0 10 3
A withStatus() 0 7 1
A withArray() 0 8 1
A withItem() 0 14 2
A withCollection() 0 18 3
A withError() 0 20 1
A errorForbidden() 0 4 1
A errorInternalError() 0 4 1
A errorNotFound() 0 4 1
A errorUnauthorized() 0 4 1
A errorWrongArgs() 0 4 1
A errorGone() 0 4 1
A errorMethodNotAllowed() 0 4 1
A errorUnwillingToProcess() 0 4 1
A errorUnprocessable() 0 4 1
A getErrorCode() 0 4 1
A setErrorCode() 0 4 1
A setStatusCode() 0 14 4
A jsonEncode() 0 21 3
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
use Zend\Diactoros\Response\InjectContentTypeTrait;
20
use Zend\Diactoros\Response\JsonResponse;
21
22
class Response implements PhpRestfulApiResponse
23
{
24
    use MessageTrait, InjectContentTypeTrait;
25
26
    const MIN_STATUS_CODE_VALUE = 100;
27
    const MAX_STATUS_CODE_VALUE = 599;
28
29
    /**
30
     * Map of standard HTTP status code/reason phrases
31
     *
32
     * @var array
33
     */
34
    protected $phrases = [
35
        // INFORMATIONAL CODES
36
        100 => 'Continue',
37
        101 => 'Switching Protocols',
38
        102 => 'Processing',
39
        103 => 'Early Hints',
40
        // SUCCESS CODES
41
        200 => 'OK',
42
        201 => 'Created',
43
        202 => 'Accepted',
44
        203 => 'Non-Authoritative Information',
45
        204 => 'No Content',
46
        205 => 'Reset Content',
47
        206 => 'Partial Content',
48
        207 => 'Multi-Status',
49
        208 => 'Already Reported',
50
        226 => 'IM Used',
51
        // REDIRECTION CODES
52
        300 => 'Multiple Choices',
53
        301 => 'Moved Permanently',
54
        302 => 'Found',
55
        303 => 'See Other',
56
        304 => 'Not Modified',
57
        305 => 'Use Proxy',
58
        306 => 'Switch Proxy', // Deprecated to 306 => '(Unused)'
59
        307 => 'Temporary Redirect',
60
        308 => 'Permanent Redirect',
61
        // CLIENT ERROR
62
        400 => 'Bad Request',
63
        401 => 'Unauthorized',
64
        402 => 'Payment Required',
65
        403 => 'Forbidden',
66
        404 => 'Not Found',
67
        405 => 'Method Not Allowed',
68
        406 => 'Not Acceptable',
69
        407 => 'Proxy Authentication Required',
70
        408 => 'Request Timeout',
71
        409 => 'Conflict',
72
        410 => 'Gone',
73
        411 => 'Length Required',
74
        412 => 'Precondition Failed',
75
        413 => 'Payload Too Large',
76
        414 => 'URI Too Long',
77
        415 => 'Unsupported Media Type',
78
        416 => 'Range Not Satisfiable',
79
        417 => 'Expectation Failed',
80
        418 => 'I\'m a teapot',
81
        421 => 'Misdirected Request',
82
        422 => 'Unprocessable Entity',
83
        423 => 'Locked',
84
        424 => 'Failed Dependency',
85
        425 => 'Unordered Collection',
86
        426 => 'Upgrade Required',
87
        428 => 'Precondition Required',
88
        429 => 'Too Many Requests',
89
        431 => 'Request Header Fields Too Large',
90
        444 => 'Connection Closed Without Response',
91
        451 => 'Unavailable For Legal Reasons',
92
        // SERVER ERROR
93
        499 => 'Client Closed Request',
94
        500 => 'Internal Server Error',
95
        501 => 'Not Implemented',
96
        502 => 'Bad Gateway',
97
        503 => 'Service Unavailable',
98
        504 => 'Gateway Timeout',
99
        505 => 'HTTP Version Not Supported',
100
        506 => 'Variant Also Negotiates',
101
        507 => 'Insufficient Storage',
102
        508 => 'Loop Detected',
103
        510 => 'Not Extended',
104
        511 => 'Network Authentication Required',
105
        599 => 'Network Connect Timeout Error',
106
    ];
107
108
    /**
109
     * @var string
110
     */
111
    protected $reasonPhrase = '';
112
113
    /**
114
     * @var int
115
     */
116
    protected $statusCode;
117
118
    /**
119
     * @var int|string
120
     */
121
    protected $errorCode;
122
123
    /**
124
     * Response constructor.
125
     * @param string $body
126
     * @param int $status
127
     * @param int $errorCode
128
     * @param array $headers
129
     */
130 27
    public function __construct($body = 'php://memory', int $status = 200, $errorCode = null, array $headers = [])
131
    {
132 27
        $this->setStatusCode($status);
133 27
        $this->setErrorCode($errorCode);
134 27
        $this->stream = $this->getStream($body, 'wb+');
135 27
        $headers = $this->injectContentType('application/json', $headers);
136 27
        $this->setHeaders($headers);
137 27
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 24
    public function getStatusCode()
143
    {
144 24
        return $this->statusCode;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 20
    public function getReasonPhrase()
151
    {
152 20
        if (! $this->reasonPhrase
153 20
            && isset($this->phrases[$this->statusCode])
154
        ) {
155 20
            $this->reasonPhrase = $this->phrases[$this->statusCode];
156
        }
157
158 20
        return $this->reasonPhrase;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 1
    public function withStatus($code, $reasonPhrase = '')
165
    {
166 1
        $new = clone $this;
167 1
        $new->setStatusCode($code);
168 1
        $new->reasonPhrase = $reasonPhrase;
169 1
        return $new;
170
    }
171
172
    /**
173
     * @param array|null $data
174
     * @param $code
175
     * @param array $headers
176
     * @return Response
177
     */
178 3
    public function withArray($data, $code = 200, array $headers = [])
179
    {
180 3
        $new = clone $this;
181 3
        $new->setStatusCode($code);
182 3
        $new->getBody()->write($this->jsonEncode($data));
183 3
        $new->headers = array_merge($new->headers, $headers);
184 3
        return $new;
185
    }
186
187
    /**
188
     * @param $data
189
     * @param TransformerAbstract|callable $transformer
190
     * @param int $code
191
     * @param null $resourceKey
192
     * @param array $meta
193
     * @param array $headers
194
     * @return Response
195
     */
196 1
    public function withItem($data, $transformer, $code = 200, $resourceKey = null, $meta = [], array $headers = [])
197
    {
198 1
        $resource = new Item($data, $transformer, $resourceKey);
199
200 1
        foreach ($meta as $metaKey => $metaValue) {
201
            $resource->setMetaValue($metaKey, $metaValue);
202
        }
203
204 1
        $manager = new Manager();
205
206 1
        $rootScope = $manager->createData($resource);
207
208 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...
209
    }
210
211
    /**
212
     * @param $data
213
     * @param TransformerAbstract|callable $transformer
214
     * @param int $code
215
     * @param null $resourceKey
216
     * @param Cursor|null $cursor
217
     * @param array $meta
218
     * @param array $headers
219
     * @return Response
220
     */
221 1
    public function withCollection($data, $transformer, $code = 200, $resourceKey = null, Cursor $cursor = null, $meta = [], array $headers = [])
222
    {
223 1
        $resource = new Collection($data, $transformer, $resourceKey);
224
225 1
        foreach ($meta as $metaKey => $metaValue) {
226
            $resource->setMetaValue($metaKey, $metaValue);
227
        }
228
229 1
        if (!is_null($cursor)) {
230
            $resource->setCursor($cursor);
231
        }
232
233 1
        $manager = new Manager();
234
235 1
        $rootScope = $manager->createData($resource);
236
237 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...
238
    }
239
240
    /**
241
     * Response for errors
242
     *
243
     * @param string|array $message
244
     * @param int $statusCode
245
     * @param int|string $errorCode
246
     * @param array  $headers
247
     * @return mixed
248
     */
249 19
    public function withError($message, int $statusCode, $errorCode = null, array $headers = [])
250
    {
251 19
        $new = clone $this;
252 19
        $new->setStatusCode($statusCode);
253 19
        $new->setErrorCode($errorCode);
254 19
        $new->getBody()->write(
255 19
            $this->jsonEncode(
256
                [
257 19
                    'error' => array_filter([
258 19
                        'http_code' => $new->statusCode,
259 19
                        'code' => $errorCode,
260 19
                        'phrase' => $new->getReasonPhrase(),
261 19
                        'message' => $message
262
                    ])
263
                ]
264
            )
265
        );
266 19
        $new->headers = array_merge($new->headers, $headers);
267 19
        return $new;
268
    }
269
270
    /**
271
     * Generates a response with a 403 HTTP header and a given message.
272
     *
273
     * @param string $message
274
     * @param int|string $errorCode
275
     * @param array  $headers
276
     * @return mixed
277
     */
278 2
    public function errorForbidden(string $message = '', $errorCode = null, array $headers = [])
279
    {
280 2
        return $this->withError($message, 403, $errorCode, $headers);
281
    }
282
283
    /**
284
     * Generates a response with a 500 HTTP header and a given message.
285
     *
286
     * @param string $message
287
     * @param int|string $errorCode
288
     * @param array $headers
289
     * @return mixed
290
     */
291 2
    public function errorInternalError(string $message = '', $errorCode = null, array $headers = [])
292
    {
293 2
        return $this->withError($message, 500, $errorCode, $headers);
294
    }
295
296
    /**
297
     * Generates a response with a 404 HTTP header and a given message.
298
     *
299
     * @param string $message
300
     * @param int|string $errorCode
301
     * @param array  $headers
302
     * @return mixed
303
     */
304 2
    public function errorNotFound(string $message = '', $errorCode = null, array $headers = [])
305
    {
306 2
        return $this->withError($message, 404, $errorCode, $headers);
307
    }
308
309
    /**
310
     * Generates a response with a 401 HTTP header and a given message.
311
     *
312
     * @param string $message
313
     * @param int|string $errorCode
314
     * @param array $headers
315
     * @return mixed
316
     */
317 2
    public function errorUnauthorized(string $message = '', $errorCode = null, array $headers = [])
318
    {
319 2
        return $this->withError($message, 401, $errorCode, $headers);
320
    }
321
322
    /**
323
     * Generates a response with a 400 HTTP header and a given message.
324
     *
325
     * @param array $message
326
     * @param int|array $errorCode
327
     * @param array $headers
328
     * @return mixed
329
     */
330 1
    public function errorWrongArgs(array $message, $errorCode = null, array $headers = [])
331
    {
332 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 330 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...
333
    }
334
335
    /**
336
     * Generates a response with a 410 HTTP header and a given message.
337
     *
338
     * @param string $message
339
     * @param int|string $errorCode
340
     * @param array $headers
341
     * @return mixed
342
     */
343 2
    public function errorGone(string $message = '', $errorCode = null, array $headers = [])
344
    {
345 2
        return $this->withError($message, 410, $errorCode, $headers);
346
    }
347
348
    /**
349
     * Generates a response with a 405 HTTP header and a given message.
350
     *
351
     * @param string $message
352
     * @param int|string $errorCode
353
     * @param array $headers
354
     * @return mixed
355
     */
356 2
    public function errorMethodNotAllowed(string $message = '', $errorCode = null, array $headers = [])
357
    {
358 2
        return $this->withError($message, 405, $errorCode, $headers);
359
    }
360
361
    /**
362
     * Generates a Response with a 431 HTTP header and a given message.
363
     *
364
     * @param string $message
365
     * @param int|string $errorCode
366
     * @param array $headers
367
     * @return mixed
368
     */
369 2
    public function errorUnwillingToProcess(string $message = '', $errorCode = null, array $headers = [])
370
    {
371 2
        return $this->withError($message, 431, $errorCode, $headers);
372
    }
373
374
    /**
375
     * Generates a Response with a 422 HTTP header and a given message.
376
     *
377
     * @param string $message
378
     * @param int|string $errorCode
379
     * @param array $headers
380
     * @return mixed
381
     */
382 2
    public function errorUnprocessable(string $message = '', $errorCode = null, array $headers = [])
383
    {
384 2
        return $this->withError($message, 422, $errorCode, $headers);
385
    }
386
387
    /**
388
     * @return int|string
389
     */
390 20
    public function getErrorCode()
391
    {
392 20
        return $this->errorCode;
393
    }
394
395
    /**
396
     * @param $errorCode
397
     * @return $this
398
     */
399 27
    public function setErrorCode($errorCode)
400
    {
401 27
        $this->errorCode = $errorCode;
402 27
    }
403
404
    /**
405
     * Set a valid status code.
406
     *
407
     * @param int $statusCode
408
     * @throws InvalidArgumentException on an invalid status code.
409
     */
410 27
    protected function setStatusCode(int $statusCode)
411
    {
412 27
        if ($statusCode < static::MIN_STATUS_CODE_VALUE
413 27
            || $statusCode > static::MAX_STATUS_CODE_VALUE
414
        ) {
415 2
            throw new InvalidArgumentException(sprintf(
416 2
                'Invalid status code "%s"; must be an integer between %d and %d, inclusive',
417 2
                (is_scalar($statusCode) ? $statusCode : gettype($statusCode)),
418 2
                static::MIN_STATUS_CODE_VALUE,
419 2
                static::MAX_STATUS_CODE_VALUE
420
            ));
421
        }
422 27
        $this->statusCode = $statusCode;
423 27
    }
424
425
    /**
426
     * Encode the provided data to JSON.
427
     *
428
     * @param mixed $data
429
     * @return string
430
     * @throws InvalidArgumentException if unable to encode the $data to JSON.
431
     */
432 22
    protected function jsonEncode($data)
433
    {
434 22
        if (is_resource($data)) {
435
            throw new InvalidArgumentException('Cannot JSON encode resources');
436
        }
437
438
        // Clear json_last_error()
439 22
        json_encode(null);
440
441 22
        $json = json_encode($data, JsonResponse::DEFAULT_JSON_FLAGS);
442
443 22
        if (JSON_ERROR_NONE !== json_last_error()) {
444
            throw new InvalidArgumentException(sprintf(
445
                'Unable to encode data to JSON in %s: %s',
446
                __CLASS__,
447
                json_last_error_msg()
448
            ));
449
        }
450
451 22
        return $json;
452
    }
453
}