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
|
|
|
namespace Reva2\JsonApi\Decoders; |
12
|
|
|
|
13
|
|
|
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface; |
14
|
|
|
use Neomerx\JsonApi\Document\Error; |
15
|
|
|
use Neomerx\JsonApi\Exceptions\JsonApiException; |
16
|
|
|
use Reva2\JsonApi\Contracts\Decoders\CallbackResolverInterface; |
17
|
|
|
use Reva2\JsonApi\Contracts\Decoders\DataParserInterface; |
18
|
|
|
use Reva2\JsonApi\Contracts\Decoders\Mapping\ClassMetadataInterface; |
19
|
|
|
use Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface; |
20
|
|
|
use Reva2\JsonApi\Contracts\Decoders\Mapping\Factory\MetadataFactoryInterface; |
21
|
|
|
use Reva2\JsonApi\Contracts\Decoders\Mapping\ObjectMetadataInterface; |
22
|
|
|
use Reva2\JsonApi\Contracts\Decoders\Mapping\PropertyMetadataInterface; |
23
|
|
|
use Reva2\JsonApi\Contracts\Decoders\Mapping\ResourceMetadataInterface; |
24
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccess; |
25
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessor; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Data parser |
29
|
|
|
* |
30
|
|
|
* @package Reva2\JsonApi\Decoders |
31
|
|
|
* @author Sergey Revenko <[email protected]> |
32
|
|
|
*/ |
33
|
|
|
class DataParser implements DataParserInterface |
34
|
|
|
{ |
35
|
|
|
const ERROR_CODE = 'ee2c1d49-ba40-4077-a6bb-b06baceb3e97'; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Current path |
39
|
|
|
* |
40
|
|
|
* @var \SplStack |
41
|
|
|
*/ |
42
|
|
|
protected $path; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Resource decoders factory |
46
|
|
|
* |
47
|
|
|
* @var MetadataFactoryInterface |
48
|
|
|
*/ |
49
|
|
|
protected $factory; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var PropertyAccessor |
53
|
|
|
*/ |
54
|
|
|
protected $accessor; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var CallbackResolverInterface |
58
|
|
|
*/ |
59
|
|
|
protected $callbackResolver; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Serialization groups |
63
|
|
|
* |
64
|
|
|
* @var string[] |
65
|
|
|
*/ |
66
|
|
|
protected $serializationGroups = ['Default']; |
67
|
19 |
|
|
68
|
1 |
|
/** |
69
|
19 |
|
* Constructor |
70
|
19 |
|
* |
71
|
19 |
|
* @param MetadataFactoryInterface $factory |
72
|
|
|
* @param CallbackResolverInterface $callbackResolver |
73
|
19 |
|
*/ |
74
|
19 |
|
public function __construct(MetadataFactoryInterface $factory, CallbackResolverInterface $callbackResolver) |
75
|
|
|
{ |
76
|
|
|
$this->factory = $factory; |
77
|
|
|
$this->callbackResolver = $callbackResolver; |
78
|
|
|
$this->accessor = PropertyAccess::createPropertyAccessorBuilder() |
79
|
14 |
|
->enableExceptionOnInvalidIndex() |
80
|
|
|
->getPropertyAccessor(); |
81
|
14 |
|
|
82
|
|
|
$this->initPathStack(); |
83
|
14 |
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @inheritdoc |
87
|
|
|
*/ |
88
|
|
|
public function setPath($path) |
89
|
14 |
|
{ |
90
|
|
|
$this->path->push($this->preparePathSegment($path)); |
91
|
14 |
|
|
92
|
|
|
return $this; |
93
|
14 |
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @inheritdoc |
97
|
|
|
*/ |
98
|
|
|
public function restorePath() |
99
|
2 |
|
{ |
100
|
|
|
$this->path->pop(); |
101
|
2 |
|
|
102
|
2 |
|
return $this; |
103
|
1 |
|
} |
104
|
2 |
|
|
105
|
|
|
/** |
106
|
2 |
|
* @return string[] |
107
|
|
|
*/ |
108
|
|
|
public function getSerializationGroups() |
109
|
|
|
{ |
110
|
|
|
return $this->serializationGroups; |
111
|
|
|
} |
112
|
14 |
|
|
113
|
|
|
/** |
114
|
14 |
|
* @param string[] $serializationGroups |
115
|
|
|
* @return $this |
116
|
|
|
*/ |
117
|
|
|
public function setSerializationGroups($serializationGroups) |
118
|
|
|
{ |
119
|
|
|
$this->serializationGroups = $serializationGroups; |
120
|
14 |
|
|
121
|
|
|
return $this; |
122
|
14 |
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @inheritdoc |
126
|
|
|
*/ |
127
|
|
|
public function getPath() |
128
|
8 |
|
{ |
129
|
|
|
$segments = []; |
130
|
8 |
|
foreach ($this->path as $segment) { |
131
|
|
|
$segments[] = $segment; |
132
|
8 |
|
} |
133
|
8 |
|
|
134
|
8 |
|
return '/' . implode('/', array_reverse($segments)); |
135
|
7 |
|
} |
136
|
7 |
|
|
137
|
7 |
|
/** |
138
|
1 |
|
* @inheritdoc |
139
|
1 |
|
*/ |
140
|
|
|
public function hasValue($data, $path) |
141
|
1 |
|
{ |
142
|
|
|
return $this->accessor->isReadable($data, $path); |
143
|
7 |
|
} |
144
|
8 |
|
|
145
|
|
|
/** |
146
|
8 |
|
* @inheritdoc |
147
|
|
|
*/ |
148
|
|
|
public function getValue($data, $path) |
149
|
|
|
{ |
150
|
|
|
return $this->accessor->getValue($data, $path); |
151
|
|
|
} |
152
|
4 |
|
|
153
|
|
|
/** |
154
|
4 |
|
* @inheritdoc |
155
|
|
|
*/ |
156
|
|
|
public function parseString($data, $path) |
157
|
|
|
{ |
158
|
|
|
$this->setPath($path); |
159
|
|
|
|
160
|
2 |
|
$pathValue = null; |
161
|
|
|
if ($this->hasValue($data, $path)) { |
162
|
2 |
|
$value = $this->getValue($data, $path); |
163
|
|
|
if ((null === $value) || (is_string($value))) { |
164
|
|
|
$pathValue = $value; |
165
|
|
|
} else { |
166
|
|
|
throw new \InvalidArgumentException( |
167
|
|
|
sprintf("Value expected to be a string, but %s given", gettype($value)), |
168
|
1 |
|
400 |
169
|
|
|
); |
170
|
1 |
|
} |
171
|
|
|
} |
172
|
1 |
|
$this->restorePath(); |
173
|
1 |
|
|
174
|
1 |
|
return $pathValue; |
175
|
1 |
|
} |
176
|
|
|
|
177
|
1 |
|
/** |
178
|
|
|
* @inheritdoc |
179
|
1 |
|
*/ |
180
|
|
|
public function parseInt($data, $path) |
181
|
|
|
{ |
182
|
|
|
return $this->parseNumeric($data, $path, 'int'); |
183
|
|
|
} |
184
|
|
|
|
185
|
4 |
|
/** |
186
|
|
|
* @inheritdoc |
187
|
4 |
|
*/ |
188
|
|
|
public function parseFloat($data, $path) |
189
|
4 |
|
{ |
190
|
4 |
|
return $this->parseNumeric($data, $path, 'float'); |
191
|
4 |
|
} |
192
|
4 |
|
|
193
|
|
|
/** |
194
|
4 |
|
* @inheritdoc |
195
|
|
|
*/ |
196
|
4 |
|
public function parseRaw($data, $path) |
|
|
|
|
197
|
|
|
{ |
198
|
|
|
$this->setPath($path); |
199
|
|
|
|
200
|
|
|
$pathValue = null; |
201
|
|
|
if ($this->hasValue($data, $path)) { |
202
|
2 |
|
$pathValue = $this->getValue($data, $path); |
203
|
|
|
} |
204
|
2 |
|
|
205
|
|
|
$this->restorePath(); |
206
|
2 |
|
|
207
|
2 |
|
return $pathValue; |
208
|
2 |
|
} |
209
|
2 |
|
|
210
|
1 |
|
/** |
211
|
2 |
|
* @inheritdoc |
212
|
2 |
|
*/ |
213
|
2 |
|
public function parseCallback($data, $path, $callback) |
|
|
|
|
214
|
1 |
|
{ |
215
|
1 |
|
$this->setPath($path); |
216
|
1 |
|
|
217
|
1 |
|
$pathValue = null; |
218
|
|
|
if ($this->hasValue($data, $path)) { |
219
|
1 |
|
$pathValue = call_user_func($callback, $this->getValue($data, $path)); |
220
|
|
|
} |
221
|
2 |
|
|
222
|
|
|
$this->restorePath(); |
223
|
2 |
|
|
224
|
|
|
return $pathValue; |
225
|
2 |
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* @inheritdoc |
229
|
|
|
*/ |
230
|
|
|
public function parseBool($data, $path) |
231
|
2 |
|
{ |
232
|
|
|
$this->setPath($path); |
233
|
2 |
|
|
234
|
|
|
$pathValue = null; |
235
|
2 |
|
if ($this->hasValue($data, $path)) { |
236
|
2 |
|
$value = $this->getValue($data, $path); |
237
|
2 |
|
if ((null === $value) || (is_bool($value))) { |
238
|
2 |
|
$pathValue = $value; |
239
|
2 |
|
} elseif (is_string($value)) { |
240
|
2 |
|
$pathValue = (in_array($value, ['true', 'yes', 'y', 'on', 'enabled'])) ? true : false; |
241
|
2 |
|
} elseif (is_numeric($value)) { |
242
|
|
|
$pathValue = (bool) $value; |
243
|
2 |
|
} else { |
244
|
1 |
|
throw new \InvalidArgumentException( |
245
|
1 |
|
sprintf("Value expected to be a boolean, but %s given", gettype($value)), |
246
|
|
|
400 |
247
|
1 |
|
); |
248
|
|
|
} |
249
|
2 |
|
} |
250
|
2 |
|
|
251
|
|
|
$this->restorePath(); |
252
|
2 |
|
|
253
|
|
|
return $pathValue; |
254
|
2 |
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* @inheritdoc |
258
|
|
|
*/ |
259
|
|
|
public function parseDateTime($data, $path, $format = 'Y-m-d') |
260
|
5 |
|
{ |
261
|
|
|
$this->setPath($path); |
262
|
4 |
|
|
263
|
|
|
$pathValue = null; |
264
|
4 |
|
if ($this->hasValue($data, $path)) { |
265
|
5 |
|
$value = $this->getValue($data, $path); |
266
|
4 |
|
if (null !== $value) { |
267
|
4 |
|
if (is_string($value)) { |
268
|
1 |
|
$pathValue = \DateTimeImmutable::createFromFormat($format, $value); |
269
|
1 |
|
} |
270
|
|
|
|
271
|
1 |
|
if (!$pathValue instanceof \DateTimeImmutable) { |
272
|
4 |
|
throw new \InvalidArgumentException( |
273
|
4 |
|
sprintf("Value expected to be a date/time string in '%s' format", $format), |
274
|
4 |
|
400 |
275
|
4 |
|
); |
276
|
4 |
|
} |
277
|
|
|
} |
278
|
4 |
|
} |
279
|
3 |
|
|
280
|
3 |
|
$this->restorePath(); |
281
|
3 |
|
|
282
|
|
|
return $pathValue; |
283
|
3 |
|
} |
284
|
|
|
|
285
|
3 |
|
/** |
286
|
|
|
* @inheritdoc |
287
|
|
|
*/ |
288
|
|
|
public function parseArray($data, $path, \Closure $itemsParser) |
289
|
|
|
{ |
290
|
|
|
$this->setPath($path); |
291
|
|
|
|
292
|
|
|
$pathValue = null; |
293
|
|
|
if ($this->hasValue($data, $path)) { |
294
|
|
|
$value = $this->getValue($data, $path); |
295
|
|
|
if ((null !== $value) && (false === is_array($value))) { |
296
|
1 |
|
throw new \InvalidArgumentException( |
297
|
|
|
sprintf("Value expected to be an array, but %s given", gettype($value)), |
298
|
1 |
|
400 |
299
|
|
|
); |
300
|
1 |
|
} elseif (is_array($value)) { |
301
|
1 |
|
$pathValue = []; |
302
|
1 |
|
$keys = array_keys($value); |
303
|
1 |
|
foreach ($keys as $key) { |
304
|
1 |
|
$arrayPath = sprintf("[%s]", $key); |
305
|
|
|
|
306
|
1 |
|
$pathValue[$key] = $itemsParser($value, $arrayPath, $this); |
307
|
1 |
|
} |
308
|
|
|
} |
309
|
1 |
|
} |
310
|
|
|
|
311
|
|
|
$this->restorePath(); |
312
|
|
|
|
313
|
|
|
return $pathValue; |
314
|
|
|
} |
315
|
5 |
|
|
316
|
|
|
/** |
317
|
5 |
|
* Parse data object value at specified path as object of specified class |
318
|
|
|
* |
319
|
5 |
|
* @param array|object $data |
320
|
5 |
|
* @param string $path |
321
|
5 |
|
* @param string $objType |
322
|
5 |
|
* @return null |
323
|
5 |
|
*/ |
324
|
|
|
public function parseObject($data, $path, $objType) |
325
|
|
|
{ |
326
|
5 |
|
$this->setPath($path); |
327
|
4 |
|
|
328
|
4 |
|
$pathValue = null; |
329
|
|
|
if ((true === $this->hasValue($data, $path)) && |
330
|
4 |
|
(null !== ($value = $this->getValue($data, $path))) |
331
|
|
|
) { |
332
|
|
|
$this->restorePath(); |
333
|
4 |
|
|
334
|
4 |
|
$pathValue = $this->parseObjectValue($value, $objType); |
335
|
1 |
|
} |
336
|
1 |
|
|
337
|
|
|
return $pathValue; |
338
|
1 |
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
4 |
|
* @inheritdoc |
342
|
4 |
|
*/ |
343
|
|
|
public function parseResource($data, $path, $resType) |
344
|
4 |
|
{ |
345
|
4 |
|
$this->setPath($path); |
346
|
4 |
|
|
347
|
|
|
$pathValue = null; |
348
|
4 |
|
if ((true === $this->hasValue($data, $path)) && |
349
|
4 |
|
(null !== ($value = $this->getValue($data, $path))) |
350
|
4 |
|
) { |
351
|
|
|
$metadata = $this->factory->getMetadataFor($resType); |
352
|
4 |
|
/* @var $metadata ResourceMetadataInterface */ |
353
|
4 |
|
|
354
|
3 |
|
$discClass = $this->getClassByDiscriminator($metadata, $value); |
355
|
3 |
|
if ((null !== $discClass) && ($discClass !== $resType)) { |
356
|
|
|
$this->restorePath(); |
357
|
3 |
|
|
358
|
|
|
return $this->parseResource($data, $path, $discClass); |
359
|
3 |
|
} |
360
|
|
|
|
361
|
|
|
$name = $this->parseString($value, 'type'); |
362
|
|
|
if ($name !== $metadata->getName()) { |
363
|
|
|
throw new \InvalidArgumentException( |
364
|
|
|
sprintf("Value must contain resource of type '%s'", $metadata->getName()), |
365
|
4 |
|
409 |
366
|
|
|
); |
367
|
|
|
} |
368
|
4 |
|
|
369
|
|
|
$pathValue = null; |
370
|
4 |
|
if ((null !== $metadata->getLoader()) && (true === $this->hasValue($value, 'id'))) { |
371
|
4 |
|
$callback = $this->callbackResolver->resolveCallback($metadata->getLoader()); |
372
|
1 |
|
$pathValue = call_user_func($callback, $this->getValue($value, 'id'), $metadata); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
if (null === $pathValue) { |
376
|
|
|
$objClass = $metadata->getClassName(); |
377
|
3 |
|
$pathValue = new $objClass(); |
378
|
3 |
|
|
379
|
|
|
if (null !== ($idMetadata = $metadata->getIdMetadata())) { |
380
|
3 |
|
$this->parseProperty($value, $pathValue, $idMetadata); |
381
|
|
|
} |
382
|
2 |
|
} |
383
|
2 |
|
|
384
|
|
|
foreach ($metadata->getAttributes() as $attribute) { |
385
|
2 |
|
$this->parseProperty($value, $pathValue, $attribute); |
386
|
2 |
|
} |
387
|
|
|
|
388
|
|
|
foreach ($metadata->getRelationships() as $relationship) { |
389
|
|
|
$this->parseProperty($value, $pathValue, $relationship); |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
393
|
3 |
|
$this->restorePath(); |
394
|
|
|
|
395
|
|
|
return $pathValue; |
396
|
3 |
|
} |
397
|
|
|
|
398
|
3 |
|
/** |
399
|
3 |
|
* @inheritdoc |
400
|
|
|
*/ |
401
|
|
|
public function parseDocument($data, $docType) |
402
|
|
|
{ |
403
|
|
|
try { |
404
|
|
|
$this->initPathStack(); |
405
|
|
|
|
406
|
3 |
|
$metadata = $this->factory->getMetadataFor($docType); |
407
|
|
|
if (!$metadata instanceof DocumentMetadataInterface) { |
408
|
|
|
throw new \InvalidArgumentException(sprintf("Failed to parse %s as JSON API document", $docType)); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/* @var $metadata \Reva2\JsonApi\Contracts\Decoders\Mapping\DocumentMetadataInterface */ |
412
|
|
|
|
413
|
|
|
$docClass = $metadata->getClassName(); |
414
|
|
|
$doc = new $docClass(); |
415
|
|
|
|
416
|
|
|
$this->parseProperty($data, $doc, $metadata->getContentMetadata()); |
417
|
|
|
|
418
|
|
|
return $doc; |
419
|
|
|
} catch (JsonApiException $e) { |
420
|
14 |
|
throw $e; |
421
|
|
|
} catch (\Exception $e) { |
422
|
14 |
|
throw $this->convertToApiException($e, 'document'); |
423
|
|
|
} |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* @inheritdoc |
428
|
19 |
|
*/ |
429
|
|
|
public function parseQueryParams($data, $paramsType) |
430
|
19 |
|
{ |
431
|
19 |
|
try { |
432
|
|
|
$this->initPathStack(); |
433
|
|
|
|
434
|
|
|
$query = $this->parseObjectValue($data, $paramsType); |
435
|
|
|
if (!$query instanceof EncodingParametersInterface) { |
436
|
|
|
throw new \InvalidArgumentException(sprintf( |
437
|
|
|
"Query parameters object must implement interface %s", |
438
|
|
|
EncodingParametersInterface::class |
439
|
|
|
)); |
440
|
|
|
} |
441
|
5 |
|
|
442
|
|
|
return $query; |
443
|
5 |
|
} catch (JsonApiException $e) { |
444
|
|
|
throw $e; |
445
|
5 |
|
} catch (\Exception $e) { |
446
|
5 |
|
throw $this->convertToApiException($e, 'query'); |
447
|
5 |
|
} |
448
|
5 |
|
} |
449
|
5 |
|
|
450
|
3 |
|
/** |
451
|
5 |
|
* Prepare path segment |
452
|
4 |
|
* |
453
|
5 |
|
* @param string $path |
454
|
2 |
|
* @return string |
455
|
2 |
|
*/ |
456
|
|
|
protected function preparePathSegment($path) |
457
|
2 |
|
{ |
458
|
|
|
return trim(preg_replace('~[\/]+~si', '/', str_replace(['.', '[', ']'], '/', (string) $path)), '/'); |
459
|
5 |
|
} |
460
|
|
|
|
461
|
5 |
|
/** |
462
|
|
|
* Initialize stack that store current path |
463
|
5 |
|
*/ |
464
|
|
|
protected function initPathStack() |
465
|
|
|
{ |
466
|
|
|
$this->path = new \SplStack(); |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* Parse numeric value |
471
|
|
|
* |
472
|
|
|
* @param mixed $data |
473
|
7 |
|
* @param string $path |
474
|
|
|
* @param string $type |
475
|
7 |
|
* @return float|int|null |
476
|
|
|
*/ |
477
|
7 |
|
protected function parseNumeric($data, $path, $type) |
478
|
4 |
|
{ |
479
|
|
|
$this->setPath($path); |
480
|
|
|
|
481
|
7 |
|
$pathValue = null; |
482
|
4 |
|
if ($this->hasValue($data, $path)) { |
483
|
4 |
|
$value = $this->getValue($data, $path); |
484
|
7 |
|
$rightType = ('int' === $type) ? is_int($value) : is_float($value); |
485
|
|
|
if ($rightType) { |
486
|
|
|
$pathValue = $value; |
487
|
7 |
|
} elseif (is_numeric($value)) { |
488
|
3 |
|
$pathValue = ('int' === $type) ? (int) $value : (float) $value; |
489
|
|
|
} elseif (null !== $value) { |
490
|
3 |
|
throw new \InvalidArgumentException( |
491
|
3 |
|
sprintf("Value expected to be %s, but %s given", $type, gettype($value)), |
492
|
|
|
400 |
493
|
7 |
|
); |
494
|
7 |
|
} |
495
|
6 |
|
} |
496
|
6 |
|
|
497
|
5 |
|
$this->restorePath(); |
498
|
5 |
|
|
499
|
|
|
return $pathValue; |
500
|
7 |
|
} |
501
|
|
|
|
502
|
|
|
/** |
503
|
|
|
* Parse property of specified object |
504
|
|
|
* |
505
|
|
|
* @param object|array $data |
506
|
|
|
* @param object $obj |
507
|
|
|
* @param PropertyMetadataInterface $metadata |
508
|
|
|
*/ |
509
|
|
|
private function parseProperty($data, $obj, PropertyMetadataInterface $metadata) |
510
|
7 |
|
{ |
511
|
|
|
$path = $metadata->getDataPath(); |
512
|
7 |
|
|
513
|
7 |
|
if ((false === $this->hasValue($data, $path)) || |
514
|
7 |
|
(true === $this->isExcludedProperty($metadata)) |
515
|
|
|
) { |
516
|
5 |
|
return; |
517
|
1 |
|
} |
518
|
1 |
|
|
519
|
1 |
|
if ('custom' === $metadata->getDataType()) { |
520
|
1 |
|
$value = $this->parseCallback($data, $path, [$obj, $metadata->getDataTypeParams()]); |
521
|
|
|
} else { |
522
|
1 |
|
$value = $this->parsePropertyValue($data, $path, $metadata); |
523
|
|
|
} |
524
|
5 |
|
|
525
|
3 |
|
if (null !== ($converter = $metadata->getConverter())) { |
526
|
|
|
$callback = $this->callbackResolver->resolveCallback($converter); |
527
|
5 |
|
|
528
|
5 |
|
$value = call_user_func($callback, $value); |
529
|
|
|
} |
530
|
1 |
|
|
531
|
1 |
|
$setter = $metadata->getSetter(); |
532
|
|
|
if (null !== $setter) { |
533
|
|
|
$obj->{$setter}($value); |
534
|
|
|
} else { |
535
|
|
|
$setter = $metadata->getPropertyName(); |
536
|
|
|
$obj->{$setter} = $value; |
537
|
|
|
} |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
/** |
541
|
|
|
* Parse value of specified property |
542
|
|
|
* |
543
|
|
|
* @param object|array $data |
544
|
|
|
* @param string $path |
545
|
|
|
* @param PropertyMetadataInterface $metadata |
546
|
|
|
* @return mixed|null |
547
|
|
|
*/ |
548
|
|
|
private function parsePropertyValue($data, $path, PropertyMetadataInterface $metadata) |
549
|
5 |
|
{ |
550
|
|
|
switch ($metadata->getDataType()) { |
551
|
5 |
|
case 'scalar': |
552
|
|
|
return $this->parseScalarValue($data, $path, $metadata->getDataTypeParams()); |
553
|
5 |
|
|
554
|
4 |
|
case 'datetime': |
555
|
|
|
$format = $metadata->getDataTypeParams(); |
556
|
1 |
|
if (empty($format)) { |
557
|
|
|
$format = 'Y-m-d'; |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
return $this->parseDateTime($data, $path, $format); |
561
|
|
|
|
562
|
|
|
case 'array': |
563
|
|
|
return $this->parseArrayValue($data, $path, $metadata->getDataTypeParams()); |
564
|
|
|
|
565
|
|
|
case 'object': |
566
|
|
|
return $this->parseResourceOrObject($data, $path, $metadata->getDataTypeParams()); |
567
|
4 |
|
|
568
|
|
|
case 'raw': |
569
|
4 |
|
return $this->parseRaw($data, $path); |
570
|
4 |
|
|
571
|
|
|
default: |
572
|
|
|
throw new \InvalidArgumentException(sprintf( |
573
|
|
|
"Unsupported property data type '%s'", |
574
|
4 |
|
$metadata->getDataType() |
575
|
4 |
|
)); |
576
|
1 |
|
} |
577
|
|
|
} |
578
|
|
|
|
579
|
4 |
|
/** |
580
|
4 |
|
* Parse value as JSON API resource or object |
581
|
|
|
* |
582
|
4 |
|
* @param object|array $data |
583
|
4 |
|
* @param string $path |
584
|
4 |
|
* @param string $objClass |
585
|
4 |
|
* @return mixed|null |
586
|
|
|
*/ |
587
|
4 |
|
public function parseResourceOrObject($data, $path, $objClass) |
588
|
|
|
{ |
589
|
|
|
$metadata = $this->factory->getMetadataFor($objClass); |
590
|
|
|
|
591
|
|
|
if ($metadata instanceof ResourceMetadataInterface) { |
592
|
|
|
return $this->parseResource($data, $path, $objClass); |
593
|
|
|
} else { |
594
|
|
|
return $this->parseObject($data, $path, $objClass); |
595
|
|
|
} |
596
|
|
|
} |
597
|
|
|
|
598
|
3 |
|
/** |
599
|
|
|
* Parse value that contains JSON API object |
600
|
3 |
|
* |
601
|
3 |
|
* @param object|array $data |
602
|
|
|
* @param string $objType |
603
|
|
|
* @return mixed |
604
|
3 |
|
*/ |
605
|
1 |
|
public function parseObjectValue($data, $objType) |
606
|
1 |
|
{ |
607
|
1 |
|
$metadata = $this->factory->getMetadataFor($objType); |
608
|
|
|
if (!$metadata instanceof ObjectMetadataInterface) { |
609
|
1 |
|
throw new \InvalidArgumentException('Invalid object metadata'); |
610
|
|
|
} |
611
|
1 |
|
|
612
|
|
|
$discClass = $this->getClassByDiscriminator($metadata, $data); |
613
|
3 |
|
if ((null !== $discClass) && ($discClass !== $objType)) { |
614
|
1 |
|
return $this->parseObjectValue($data, $discClass); |
615
|
1 |
|
} |
616
|
1 |
|
|
617
|
1 |
|
$objClass = $metadata->getClassName(); |
618
|
|
|
$obj = new $objClass(); |
619
|
1 |
|
|
620
|
|
|
$properties = $metadata->getProperties(); |
621
|
1 |
|
foreach ($properties as $property) { |
622
|
|
|
$this->parseProperty($data, $obj, $property); |
623
|
3 |
|
} |
624
|
3 |
|
|
625
|
3 |
|
return $obj; |
626
|
3 |
|
} |
627
|
|
|
|
628
|
3 |
|
/** |
629
|
|
|
* Parse value that contains array |
630
|
3 |
|
* |
631
|
|
|
* @param object|array $data |
632
|
1 |
|
* @param string $path |
633
|
1 |
|
* @param array $params |
634
|
1 |
|
* @return array|null |
635
|
1 |
|
*/ |
636
|
|
|
public function parseArrayValue($data, $path, array $params) |
637
|
1 |
|
{ |
638
|
|
|
$type = $params[0]; |
639
|
1 |
|
$typeParams = $params[1]; |
640
|
|
|
|
641
|
1 |
|
switch ($type) { |
642
|
1 |
|
case 'scalar': |
643
|
1 |
|
return $this->parseArray( |
644
|
1 |
|
$data, |
645
|
1 |
|
$path, |
646
|
1 |
|
function ($data, $path, DataParser $parser) use ($typeParams) { |
647
|
|
|
return $parser->parseScalarValue($data, $path, $typeParams); |
648
|
1 |
|
} |
649
|
|
|
); |
650
|
|
|
|
651
|
|
|
case 'datetime': |
652
|
|
|
$format = (!empty($typeParams)) ? $typeParams : 'Y-m-d'; |
653
|
|
|
return $this->parseArray( |
654
|
|
|
$data, |
655
|
|
|
$path, |
656
|
|
|
function ($data, $path, DataParser $parser) use ($format) { |
657
|
|
|
return $parser->parseDateTime($data, $path, $format); |
658
|
|
|
} |
659
|
|
|
); |
660
|
|
|
|
661
|
|
|
case 'object': |
662
|
|
|
return $this->parseArray( |
663
|
|
|
$data, |
664
|
|
|
$path, |
665
|
|
|
function ($data, $path, DataParser $parser) use ($typeParams) { |
666
|
7 |
|
return $parser->parseResourceOrObject($data, $path, $typeParams); |
667
|
|
|
} |
668
|
|
|
); |
669
|
7 |
|
|
670
|
5 |
|
case 'array': |
671
|
|
|
return $this->parseArray( |
672
|
3 |
|
$data, |
673
|
3 |
|
$path, |
674
|
1 |
|
function ($data, $path, DataParser $parser) use ($typeParams) { |
675
|
|
|
return $parser->parseArrayValue($data, $path, $typeParams); |
676
|
3 |
|
} |
677
|
3 |
|
); |
678
|
3 |
|
|
679
|
|
|
case 'raw': |
680
|
1 |
|
return $this->parseArray( |
681
|
1 |
|
$data, |
682
|
1 |
|
$path, |
683
|
|
|
function ($data, $path, DataParser $parser) { |
684
|
|
|
return $parser->parseRaw($data, $path); |
685
|
|
|
} |
686
|
|
|
); |
687
|
|
|
|
688
|
|
|
default: |
689
|
|
|
throw new \InvalidArgumentException(sprintf( |
690
|
|
|
"Unsupported array item type '%s' specified", |
691
|
|
|
$type |
692
|
|
|
)); |
693
|
|
|
} |
694
|
|
|
} |
695
|
|
|
|
696
|
2 |
|
/** |
697
|
|
|
* Parse scalar value |
698
|
2 |
|
* |
699
|
2 |
|
* @param object|array $data |
700
|
2 |
|
* @param string $path |
701
|
1 |
|
* @param string $type |
702
|
1 |
|
* @return bool|float|int|null|string |
703
|
1 |
|
*/ |
704
|
|
|
public function parseScalarValue($data, $path, $type) |
705
|
2 |
|
{ |
706
|
|
|
switch ($type) { |
707
|
2 |
|
case 'string': |
708
|
2 |
|
return $this->parseString($data, $path); |
709
|
2 |
|
|
710
|
|
|
case 'bool': |
711
|
|
|
case 'boolean': |
712
|
|
|
return $this->parseBool($data, $path); |
713
|
|
|
|
714
|
|
|
case 'int': |
715
|
|
|
case 'integer': |
716
|
2 |
|
return $this->parseInt($data, $path); |
717
|
|
|
|
718
|
2 |
|
case 'float': |
719
|
|
|
case 'double': |
720
|
|
|
return $this->parseFloat($data, $path); |
721
|
|
|
|
722
|
|
|
default: |
723
|
|
|
throw new \InvalidArgumentException(sprintf("Unsupported scalar type '%s' specified", $type)); |
724
|
|
|
} |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
/** |
728
|
8 |
|
* Convert any exception to JSON API exception |
729
|
|
|
* |
730
|
8 |
|
* @param \Exception $e |
731
|
7 |
|
* @param string $objType |
732
|
|
|
* @return JsonApiException |
733
|
|
|
*/ |
734
|
6 |
|
private function convertToApiException(\Exception $e, $objType) |
735
|
6 |
|
{ |
736
|
1 |
|
$status = $e->getCode(); |
737
|
|
|
$message = 'Failed to parse request'; |
738
|
1 |
|
if (empty($status)) { |
739
|
|
|
$message = 'Internal server error'; |
740
|
|
|
$status = 500; |
741
|
5 |
|
} |
742
|
|
|
|
743
|
|
|
$source = null; |
744
|
|
|
switch ($objType) { |
745
|
|
|
case 'document': |
746
|
|
|
$source = ['pointer' => $this->getPath()]; |
747
|
|
|
break; |
748
|
|
|
|
749
|
|
|
case 'query': |
750
|
|
|
$source = ['parameter' => $this->getPath()]; |
751
|
|
|
break; |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
$error = new Error(rand(), null, $status, self::ERROR_CODE, $message, $e->getMessage(), $source); |
755
|
|
|
|
756
|
|
|
return new JsonApiException($error, $status, $e); |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
/** |
760
|
|
|
* Returns appropriate discriminator class for specified data |
761
|
|
|
* |
762
|
|
|
* @param ClassMetadataInterface $metadata |
763
|
|
|
* @param array|object $data |
764
|
|
|
* @return string|null |
765
|
|
|
*/ |
766
|
|
|
private function getClassByDiscriminator(ClassMetadataInterface $metadata, $data) |
767
|
|
|
{ |
768
|
|
|
if (null === ($discField = $metadata->getDiscriminatorField())) { |
769
|
|
|
return null; |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
$discValue = $this->parseString($data, $discField->getDataPath()); |
773
|
|
|
if (empty($discValue)) { |
774
|
|
|
$this->setPath($discField->getDataPath()); |
775
|
|
|
|
776
|
|
|
throw new \InvalidArgumentException("Field value required and can not be empty", 422); |
777
|
|
|
} |
778
|
|
|
|
779
|
|
|
return $metadata->getDiscriminatorClass($discValue); |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
/** |
783
|
|
|
* Check if specified property should be excluded |
784
|
|
|
* |
785
|
|
|
* @param PropertyMetadataInterface $metadata |
786
|
|
|
* @return bool |
787
|
|
|
*/ |
788
|
|
|
private function isExcludedProperty(PropertyMetadataInterface $metadata) |
789
|
|
|
{ |
790
|
|
|
$propertyGroups = $metadata->getGroups(); |
791
|
|
|
foreach ($propertyGroups as $group) { |
792
|
|
|
if (in_array($group, $this->serializationGroups)) { |
793
|
|
|
return false; |
794
|
|
|
} |
795
|
|
|
} |
796
|
|
|
|
797
|
|
|
return true; |
798
|
|
|
} |
799
|
|
|
} |
800
|
|
|
|
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.