Passed
Pull Request — master (#4)
by
unknown
06:28
created

Response::jsonEncode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.8449

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 6
cts 11
cp 0.5455
rs 9.3142
c 0
b 0
f 0
cc 3
eloc 11
nc 3
nop 1
crap 3.8449
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\JsonResponse;
20
21
class Response implements PhpRestfulApiResponse
22
{
23
    use MessageTrait;
24
25
    const MIN_STATUS_CODE_VALUE = 100;
26
    const MAX_STATUS_CODE_VALUE = 599;
27
28
    /**
29
     * Map of standard HTTP status code/reason phrases
30
     *
31
     * @var array
32
     */
33
    private $phrases = [
34
        // INFORMATIONAL CODES
35
        100 => 'Continue',
36
        101 => 'Switching Protocols',
37
        102 => 'Processing',
38
        103 => 'Early Hints',
39
        // SUCCESS CODES
40
        200 => 'OK',
41
        201 => 'Created',
42
        202 => 'Accepted',
43
        203 => 'Non-Authoritative Information',
44
        204 => 'No Content',
45
        205 => 'Reset Content',
46
        206 => 'Partial Content',
47
        207 => 'Multi-Status',
48
        208 => 'Already Reported',
49
        226 => 'IM Used',
50
        // REDIRECTION CODES
51
        300 => 'Multiple Choices',
52
        301 => 'Moved Permanently',
53
        302 => 'Found',
54
        303 => 'See Other',
55
        304 => 'Not Modified',
56
        305 => 'Use Proxy',
57
        306 => 'Switch Proxy', // Deprecated to 306 => '(Unused)'
58
        307 => 'Temporary Redirect',
59
        308 => 'Permanent Redirect',
60
        // CLIENT ERROR
61
        400 => 'Bad Request',
62
        401 => 'Unauthorized',
63
        402 => 'Payment Required',
64
        403 => 'Forbidden',
65
        404 => 'Not Found',
66
        405 => 'Method Not Allowed',
67
        406 => 'Not Acceptable',
68
        407 => 'Proxy Authentication Required',
69
        408 => 'Request Timeout',
70
        409 => 'Conflict',
71
        410 => 'Gone',
72
        411 => 'Length Required',
73
        412 => 'Precondition Failed',
74
        413 => 'Payload Too Large',
75
        414 => 'URI Too Long',
76
        415 => 'Unsupported Media Type',
77
        416 => 'Range Not Satisfiable',
78
        417 => 'Expectation Failed',
79
        418 => 'I\'m a teapot',
80
        421 => 'Misdirected Request',
81
        422 => 'Unprocessable Entity',
82
        423 => 'Locked',
83
        424 => 'Failed Dependency',
84
        425 => 'Unordered Collection',
85
        426 => 'Upgrade Required',
86
        428 => 'Precondition Required',
87
        429 => 'Too Many Requests',
88
        431 => 'Request Header Fields Too Large',
89
        444 => 'Connection Closed Without Response',
90
        451 => 'Unavailable For Legal Reasons',
91
        // SERVER ERROR
92
        499 => 'Client Closed Request',
93
        500 => 'Internal Server Error',
94
        501 => 'Not Implemented',
95
        502 => 'Bad Gateway',
96
        503 => 'Service Unavailable',
97
        504 => 'Gateway Timeout',
98
        505 => 'HTTP Version Not Supported',
99
        506 => 'Variant Also Negotiates',
100
        507 => 'Insufficient Storage',
101
        508 => 'Loop Detected',
102
        510 => 'Not Extended',
103
        511 => 'Network Authentication Required',
104
        599 => 'Network Connect Timeout Error',
105
    ];
106
107
    /**
108
     * @var string
109
     */
110
    private $reasonPhrase = '';
111
112
    /**
113
     * @var int
114
     */
115
    private $statusCode;
116
117
    /**
118
     * @var int|string
119
     */
120
    private $errorCode;
121
122
    /**
123
     * Response constructor.
124
     * @param string $body
125
     * @param int $status
126
     * @param int $errorCode
127
     * @param array $headers
128
     */
129 27
    public function __construct($body = 'php://memory', int $status = 200, $errorCode = null, array $headers = [])
130
    {
131 27
        $this->setStatusCode($status);
132 27
        $this->setErrorCode($errorCode);
133 27
        $this->stream = $this->getStream($body, 'wb+');
134 27
        $this->setHeaders($headers);
135 27
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140 24
    public function getStatusCode()
141
    {
142 24
        return $this->statusCode;
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148 19
    public function getReasonPhrase()
149
    {
150 19
        if (! $this->reasonPhrase
151 19
            && isset($this->phrases[$this->statusCode])
152
        ) {
153 19
            $this->reasonPhrase = $this->phrases[$this->statusCode];
154
        }
155
156 19
        return $this->reasonPhrase;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 1
    public function withStatus($code, $reasonPhrase = '')
163
    {
164 1
        $new = clone $this;
165 1
        $new->setStatusCode($code);
166 1
        $new->reasonPhrase = $reasonPhrase;
167 1
        return $new;
168
    }
169
170
    /**
171
     * @param array|null $data
172
     * @param $code
173
     * @param array $headers
174
     * @return Response
175
     */
176 3
    public function withArray($data, $code = 200, array $headers = [])
177
    {
178 3
        $new = clone $this;
179 3
        $new->setStatusCode($code);
180 3
        $new->getBody()->write($this->jsonEncode($data));
181 3
        $new = $new->withHeader('Content-Type', 'application/json');
182 3
        $new->headers = array_merge($new->headers, $headers);
183 3
        return $new;
184
    }
185
186
    /**
187
     * @param $data
188
     * @param TransformerAbstract|callable $transformer
189
     * @param int $code
190
     * @param null $resourceKey
191
     * @param array $meta
192
     * @param array $headers
193
     * @return Response
194
     */
195 1
    public function withItem($data, $transformer, $code = 200, $resourceKey = null, $meta = [], array $headers = [])
196
    {
197 1
        $resource = new Item($data, $transformer, $resourceKey);
198
199 1
        foreach ($meta as $metaKey => $metaValue) {
200
            $resource->setMetaValue($metaKey, $metaValue);
201
        }
202
203 1
        $manager = new Manager();
204
205 1
        $rootScope = $manager->createData($resource);
206
207 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...
208
    }
209
210
    /**
211
     * @param $data
212
     * @param TransformerAbstract|callable $transformer
213
     * @param int $code
214
     * @param null $resourceKey
215
     * @param Cursor|null $cursor
216
     * @param array $meta
217
     * @param array $headers
218
     * @return Response
219
     */
220 1
    public function withCollection($data, $transformer, $code = 200, $resourceKey = null, Cursor $cursor = null, $meta = [], array $headers = [])
221
    {
222 1
        $resource = new Collection($data, $transformer, $resourceKey);
223
224 1
        foreach ($meta as $metaKey => $metaValue) {
225
            $resource->setMetaValue($metaKey, $metaValue);
226
        }
227
228 1
        if (!is_null($cursor)) {
229
            $resource->setCursor($cursor);
230
        }
231
232 1
        $manager = new Manager();
233
234 1
        $rootScope = $manager->createData($resource);
235
236 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...
237
    }
238
239
    /**
240
     * Response for errors
241
     *
242
     * @param string|array $message
243
     * @param int $statusCode
244
     * @param int|string $errorCode
245
     * @param array  $headers
246
     * @return mixed
247
     */
248 19
    public function withError($message, int $statusCode, $errorCode = null, array $headers = [])
249
    {
250 19
        $new = clone $this;
251 19
        $new->setStatusCode($statusCode);
252 19
        $new->setErrorCode($errorCode);
253 19
        $new->getBody()->write(
254 19
            $this->jsonEncode(
255
                [
256 19
                    'error' => array_filter([
257 19
                        'http_code' => $new->statusCode,
258 19
                        'code' => $errorCode,
259 19
                        'phrase' => $new->getReasonPhrase(),
260 19
                        'message' => $message
261
                    ])
262
                ]
263
            )
264
        );
265 19
        $new = $new->withHeader('Content-Type', 'application/json');
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
    private 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
    private 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
}