GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

JSend::withError()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 14
c 0
b 0
f 0
ccs 9
cts 9
cp 1
rs 9.7998
cc 4
nc 2
nop 2
crap 4
1
<?php
2
3
/**
4
 * This file is part of Carpediem\JSend, a JSend Response object.
5
 *
6
 * @copyright Carpe Diem. All rights reserved
7
 * @license MIT See LICENSE.md at the root of the project for more info
8
 */
9
10
declare(strict_types=1);
11
12
namespace Carpediem\JSend;
13
14
use JsonSerializable;
15
use TypeError;
16
use const JSON_ERROR_NONE;
17
use const JSON_HEX_AMP;
18
use const JSON_HEX_APOS;
19
use const JSON_HEX_QUOT;
20
use function array_filter;
21
use function array_merge;
22
use function header;
23
use function is_array;
24
use function is_object;
25
use function is_scalar;
26
use function json_decode;
27
use function json_encode;
28
use function json_last_error;
29
use function json_last_error_msg;
30
use function method_exists;
31
use function preg_match;
32
use function sprintf;
33
use function strlen;
34
use function trim;
35
36
/**
37
 * A Immutable Value Object Class to represent a JSend object.
38
 */
39
final class JSend implements JsonSerializable
40
{
41
    const STATUS_SUCCESS = 'success';
42
43
    const STATUS_ERROR = 'error';
44
45
    const STATUS_FAIL = 'fail';
46
47
    /**
48
     * JSend status.
49
     *
50
     * @var string
51
     */
52
    private $status;
53
54
    /**
55
     * JSend Data.
56
     *
57
     * @var array
58
     */
59
    private $data;
60
61
    /**
62
     * JSend Error Message.
63
     *
64
     * @var string|null
65
     */
66
    private $errorMessage;
67
68
    /**
69
     * JSend Error Code.
70
     *
71
     * @var int|null
72
     */
73
    private $errorCode;
74
75
    /**
76
     * Returns a new instance from a JSON string.
77
     *
78
     * @throws Exception If the string can not be decode
79
     */
80 9
    public static function fromJSON($json, int $depth = 512, int $options = 0): self
81
    {
82 9
        if ($json instanceof JsonSerializable) {
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...
83 3
            return self::fromArray((array) $json->jsonSerialize());
84
        }
85
86 9
        if (!is_scalar($json) && !method_exists($json, '__toString')) {
87 3
            throw new TypeError('The json argument must be a string, a stringable object or an object implementing the JsonSerializable interface');
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with 'The json argument must ...Serializable interface'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
88
        }
89
90 6
        $raw = json_decode((string) $json, true, $depth, $options);
91 6
        if (JSON_ERROR_NONE === json_last_error()) {
92 3
            return static::fromArray((array) $raw);
93
        }
94
95 3
        throw new Exception(sprintf('Unable to decode the submitted JSON string: %s', json_last_error_msg()));
96
    }
97
98
    /**
99
     * Returns a new instance from an array.
100
     */
101 6
    public static function fromArray(array $arr): self
102
    {
103 6
        return new self($arr['status'] ?? '', $arr['data'] ?? null, $arr['message'] ?? null, $arr['code'] ?? null);
104
    }
105
106
    /**
107
     * Returns a successful JSend object with the specified data.
108
     *
109
     * @param null|mixed $data
110
     */
111 102
    public static function success($data = null): self
112
    {
113 102
        return new self(self::STATUS_SUCCESS, $data);
114
    }
115
116
    /**
117
     * Returns a failed JSend object with the specified data.
118
     *
119
     * @param null|mixed $data
120
     */
121 3
    public static function fail($data = null): self
122
    {
123 3
        return new self(self::STATUS_FAIL, $data);
124
    }
125
126
    /**
127
     * Returns a error JSend object with the specified error message and error code.
128
     *
129
     * @param null|mixed $data
130
     */
131 15
    public static function error($errorMessage, int $errorCode = null, $data = null): self
132
    {
133 15
        return new self(self::STATUS_ERROR, $data, $errorMessage, $errorCode);
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 3
    public static function __set_state(array $prop)
140
    {
141 3
        return new self($prop['status'], $prop['data'], $prop['errorMessage'], $prop['errorCode']);
142
    }
143
144
    /**
145
     * New Instance.
146
     *
147
     * @param null|mixed $data
148
     * @param null|mixed $errorMessage
149
     */
150 102
    private function __construct(
151
        string $status,
152
        $data = null,
153
        $errorMessage = null,
154
        int $errorCode = null
155
    ) {
156 102
        $this->status = $this->filterStatus($status);
157 102
        $this->data = $this->filterData($data);
158 102
        list($this->errorMessage, $this->errorCode) = $this->filterError($errorMessage, $errorCode);
159 102
    }
160
161
    /**
162
     * Filter and Validate the JSend Status.
163
     *
164
     * @throws Exception If the status value does not conform to JSend Spec.
165
     */
166 102
    private function filterStatus(string $status): string
167
    {
168 102
        static $res = [self::STATUS_SUCCESS => 1, self::STATUS_ERROR => 1, self::STATUS_FAIL => 1];
169 102
        if (isset($res[$status])) {
170 102
            return $status;
171
        }
172
173 3
        throw new Exception('The given status does not conform to Jsend specification');
174
    }
175
176
    /**
177
     * Filter and Validate the JSend Data.
178
     *
179
     * @param mixed $data The data can be
180
     *                    <ul>
181
     *                    <li>An Array
182
     *                    <li>A JsonSerializable object
183
     *                    <li>null
184
     *                    </ul>
185
     *
186
     * @throws Exception If the input does not conform to one of the valid type
187
     */
188 102
    private function filterData($data): array
189
    {
190 102
        if (null === $data) {
191 15
            return [];
192
        }
193
194 102
        if (is_array($data)) {
195 102
            return $data;
196
        }
197
198 9
        if (!$data instanceof JsonSerializable) {
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...
199 3
            throw new TypeError('The data must be an array, a JsonSerializable object or null');
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with 'The data must be an arr...lizable object or null'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
200
        }
201
202 6
        if (is_array($res = $data->jsonSerialize())) {
203 3
            return $res;
204
        }
205
206 3
        throw new Exception(sprintf('The JsonSerializable object must return an array %s returned', is_object($res) ? get_class($res) : gettype($res)));
207
    }
208
209
    /**
210
     * Filter and Validate the JSend Error properties.
211
     */
212 102
    private function filterError($errorMessage, int $errorCode = null): array
213
    {
214 102
        if (self::STATUS_ERROR === $this->status) {
215 15
            return [$this->filterErrorMessage($errorMessage), $errorCode];
216
        }
217
218 102
        return [null, null];
219
    }
220
221
    /**
222
     * Validate a string.
223
     *
224
     * @throws Exception If the data value is not a empty string
225
     */
226 15
    private function filterErrorMessage($str): string
227
    {
228 15
        if (!is_scalar($str) && !(is_object($str) && method_exists($str, '__toString'))) {
229 3
            throw new Exception('The error message must be a scalar or a object implementing the __toString method.');
230
        }
231
232 12
        $str = (string) $str;
233 12
        if ('' !== trim($str)) {
234 9
            return $str;
235
        }
236
237 3
        throw new Exception('The error message can not be empty.');
238
    }
239
240
    /**
241
     * Returns the status.
242
     */
243 42
    public function getStatus(): string
244
    {
245 42
        return $this->status;
246
    }
247
248
    /**
249
     * Returns the data.
250
     */
251 39
    public function getData(): array
252
    {
253 39
        return $this->data;
254
    }
255
256
    /**
257
     * Returns the error message.
258
     *
259
     * @return null|string
260
     */
261 15
    public function getErrorMessage()
262
    {
263 15
        return $this->errorMessage;
264
    }
265
266
    /**
267
     * Returns the error code.
268
     *
269
     * @return null|int
270
     */
271 18
    public function getErrorCode()
272
    {
273 18
        return $this->errorCode;
274
    }
275
276
    /**
277
     * Returns whether the status is success.
278
     */
279 9
    public function isSuccess(): bool
280
    {
281 9
        return self::STATUS_SUCCESS === $this->status;
282
    }
283
284
    /**
285
     * Returns whether the status is fail.
286
     */
287 9
    public function isFail(): bool
288
    {
289 9
        return self::STATUS_FAIL === $this->status;
290
    }
291
292
    /**
293
     * Returns whether the status is error.
294
     */
295 15
    public function isError(): bool
296
    {
297 15
        return self::STATUS_ERROR === $this->status;
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303 9
    public function jsonSerialize()
304
    {
305 9
        return $this->toArray();
306
    }
307
308
    /**
309
     * Returns the array representation.
310
     */
311 57
    public function toArray(): array
312
    {
313 57
        $arr = ['status' => $this->status, 'data' => $this->data ?: null];
314 57
        if (self::STATUS_ERROR !== $this->status) {
315 39
            return $arr;
316
        }
317
318 18
        $filter = function ($value): bool {
319 18
            return null !== $value;
320 18
        };
321
322 18
        $arr['message'] = (string) $this->errorMessage;
323 18
        $arr['code'] = $this->errorCode;
324
325 18
        return array_filter($arr, $filter);
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331 48
    public function __toString()
332
    {
333 48
        return json_encode($this->toArray(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
334
    }
335
336
    /**
337
     * {@inheritdoc}
338
     */
339 3
    public function __debugInfo()
340
    {
341 3
        return $this->toArray();
342
    }
343
344
    /**
345
     * Output all the data of the JSend object.
346
     *
347
     * @param array $headers Optional headers to add to the response
348
     *
349
     * @return int Returns the number of characters read from the JSend object
350
     *             and passed throught to the output
351
     */
352 12
    public function send(array $headers = []): int
353
    {
354 12
        $body = $this->__toString();
355 12
        $length = strlen($body);
356 12
        $headers = $this->filterHeaders(array_merge([
357 12
            'Content-Type' => 'application/json;charset=utf-8',
358 12
            'Content-Length' => (string) $length,
359 12
        ], $headers));
360
361 3
        foreach ($headers as $header) {
362 3
            header($header);
363
        }
364 3
        echo $body;
365
366 3
        return $length;
367
    }
368
369
    /**
370
     * Filter Submitted Headers.
371
     *
372
     * @param array $headers a Collection of key/value headers
373
     */
374 12
    private function filterHeaders(array $headers): array
375
    {
376 12
        $formattedHeaders = [];
377 12
        foreach ($headers as $name => $value) {
378 12
            $formattedHeaders[] = $this->validateHeaderName($name).': '.$this->validateHeaderValue($value);
379
        }
380
381 3
        return $formattedHeaders;
382
    }
383
384
    /**
385
     * Validate Header name.
386
     *
387
     * @throws Exception if the header name is invalid
388
     */
389 12
    private function validateHeaderName(string $name): string
390
    {
391 12
        if (preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
392 12
            return $name;
393
        }
394
395 3
        throw new Exception(sprintf('Invalid header name: %s', $name));
396
    }
397
398
    /**
399
     * Validate Header value.
400
     *
401
     * @throws Exception if the header value is invalid
402
     */
403 12
    private function validateHeaderValue(string $value): string
404
    {
405 12
        if (!preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)
406 12
            && !preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)
407
        ) {
408 12
            return $value;
409
        }
410
411 6
        throw new Exception(sprintf('Invalid header value: %s', $value));
412
    }
413
414
    /**
415
     * Returns an instance with the specified status.
416
     *
417
     * This method MUST retain the state of the current instance, and return
418
     * an instance that contains the specified status.
419
     */
420 6
    public function withStatus(string $status): self
421
    {
422 6
        $status = $this->filterStatus($status);
423 6
        if ($status === $this->status) {
424 3
            return $this;
425
        }
426
427 3
        return new self($status, $this->data, $this->errorMessage, $this->errorCode);
428
    }
429
430
    /**
431
     * Returns an instance with the specified data.
432
     *
433
     * This method MUST retain the state of the current instance, and return
434
     * an instance that contains the specified data.
435
     */
436 6
    public function withData($data): self
437
    {
438 6
        $data = $this->filterData($data);
439 6
        if ($data === $this->data) {
440 3
            return $this;
441
        }
442
443 3
        $clone = clone $this;
444 3
        $clone->data = $data;
445
446 3
        return $clone;
447
    }
448
449
    /**
450
     * Returns an instance with the specified error message and error code.
451
     *
452
     * This method MUST retain the state of the current instance, and return
453
     * an instance that contains the specified error message and error code.
454
     */
455 6
    public function withError($errorMessage, int $errorCode = null): self
456
    {
457 6
        list($errorMessage, $errorCode) = $this->filterError($errorMessage, $errorCode);
458 6
        if ($this->isError() && $errorMessage === $this->errorMessage && $errorCode === $this->errorCode) {
459 3
            return $this;
460
        }
461
462 3
        $clone = clone $this;
463 3
        $clone->errorMessage = $errorMessage;
464 3
        $clone->errorCode = $errorCode;
465 3
        $clone->status = self::STATUS_ERROR;
466
467 3
        return $clone;
468
    }
469
}
470