1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of the reva2/jsonapi. |
4
|
|
|
* |
5
|
|
|
* (c) Sergey Revenko <[email protected]> |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
|
12
|
|
|
namespace Reva2\JsonApi\Decoders; |
13
|
|
|
|
14
|
|
|
use Neomerx\JsonApi\Document\Error; |
15
|
|
|
use Neomerx\JsonApi\Exceptions\JsonApiException; |
16
|
|
|
use Reva2\JsonApi\Contracts\Decoders\DataParserInterface; |
17
|
|
|
use Reva2\JsonApi\Contracts\Decoders\DecodersFactoryInterface; |
18
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccess; |
19
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessor; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Data parser |
23
|
|
|
* |
24
|
|
|
* @package Reva2\JsonApi\Decoders |
25
|
|
|
* @author Sergey Revenko <[email protected]> |
26
|
|
|
*/ |
27
|
|
|
class DataParser implements DataParserInterface |
28
|
|
|
{ |
29
|
|
|
const ERROR_CODE = 'ee2c1d49-ba40-4077-a6bb-b06baceb3e97'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Current path |
33
|
|
|
* |
34
|
|
|
* @var \SplStack |
35
|
|
|
*/ |
36
|
|
|
protected $path; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Resource decoders factory |
40
|
|
|
* |
41
|
|
|
* @var DecodersFactoryInterface |
42
|
|
|
*/ |
43
|
|
|
protected $factory; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var PropertyAccessor |
47
|
|
|
*/ |
48
|
|
|
protected $accessor; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Constructor |
52
|
|
|
* |
53
|
|
|
* @param DecodersFactoryInterface $factory |
54
|
|
|
*/ |
55
|
14 |
|
public function __construct(DecodersFactoryInterface $factory) |
56
|
|
|
{ |
57
|
14 |
|
$this->factory = $factory; |
58
|
14 |
|
$this->accessor = PropertyAccess::createPropertyAccessor(); |
59
|
|
|
|
60
|
14 |
|
$this->initPathStack(); |
61
|
14 |
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @inheritdoc |
65
|
|
|
*/ |
66
|
13 |
|
public function setPath($path) |
67
|
|
|
{ |
68
|
13 |
|
$this->path->push($this->preparePathSegment($path)); |
69
|
|
|
|
70
|
12 |
|
return $this; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @inheritdoc |
75
|
|
|
*/ |
76
|
12 |
|
public function restorePath() |
77
|
|
|
{ |
78
|
12 |
|
$this->path->pop(); |
79
|
|
|
|
80
|
12 |
|
return $this; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @inheritdoc |
85
|
|
|
*/ |
86
|
1 |
|
public function getPath() |
87
|
|
|
{ |
88
|
1 |
|
$segments = []; |
89
|
1 |
|
foreach ($this->path as $segment) { |
90
|
|
|
$segments[] = $segment; |
91
|
1 |
|
} |
92
|
|
|
|
93
|
1 |
|
return '/' . implode('/', array_reverse($segments)); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @inheritdoc |
98
|
|
|
*/ |
99
|
12 |
|
public function hasValue($data, $path) |
100
|
|
|
{ |
101
|
12 |
|
return $this->accessor->isReadable($data, $path); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* @inheritdoc |
106
|
|
|
*/ |
107
|
12 |
|
public function getValue($data, $path) |
108
|
|
|
{ |
109
|
12 |
|
return $this->accessor->getValue($data, $path); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @inheritdoc |
114
|
|
|
*/ |
115
|
7 |
|
public function parseString($data, $path) |
116
|
|
|
{ |
117
|
7 |
|
$this->setPath($path); |
118
|
|
|
|
119
|
7 |
|
$pathValue = null; |
120
|
7 |
|
if ($this->hasValue($data, $path)) { |
121
|
7 |
|
$value = $this->getValue($data, $path); |
122
|
7 |
View Code Duplication |
if ((null === $value) || (is_string($value))) { |
|
|
|
|
123
|
7 |
|
$pathValue = $value; |
124
|
7 |
|
} else { |
125
|
1 |
|
throw new \InvalidArgumentException( |
126
|
1 |
|
sprintf("Value expected to be a string, but %s given", gettype($value)), |
127
|
|
|
400 |
128
|
1 |
|
); |
129
|
|
|
} |
130
|
7 |
|
} |
131
|
7 |
|
$this->restorePath(); |
132
|
|
|
|
133
|
7 |
|
return $pathValue; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* @inheritdoc |
138
|
|
|
*/ |
139
|
2 |
|
public function parseInt($data, $path) |
140
|
|
|
{ |
141
|
2 |
|
return $this->parseNumeric($data, $path, 'int'); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @inheritdoc |
146
|
|
|
*/ |
147
|
2 |
|
public function parseFloat($data, $path) |
148
|
|
|
{ |
149
|
2 |
|
return $this->parseNumeric($data, $path, 'float'); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* @inheritdoc |
154
|
|
|
*/ |
155
|
2 |
|
public function parseBool($data, $path) |
156
|
|
|
{ |
157
|
2 |
|
$this->setPath($path); |
158
|
|
|
|
159
|
2 |
|
$pathValue = null; |
160
|
2 |
|
if ($this->hasValue($data, $path)) { |
161
|
2 |
|
$value = $this->getValue($data, $path); |
162
|
2 |
|
if ((null === $value) || (is_bool($value))) { |
163
|
2 |
|
$pathValue = $value; |
164
|
2 |
|
} elseif (is_string($value)) { |
165
|
1 |
|
$pathValue = (in_array($value, ['true', 'yes', 'y', 'on', 'enabled'])) ? true : false; |
166
|
1 |
|
} elseif (is_numeric($value)) { |
167
|
1 |
|
$pathValue = (bool) $value; |
168
|
1 |
|
} else { |
169
|
1 |
|
throw new \InvalidArgumentException( |
170
|
1 |
|
sprintf("Value expected to be a boolean, but %s given", gettype($value)), |
171
|
|
|
400 |
172
|
1 |
|
); |
173
|
|
|
} |
174
|
2 |
|
} |
175
|
|
|
|
176
|
2 |
|
$this->restorePath(); |
177
|
|
|
|
178
|
2 |
|
return $pathValue; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @inheritdoc |
183
|
|
|
*/ |
184
|
2 |
|
public function parseDateTime($data, $path, $format = 'Y-m-d') |
185
|
|
|
{ |
186
|
2 |
|
$this->setPath($path); |
187
|
|
|
|
188
|
2 |
|
$pathValue = null; |
189
|
2 |
|
if ($this->hasValue($data, $path)) { |
190
|
2 |
|
$value = $this->getValue($data, $path); |
191
|
2 |
|
if (null === $value) { |
192
|
|
|
$pathValue = $value; |
193
|
|
View Code Duplication |
} else { |
|
|
|
|
194
|
2 |
|
if (is_string($value)) { |
195
|
2 |
|
$pathValue = \DateTimeImmutable::createFromFormat($format, $value); |
196
|
2 |
|
} |
197
|
|
|
|
198
|
2 |
|
if (!$pathValue instanceof \DateTimeImmutable) { |
199
|
1 |
|
throw new \InvalidArgumentException( |
200
|
1 |
|
sprintf("Value expected to be a date/time string in '%s' format", $format), |
201
|
|
|
400 |
202
|
1 |
|
); |
203
|
|
|
} |
204
|
|
|
} |
205
|
2 |
|
} |
206
|
|
|
|
207
|
2 |
|
$this->restorePath(); |
208
|
|
|
|
209
|
2 |
|
return $pathValue; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @inheritdoc |
214
|
|
|
*/ |
215
|
1 |
|
public function parseArray($data, $path, $itemsParser) |
216
|
|
|
{ |
217
|
1 |
|
$this->setPath($path); |
218
|
|
|
|
219
|
1 |
|
$pathValue = null; |
220
|
1 |
|
if ($this->hasValue($data, $path)) { |
221
|
1 |
|
$value = $this->getValue($data, $path); |
222
|
1 |
|
if ((null !== $value) && (false === is_array($value))) { |
223
|
1 |
|
throw new \InvalidArgumentException( |
224
|
1 |
|
sprintf("Value expected to be an array, but %s given", gettype($value)), |
225
|
|
|
400 |
226
|
1 |
|
); |
227
|
1 |
|
} elseif (is_array($value)) { |
228
|
1 |
|
$pathValue = []; |
229
|
1 |
|
$keys = array_keys($value); |
230
|
1 |
|
foreach ($keys as $key) { |
231
|
1 |
|
$this->setPath($key); |
232
|
|
|
|
233
|
1 |
|
$pathValue[$key] = $this->parseArrayItem($value, $key, $itemsParser); |
234
|
|
|
|
235
|
1 |
|
$this->restorePath(); |
236
|
1 |
|
} |
237
|
1 |
|
} |
238
|
1 |
|
} |
239
|
|
|
|
240
|
1 |
|
$this->restorePath(); |
241
|
|
|
|
242
|
1 |
|
return $pathValue; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @inheritdoc |
247
|
|
|
*/ |
248
|
1 |
|
public function parseResource($data, $path, $resType) |
249
|
|
|
{ |
250
|
1 |
|
$this->setPath($path); |
251
|
|
|
|
252
|
1 |
|
$pathValue = null; |
253
|
1 |
|
if ($this->hasValue($data, $path)) { |
254
|
1 |
|
$this->setPath($path); |
255
|
|
|
|
256
|
1 |
|
$decoder = $this->factory->getResourceDecoder($resType); |
257
|
1 |
|
$pathValue = $decoder->decode($this->getValue($data, $path), $this); |
258
|
|
|
|
259
|
1 |
|
$this->restorePath(); |
260
|
1 |
|
} |
261
|
|
|
|
262
|
1 |
|
return $pathValue; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* @inheritdoc |
267
|
|
|
*/ |
268
|
2 |
|
public function parseDocument($data, $docType) |
269
|
|
|
{ |
270
|
|
|
try { |
271
|
2 |
|
$this->initPathStack(); |
272
|
|
|
|
273
|
2 |
|
$decoder = $this->factory->getDocumentDecoder($docType); |
274
|
|
|
|
275
|
2 |
|
return $decoder->decode($data, $this); |
276
|
1 |
|
} catch (JsonApiException $e) { |
277
|
|
|
throw $e; |
278
|
1 |
|
} catch (\Exception $e) { |
279
|
1 |
|
$status = $e->getCode(); |
280
|
1 |
|
$message = 'Failed to parse document'; |
281
|
1 |
|
if (empty($status)) { |
282
|
1 |
|
$message = 'Internal server error'; |
283
|
1 |
|
$status = 500; |
284
|
1 |
|
} |
285
|
|
|
|
286
|
1 |
|
$error = new Error( |
287
|
1 |
|
rand(), |
288
|
1 |
|
null, |
289
|
1 |
|
$status, |
290
|
1 |
|
self::ERROR_CODE, |
291
|
1 |
|
$message, |
292
|
1 |
|
$e->getMessage(), |
293
|
1 |
|
['pointer' => $this->getPath()] |
294
|
1 |
|
); |
295
|
|
|
|
296
|
1 |
|
throw new JsonApiException($error, $status, $e); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* @inheritdoc |
302
|
|
|
*/ |
303
|
|
|
public function parseQueryParams($data, $paramsType) |
304
|
|
|
{ |
305
|
|
|
throw new \RuntimeException('Not implemented'); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Prepare path segment |
310
|
|
|
* |
311
|
|
|
* @param string $path |
312
|
|
|
* @return string |
313
|
|
|
*/ |
314
|
12 |
|
protected function preparePathSegment($path) |
315
|
|
|
{ |
316
|
12 |
|
return trim(preg_replace('~[\/]+~si', '/', str_replace(['.', '[', ']'], '/', (string) $path)), '/'); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Initialize stack that store current path |
321
|
|
|
*/ |
322
|
14 |
|
protected function initPathStack() |
323
|
|
|
{ |
324
|
14 |
|
$this->path = new \SplStack(); |
325
|
14 |
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Parse single array item |
329
|
|
|
* |
330
|
|
|
* @param object|array $value |
331
|
|
|
* @param string $key |
332
|
|
|
* @param string|\Closure $itemsParser |
333
|
|
|
* @return mixed |
334
|
|
|
*/ |
335
|
1 |
|
protected function parseArrayItem($value, $key, $itemsParser) |
336
|
|
|
{ |
337
|
1 |
|
$arrayPath = sprintf('[%s]', $key); |
338
|
|
|
|
339
|
1 |
|
if (is_string($itemsParser)) { |
340
|
|
|
switch ($itemsParser) { |
341
|
1 |
|
case 'string': |
342
|
1 |
|
return $this->parseString($value, $arrayPath); |
343
|
|
|
|
344
|
1 |
|
case 'int': |
345
|
1 |
|
case 'integer': |
346
|
1 |
|
return $this->parseInt($value, $arrayPath); |
347
|
|
|
|
348
|
1 |
|
case 'float': |
349
|
1 |
|
case 'double': |
350
|
1 |
|
return $this->parseFloat($value, $arrayPath); |
351
|
|
|
|
352
|
1 |
|
case 'bool': |
353
|
1 |
|
case 'boolean': |
354
|
1 |
|
return $this->parseBool($value, $arrayPath); |
355
|
|
|
|
356
|
1 |
|
case 'date': |
357
|
1 |
|
return $this->parseDateTime($value, $arrayPath); |
358
|
|
|
|
359
|
1 |
|
case 'datetime': |
360
|
1 |
|
return $this->parseDateTime($value, $arrayPath, 'Y-m-d H:i:s'); |
361
|
|
|
|
362
|
1 |
|
case 'time': |
363
|
1 |
|
return $this->parseDateTime($value, $arrayPath, 'H:i:s'); |
364
|
|
|
|
365
|
|
|
default: |
366
|
|
|
throw new \InvalidArgumentException( |
367
|
|
|
sprintf("Unknown array items parser '%s' specified", $itemsParser), |
368
|
|
|
500 |
369
|
|
|
); |
370
|
|
|
} |
371
|
1 |
|
} elseif ($itemsParser instanceof \Closure) { |
372
|
1 |
|
return $itemsParser($value, $arrayPath, $this); |
373
|
|
|
} else { |
374
|
|
|
throw new \InvalidArgumentException( |
375
|
|
|
sprintf( |
376
|
|
|
"Array items parser must be a string or \\Closure, but %s given", |
377
|
|
|
gettype($itemsParser) |
378
|
|
|
), |
379
|
|
|
500 |
380
|
|
|
); |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Parse numeric value |
386
|
|
|
* |
387
|
|
|
* @param mixed $data |
388
|
|
|
* @param string $path |
389
|
|
|
* @param string $type |
390
|
|
|
* @return float|int|null |
391
|
|
|
*/ |
392
|
3 |
|
protected function parseNumeric($data, $path, $type) |
393
|
|
|
{ |
394
|
3 |
|
$this->setPath($path); |
395
|
|
|
|
396
|
3 |
|
$pathValue = null; |
397
|
3 |
|
if ($this->hasValue($data, $path)) { |
398
|
3 |
|
$value = $this->getValue($data, $path); |
399
|
3 |
|
$rightType = ('int' === $type) ? is_int($value) : is_float($value); |
400
|
3 |
|
if ($rightType) { |
401
|
3 |
|
$pathValue = $value; |
402
|
3 |
|
} elseif (is_numeric($pathValue)) { |
403
|
|
|
$pathValue = ('int' === $type) ? (int) $value : (float) $value; |
404
|
2 |
|
} elseif (null !== $value) { |
405
|
2 |
|
throw new \InvalidArgumentException( |
406
|
2 |
|
sprintf("Value expected to be %s, but %s given", $type, gettype($value)), |
407
|
|
|
400 |
408
|
2 |
|
); |
409
|
|
|
} |
410
|
3 |
|
} |
411
|
|
|
|
412
|
3 |
|
$this->restorePath(); |
413
|
|
|
|
414
|
3 |
|
return $pathValue; |
415
|
|
|
} |
416
|
|
|
} |
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.