1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Carpediem\JSend; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use JsonSerializable; |
7
|
|
|
use UnexpectedValueException; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* A Immutable Value Object Class to represent a JSend object |
11
|
|
|
*/ |
12
|
|
|
class JSend implements JsonSerializable |
13
|
|
|
{ |
14
|
|
|
const STATUS_SUCCESS = 'success'; |
15
|
|
|
|
16
|
|
|
const STATUS_ERROR = 'error'; |
17
|
|
|
|
18
|
|
|
const STATUS_FAIL = 'fail'; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* JSend status |
22
|
|
|
* |
23
|
|
|
* @var string |
24
|
|
|
*/ |
25
|
|
|
protected $status; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* JSend Data |
29
|
|
|
* |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $data = []; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* JSend Error Message |
36
|
|
|
* |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
protected $errorMessage = ''; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* JSend Error Code |
43
|
|
|
* @var int|null |
44
|
|
|
*/ |
45
|
|
|
protected $errorCode; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* New Instance |
49
|
|
|
* |
50
|
|
|
* @param string $status |
51
|
|
|
* @param array $data |
52
|
|
|
* @param string $errorMessage |
53
|
|
|
* @param int $errorCode |
54
|
|
|
*/ |
55
|
|
|
public function __construct($status, array $data = null, $errorMessage = null, $errorCode = null) |
56
|
|
|
{ |
57
|
|
|
$this->status = $this->filterStatus($status); |
58
|
|
|
$this->data = $data ?: []; |
59
|
|
|
$this->filterError($errorMessage, $errorCode); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Filter and Validate the JSend Status |
64
|
|
|
* |
65
|
|
|
* @param string $status |
66
|
|
|
* |
67
|
|
|
* @throws UnexpectedValueException If the status value does not conform to JSend Spec. |
68
|
|
|
* |
69
|
|
|
* @return string |
70
|
|
|
*/ |
71
|
|
|
protected function filterStatus($status) |
72
|
|
|
{ |
73
|
|
|
$res = [self::STATUS_SUCCESS => 1, self::STATUS_ERROR => 1, self::STATUS_FAIL => 1]; |
74
|
|
|
if (isset($res[$status])) { |
75
|
|
|
return $status; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
throw new UnexpectedValueException('The given status does not conform to Jsend specification'); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Filter and Validate the JSend Error properties |
83
|
|
|
* |
84
|
|
|
* @param string $errorMessage |
85
|
|
|
* @param int $errorCode |
86
|
|
|
*/ |
87
|
|
|
protected function filterError($errorMessage, $errorCode) |
88
|
|
|
{ |
89
|
|
|
if (self::STATUS_ERROR != $this->status) { |
90
|
|
|
return; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
$this->errorMessage = $this->validateType('string', 'is_string', $errorMessage); |
94
|
|
|
if (!is_null($errorCode)) { |
95
|
|
|
$this->errorCode = $this->validateType('numeric', 'is_numeric', $errorCode); |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Validate a Type |
101
|
|
|
* |
102
|
|
|
* @param string $type |
103
|
|
|
* @param callable $func |
104
|
|
|
* @param mixed $str |
105
|
|
|
* |
106
|
|
|
* @throws UnexpectedValueException If the data value does not conform to the specified type |
107
|
|
|
* |
108
|
|
|
* @return mixed |
109
|
|
|
*/ |
110
|
|
|
protected function validateType($type, callable $func, $str) |
111
|
|
|
{ |
112
|
|
|
if ($func($str)) { |
113
|
|
|
return $str; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
throw new UnexpectedValueException(sprintf( |
117
|
|
|
'Expected data to be a %s; received "%s"', |
118
|
|
|
$type, |
119
|
|
|
(is_object($str) ? get_class($str) : gettype($str)) |
120
|
|
|
)); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Returns the JSend status |
125
|
|
|
* |
126
|
|
|
* @return string |
127
|
|
|
*/ |
128
|
|
|
public function getStatus() |
129
|
|
|
{ |
130
|
|
|
return $this->status; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Returns the JSend data |
135
|
|
|
* |
136
|
|
|
* @return array |
137
|
|
|
*/ |
138
|
|
|
public function getData() |
139
|
|
|
{ |
140
|
|
|
return $this->data; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Returns the JSend error message |
145
|
|
|
* |
146
|
|
|
* @return string |
147
|
|
|
*/ |
148
|
|
|
public function getErrorMessage() |
149
|
|
|
{ |
150
|
|
|
return $this->errorMessage; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Returns the JSend error code |
155
|
|
|
* |
156
|
|
|
* @return int|null |
157
|
|
|
*/ |
158
|
|
|
public function getErrorCode() |
159
|
|
|
{ |
160
|
|
|
return $this->errorCode; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Returns true if the status is success |
165
|
|
|
* |
166
|
|
|
* @return bool |
167
|
|
|
*/ |
168
|
|
|
public function isSuccess() |
169
|
|
|
{ |
170
|
|
|
return self::STATUS_SUCCESS === $this->status; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Returns true if the status is fail |
175
|
|
|
* |
176
|
|
|
* @return bool |
177
|
|
|
*/ |
178
|
|
|
public function isFail() |
179
|
|
|
{ |
180
|
|
|
return self::STATUS_FAIL === $this->status; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Returns true if the status is error |
185
|
|
|
* |
186
|
|
|
* @return bool |
187
|
|
|
*/ |
188
|
|
|
public function isError() |
189
|
|
|
{ |
190
|
|
|
return self::STATUS_ERROR === $this->status; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @inheritdoc |
195
|
|
|
*/ |
196
|
|
|
public function __toString() |
197
|
|
|
{ |
198
|
|
|
return json_encode($this, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @inheritdoc |
203
|
|
|
*/ |
204
|
|
|
public function jsonSerialize() |
205
|
|
|
{ |
206
|
|
|
return $this->toArray(); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Transcode the JSend object into an array |
211
|
|
|
* |
212
|
|
|
* @return array |
213
|
|
|
*/ |
214
|
|
|
public function toArray() |
215
|
|
|
{ |
216
|
|
|
$arr = ['status' => $this->status, 'data' => $this->data ?: null]; |
217
|
|
|
if (self::STATUS_ERROR !== $this->status) { |
218
|
|
|
return $arr; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
$arr['message'] = $this->errorMessage; |
222
|
|
|
if (!is_null($this->errorCode)) { |
223
|
|
|
$arr['code'] = $this->errorCode; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
if (is_null($arr['data'])) { |
227
|
|
|
unset($arr['data']); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $arr; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @inheritdoc |
235
|
|
|
*/ |
236
|
|
|
public function __debugInfo() |
237
|
|
|
{ |
238
|
|
|
return $this->toArray(); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Encode and Send the JSend object as an HTTP Response |
243
|
|
|
* |
244
|
|
|
* @param array $headers Optional headers to add to the response |
245
|
|
|
* |
246
|
|
|
* @return string |
247
|
|
|
*/ |
248
|
|
|
public function send(array $headers = []) |
249
|
|
|
{ |
250
|
|
|
$headers = $this->filterHeaders($headers); |
251
|
|
|
$headers[] = 'Content-Type: application/json;charset=utf-8'; |
252
|
|
|
foreach ($headers as $header) { |
253
|
|
|
header($header); |
254
|
|
|
} |
255
|
|
|
echo $this; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Filter Submitted Headers |
260
|
|
|
* |
261
|
|
|
* @param array $headers a Collection of key/value headers |
262
|
|
|
* |
263
|
|
|
* @return array |
264
|
|
|
*/ |
265
|
|
|
protected function filterHeaders(array $headers) |
266
|
|
|
{ |
267
|
|
|
$formattedHeaders = []; |
268
|
|
|
foreach ($headers as $name => $value) { |
269
|
|
|
$formattedHeaders[] = $this->validateHeaderName($name).': '.$this->validateHeaderValue($value); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return $formattedHeaders; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Validate Header name |
277
|
|
|
* |
278
|
|
|
* @param string $name |
279
|
|
|
* |
280
|
|
|
* @throws InvalidArgumentException if the header name is invalid |
281
|
|
|
* |
282
|
|
|
* @return string |
283
|
|
|
*/ |
284
|
|
|
protected function validateHeaderName($name) |
285
|
|
|
{ |
286
|
|
|
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) { |
287
|
|
|
throw new InvalidArgumentException('Invalid header name'); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
return $name; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Validate Header value |
295
|
|
|
* |
296
|
|
|
* @param string $value |
297
|
|
|
* |
298
|
|
|
* @throws InvalidArgumentException if the header value is invalid |
299
|
|
|
* |
300
|
|
|
* @return string |
301
|
|
|
*/ |
302
|
|
|
protected function validateHeaderValue($value) |
303
|
|
|
{ |
304
|
|
|
if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value) |
305
|
|
|
|| preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value) |
306
|
|
|
) { |
307
|
|
|
throw new InvalidArgumentException('Invalid header value'); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $value; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Returns an instance with the specified status. |
315
|
|
|
* |
316
|
|
|
* This method MUST retain the state of the current instance, and return |
317
|
|
|
* an instance that contains the specified status. |
318
|
|
|
* |
319
|
|
|
* @param string $status The status to use with the new instance. |
320
|
|
|
* |
321
|
|
|
* @return static A new instance with the specified status. |
322
|
|
|
*/ |
323
|
|
|
public function withStatus($status) |
|
|
|
|
324
|
|
|
{ |
325
|
|
|
if ($status === $this->status) { |
326
|
|
|
return $this; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
return new static($status, $this->data, $this->errorMessage, $this->errorCode); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Returns an instance with the specified data. |
334
|
|
|
* |
335
|
|
|
* This method MUST retain the state of the current instance, and return |
336
|
|
|
* an instance that contains the specified data. |
337
|
|
|
* |
338
|
|
|
* @param array $data The data to use with the new instance. |
339
|
|
|
* |
340
|
|
|
* @return static A new instance with the specified data. |
341
|
|
|
*/ |
342
|
|
|
public function withData(array $data) |
|
|
|
|
343
|
|
|
{ |
344
|
|
|
if ($data === $this->data) { |
345
|
|
|
return $this; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
return new static($this->status, $data, $this->errorMessage, $this->errorCode); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Returns an instance with the specified error message and error code. |
353
|
|
|
* |
354
|
|
|
* This method MUST retain the state of the current instance, and return |
355
|
|
|
* an instance that contains the specified error message and error code. |
356
|
|
|
* |
357
|
|
|
* @param string $errorMessage The error message to use with the new instance. |
358
|
|
|
* @param int|null $errorCode The error code to use with the new instance. |
359
|
|
|
* |
360
|
|
|
* @return static A new instance with the specified status. |
361
|
|
|
*/ |
362
|
|
|
public function withError($errorMessage, $errorCode = null) |
363
|
|
|
{ |
364
|
|
|
if ($errorMessage == $this->errorMessage && $errorCode == $this->errorCode) { |
365
|
|
|
return $this; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
return new static($this->status, $this->data, $errorMessage, $errorCode); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Returns a successful JSend object with the specified data |
373
|
|
|
* |
374
|
|
|
* @param array $data The data to use with the new instance. |
375
|
|
|
* |
376
|
|
|
* @return static A new succesful instance with the specified data. |
377
|
|
|
*/ |
378
|
|
|
public static function success(array $data = []) |
379
|
|
|
{ |
380
|
|
|
return new static(static::STATUS_SUCCESS, $data); |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* Returns a failed JSend object with the specified data |
385
|
|
|
* |
386
|
|
|
* @param array $data The data to use with the new instance. |
387
|
|
|
* |
388
|
|
|
* @return static A new failed instance with the specified data. |
389
|
|
|
*/ |
390
|
|
|
public static function fail(array $data = []) |
391
|
|
|
{ |
392
|
|
|
return new static(static::STATUS_FAIL, $data); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* Returns a error JSend object with the specified error message and error code. |
397
|
|
|
* |
398
|
|
|
* @param string $errorMessage The error message to use with the new instance. |
399
|
|
|
* @param int|null $errorCode The error code to use with the new instance. |
400
|
|
|
* @param array $data The optional data to use with the new instance. |
401
|
|
|
* |
402
|
|
|
* @return static A new failed instance with the specified data. |
403
|
|
|
*/ |
404
|
|
|
public static function error($errorMessage, $errorCode = null, $data = null) |
405
|
|
|
{ |
406
|
|
|
return new static(static::STATUS_ERROR, $data, $errorMessage, $errorCode); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Returns a new instance from a JSON string |
411
|
|
|
* |
412
|
|
|
* @param string $json The string being decoded |
413
|
|
|
* @param int $depth User specified recursion depth. |
414
|
|
|
* @param int $options Bitmask of JSON decode options |
415
|
|
|
* |
416
|
|
|
* @throws InvalidArgumentException If the string can not be decode |
417
|
|
|
* |
418
|
|
|
* @return static |
419
|
|
|
*/ |
420
|
|
|
public static function createFromString($json, $depth = 512, $options = 0) |
421
|
|
|
{ |
422
|
|
|
$raw = json_decode($json, true, $depth, $options); |
423
|
|
|
if (JSON_ERROR_NONE !== json_last_error()) { |
424
|
|
|
throw new InvalidArgumentException(sprintf( |
425
|
|
|
'Unable to decode JSON to array in %s: %s', |
426
|
|
|
__CLASS__, |
427
|
|
|
json_last_error_msg() |
428
|
|
|
)); |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
return static::createFromArray($raw); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* @inheritdoc |
436
|
|
|
*/ |
437
|
|
|
public static function __set_state(array $properties) |
438
|
|
|
{ |
439
|
|
|
return static::createFromArray($properties); |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
/** |
443
|
|
|
* Returns a new instance from an array |
444
|
|
|
* |
445
|
|
|
* @param array $arr The array to build a new JSend object with |
446
|
|
|
* |
447
|
|
|
* @return static |
448
|
|
|
*/ |
449
|
|
|
public static function createFromArray(array $arr) |
450
|
|
|
{ |
451
|
|
|
$defaultValues = ['status' => null, 'data' => null, 'message' => null, 'code' => null]; |
452
|
|
|
$arr = array_replace($defaultValues, array_intersect_key($arr, $defaultValues)); |
453
|
|
|
|
454
|
|
|
return new static($arr['status'], $arr['data'], $arr['message'], $arr['code']); |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.