Completed
Push — master ( 6e9ccf...5245c1 )
by Han Hui
20s queued 12s
created

src/Serializer/AbstractItemNormalizer.php (2 issues)

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Serializer;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
18
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
19
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
20
use ApiPlatform\Core\Exception\InvalidArgumentException;
21
use ApiPlatform\Core\Exception\InvalidValueException;
22
use ApiPlatform\Core\Exception\ItemNotFoundException;
23
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
24
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
25
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
26
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
27
use ApiPlatform\Core\Util\ClassInfoTrait;
28
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
29
use Symfony\Component\PropertyAccess\PropertyAccess;
30
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
31
use Symfony\Component\PropertyInfo\Type;
32
use Symfony\Component\Serializer\Exception\LogicException;
33
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
34
use Symfony\Component\Serializer\Exception\RuntimeException;
35
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
36
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
37
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
38
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
39
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
40
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
41
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
42
43
/**
44
 * Base item normalizer.
45
 *
46
 * @author Kévin Dunglas <[email protected]>
47
 */
48
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
49
{
50
    use ClassInfoTrait;
51
    use ContextTrait;
52
    use InputOutputMetadataTrait;
53
54
    protected $propertyNameCollectionFactory;
55
    protected $propertyMetadataFactory;
56
    protected $iriConverter;
57
    protected $resourceClassResolver;
58
    protected $propertyAccessor;
59
    protected $itemDataProvider;
60
    protected $allowPlainIdentifiers;
61
    protected $dataTransformers = [];
62
    protected $localCache = [];
63
64
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
65
    {
66
        if (!isset($defaultContext['circular_reference_handler'])) {
67
            $defaultContext['circular_reference_handler'] = function ($object) {
68
                return $this->iriConverter->getIriFromItem($object);
69
            };
70
        }
71
        if (!interface_exists(AdvancedNameConverterInterface::class)) {
72
            $this->setCircularReferenceHandler($defaultContext['circular_reference_handler']);
73
        }
74
75
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable([$this, 'getObjectClass']), $defaultContext);
76
77
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
78
        $this->propertyMetadataFactory = $propertyMetadataFactory;
79
        $this->iriConverter = $iriConverter;
80
        $this->resourceClassResolver = $resourceClassResolver;
81
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
82
        $this->itemDataProvider = $itemDataProvider;
83
        $this->allowPlainIdentifiers = $allowPlainIdentifiers;
84
        $this->dataTransformers = $dataTransformers;
85
        $this->resourceMetadataFactory = $resourceMetadataFactory;
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function supportsNormalization($data, $format = null)
92
    {
93
        if (!\is_object($data) || $data instanceof \Traversable) {
94
            return false;
95
        }
96
97
        return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function hasCacheableSupportsMethod(): bool
104
    {
105
        return true;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     *
111
     * @throws LogicException
112
     */
113
    public function normalize($object, $format = null, array $context = [])
114
    {
115
        if ($object !== $transformed = $this->transformOutput($object, $context)) {
116
            if (!$this->serializer instanceof NormalizerInterface) {
117
                throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
118
            }
119
120
            $context['api_normalize'] = true;
121
            $context['api_resource'] = $object;
122
            unset($context['output']);
123
            unset($context['resource_class']);
124
125
            return $this->serializer->normalize($transformed, $format, $context);
126
        }
127
128
        $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null);
129
        $context = $this->initContext($resourceClass, $context);
130
        $iri = $context['iri'] ?? $this->iriConverter->getIriFromItem($object);
131
        $context['iri'] = $iri;
132
        $context['api_normalize'] = true;
133
134
        /*
135
         * When true, converts the normalized data array of a resource into an
136
         * IRI, if the normalized data array is empty.
137
         *
138
         * This is useful when traversing from a non-resource towards an attribute
139
         * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}.
140
         *
141
         * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink}
142
         * should take effect.
143
         */
144
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
145
        unset($context['api_empty_resource_as_iri']);
146
147
        if (isset($context['resources'])) {
148
            $context['resources'][$iri] = $iri;
149
        }
150
151
        $data = parent::normalize($object, $format, $context);
152
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
153
            return $iri;
154
        }
155
156
        return $data;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function supportsDenormalization($data, $type, $format = null)
163
    {
164
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public function denormalize($data, $class, $format = null, array $context = [])
171
    {
172
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $class);
173
        $context['api_denormalize'] = true;
174
        $context['resource_class'] = $resourceClass;
175
176
        if (null !== ($inputClass = $this->getInputClass($resourceClass, $context)) && null !== ($dataTransformer = $this->getDataTransformer($data, $resourceClass, $context))) {
177
            $dataTransformerContext = $context;
178
179
            unset($context['input']);
180
            unset($context['resource_class']);
181
182
            if (!$this->serializer instanceof DenormalizerInterface) {
183
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
184
            }
185
            $denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context);
186
187
            return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
0 ignored issues
show
It seems like $denormalizedInput can also be of type array; however, parameter $object of ApiPlatform\Core\DataTra...rInterface::transform() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
            return $dataTransformer->transform(/** @scrutinizer ignore-type */ $denormalizedInput, $resourceClass, $dataTransformerContext);
Loading history...
188
        }
189
190
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
191
192
        if (\is_string($data)) {
193
            try {
194
                return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]);
195
            } catch (ItemNotFoundException $e) {
196
                if (!$supportsPlainIdentifiers) {
197
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
198
                }
199
            } catch (InvalidArgumentException $e) {
200
                if (!$supportsPlainIdentifiers) {
201
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
202
                }
203
            }
204
        }
205
206
        if (!\is_array($data)) {
207
            if (!$supportsPlainIdentifiers) {
208
                throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
209
            }
210
211
            $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);
212
            if (null === $item) {
213
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data));
214
            }
215
216
            return $item;
217
        }
218
219
        return parent::denormalize($data, $resourceClass, $format, $context);
220
    }
221
222
    /**
223
     * Method copy-pasted from symfony/serializer.
224
     * Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263.
225
     *
226
     * {@inheritdoc}
227
     *
228
     * @internal
229
     */
230
    protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
231
    {
232
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
233
            unset($context[static::OBJECT_TO_POPULATE]);
234
235
            return $object;
236
        }
237
238
        if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
239
            if (!isset($data[$mapping->getTypeProperty()])) {
240
                throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
241
            }
242
243
            $type = $data[$mapping->getTypeProperty()];
244
            if (null === ($mappedClass = $mapping->getClassForType($type))) {
245
                throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
246
            }
247
248
            $class = $mappedClass;
249
            $reflectionClass = new \ReflectionClass($class);
250
        }
251
252
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
253
        if ($constructor) {
254
            $constructorParameters = $constructor->getParameters();
255
256
            $params = [];
257
            foreach ($constructorParameters as $constructorParameter) {
258
                $paramName = $constructorParameter->name;
259
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
260
261
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
262
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
263
                if ($constructorParameter->isVariadic()) {
264
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
265
                        if (!\is_array($data[$paramName])) {
266
                            throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name));
267
                        }
268
269
                        $params = array_merge($params, $data[$paramName]);
270
                    }
271
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
272
                    $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
273
274
                    // Don't run set for a parameter passed to the constructor
275
                    unset($data[$key]);
276
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
277
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
278
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
279
                    $params[] = $constructorParameter->getDefaultValue();
280
                } else {
281
                    throw new MissingConstructorArgumentsException(
282
                        sprintf(
283
                            'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
284
                            $class,
285
                            $constructorParameter->name
286
                        )
287
                    );
288
                }
289
            }
290
291
            if ($constructor->isConstructor()) {
292
                return $reflectionClass->newInstanceArgs($params);
293
            }
294
295
            return $constructor->invokeArgs(null, $params);
296
        }
297
298
        return new $class();
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
305
    {
306
        return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context);
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     *
312
     * Unused in this context.
313
     */
314
    protected function extractAttributes($object, $format = null, array $context = [])
315
    {
316
        return [];
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
323
    {
324
        $options = $this->getFactoryOptions($context);
325
        $propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options);
326
327
        $allowedAttributes = [];
328
        foreach ($propertyNames as $propertyName) {
329
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options);
330
331
            if (
332
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
333
                (
334
                    isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
335
                    isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
336
                )
337
            ) {
338
                $allowedAttributes[] = $propertyName;
339
            }
340
        }
341
342
        return $allowedAttributes;
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
349
    {
350
        $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
351
    }
352
353
    /**
354
     * Validates the type of the value. Allows using integers as floats for JSON formats.
355
     *
356
     * @throws InvalidArgumentException
357
     */
358
    protected function validateType(string $attribute, Type $type, $value, string $format = null)
359
    {
360
        $builtinType = $type->getBuiltinType();
361
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && false !== strpos($format, 'json')) {
362
            $isValid = \is_float($value) || \is_int($value);
363
        } else {
364
            $isValid = \call_user_func('is_'.$builtinType, $value);
365
        }
366
367
        if (!$isValid) {
368
            throw new InvalidArgumentException(sprintf(
369
                'The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value)
370
            ));
371
        }
372
    }
373
374
    /**
375
     * Denormalizes a collection of objects.
376
     *
377
     * @throws InvalidArgumentException
378
     */
379
    protected function denormalizeCollection(string $attribute, PropertyMetadata $propertyMetadata, Type $type, string $className, $value, ?string $format, array $context): array
380
    {
381
        if (!\is_array($value)) {
382
            throw new InvalidArgumentException(sprintf(
383
                'The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)
384
            ));
385
        }
386
387
        $collectionKeyType = $type->getCollectionKeyType();
388
        $collectionKeyBuiltinType = null === $collectionKeyType ? null : $collectionKeyType->getBuiltinType();
389
390
        $values = [];
391
        foreach ($value as $index => $obj) {
392
            if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
393
                throw new InvalidArgumentException(sprintf(
394
                        'The type of the key "%s" must be "%s", "%s" given.',
395
                        $index, $collectionKeyBuiltinType, \gettype($index))
396
                );
397
            }
398
399
            $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute, $format));
400
        }
401
402
        return $values;
403
    }
404
405
    /**
406
     * Denormalizes a relation.
407
     *
408
     * @throws LogicException
409
     * @throws UnexpectedValueException
410
     * @throws ItemNotFoundException
411
     *
412
     * @return object|null
413
     */
414
    protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context)
415
    {
416
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
417
418
        if (\is_string($value)) {
419
            try {
420
                return $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]);
421
            } catch (ItemNotFoundException $e) {
422
                if (!$supportsPlainIdentifiers) {
423
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
424
                }
425
            } catch (InvalidArgumentException $e) {
426
                if (!$supportsPlainIdentifiers) {
427
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
428
                }
429
            }
430
        }
431
432
        if ($propertyMetadata->isWritableLink()) {
433
            $context['api_allow_update'] = true;
434
435
            if (!$this->serializer instanceof DenormalizerInterface) {
436
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
437
            }
438
439
            try {
440
                return $this->serializer->denormalize($value, $className, $format, $context);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->serializer...ame, $format, $context) also could return the type array which is incompatible with the documented return type null|object.
Loading history...
441
            } catch (InvalidValueException $e) {
442
                if (!$supportsPlainIdentifiers) {
443
                    throw $e;
444
                }
445
            }
446
        }
447
448
        if (!\is_array($value)) {
449
            if (!$supportsPlainIdentifiers) {
450
                throw new UnexpectedValueException(sprintf(
451
                    'Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value)
452
                ));
453
            }
454
455
            $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]);
456
            if (null === $item) {
457
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value));
458
            }
459
460
            return $item;
461
        }
462
463
        throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName));
464
    }
465
466
    /**
467
     * Gets a valid context for property metadata factories.
468
     *
469
     * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php
470
     */
471
    protected function getFactoryOptions(array $context): array
472
    {
473
        $options = [];
474
475
        if (isset($context[self::GROUPS])) {
476
            $options['serializer_groups'] = $context[self::GROUPS];
477
        }
478
479
        if (isset($context['collection_operation_name'])) {
480
            $options['collection_operation_name'] = $context['collection_operation_name'];
481
        }
482
483
        if (isset($context['item_operation_name'])) {
484
            $options['item_operation_name'] = $context['item_operation_name'];
485
        }
486
487
        return $options;
488
    }
489
490
    /**
491
     * Creates the context to use when serializing a relation.
492
     *
493
     * @deprecated since version 2.1, to be removed in 3.0.
494
     */
495
    protected function createRelationSerializationContext(string $resourceClass, array $context): array
496
    {
497
        @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED);
498
499
        return $context;
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     *
505
     * @throws NoSuchPropertyException
506
     * @throws LogicException
507
     */
508
    protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
509
    {
510
        $context['api_attribute'] = $attribute;
511
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
512
513
        try {
514
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
515
        } catch (NoSuchPropertyException $e) {
516
            if (!$propertyMetadata->hasChildInherited()) {
517
                throw $e;
518
            }
519
520
            $attributeValue = null;
521
        }
522
523
        $type = $propertyMetadata->getType();
524
525
        if (
526
            is_iterable($attributeValue) &&
527
            $type &&
528
            $type->isCollection() &&
529
            ($collectionValueType = $type->getCollectionValueType()) &&
530
            ($className = $collectionValueType->getClassName()) &&
531
            $this->resourceClassResolver->isResourceClass($className)
532
        ) {
533
            $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
534
            $childContext = $this->createChildContext($context, $attribute, $format);
535
            $childContext['resource_class'] = $resourceClass;
536
            unset($childContext['iri']);
537
538
            return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
539
        }
540
541
        if (
542
            $type &&
543
            ($className = $type->getClassName()) &&
544
            $this->resourceClassResolver->isResourceClass($className)
545
        ) {
546
            $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
547
            $childContext = $this->createChildContext($context, $attribute, $format);
548
            $childContext['resource_class'] = $resourceClass;
549
            unset($childContext['iri']);
550
551
            return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
552
        }
553
554
        if (!$this->serializer instanceof NormalizerInterface) {
555
            throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
556
        }
557
558
        unset($context['resource_class']);
559
560
        return $this->serializer->normalize($attributeValue, $format, $context);
561
    }
562
563
    /**
564
     * Normalizes a collection of relations (to-many).
565
     *
566
     * @param iterable $attributeValue
567
     */
568
    protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, ?string $format, array $context): array
569
    {
570
        $value = [];
571
        foreach ($attributeValue as $index => $obj) {
572
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
573
        }
574
575
        return $value;
576
    }
577
578
    /**
579
     * Normalizes a relation as an object if is a Link or as an URI.
580
     *
581
     * @throws LogicException
582
     *
583
     * @return string|array
584
     */
585
    protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context)
586
    {
587
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
588
            if (!$this->serializer instanceof NormalizerInterface) {
589
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
590
            }
591
592
            return $this->serializer->normalize($relatedObject, $format, $context);
593
        }
594
595
        $iri = $this->iriConverter->getIriFromItem($relatedObject);
596
        if (isset($context['resources'])) {
597
            $context['resources'][$iri] = $iri;
598
        }
599
        if (isset($context['resources_to_push']) && $propertyMetadata->getAttribute('push', false)) {
600
            $context['resources_to_push'][$iri] = $iri;
601
        }
602
603
        return $iri;
604
    }
605
606
    /**
607
     * Finds the first supported data transformer if any.
608
     *
609
     * @param object|array $data object on normalize / array on denormalize
610
     */
611
    protected function getDataTransformer($data, string $to, array $context = []): ?DataTransformerInterface
612
    {
613
        foreach ($this->dataTransformers as $dataTransformer) {
614
            if ($dataTransformer->supportsTransformation($data, $to, $context)) {
615
                return $dataTransformer;
616
            }
617
        }
618
619
        return null;
620
    }
621
622
    /**
623
     * For a given resource, it returns an output representation if any
624
     * If not, the resource is returned.
625
     */
626
    protected function transformOutput($object, array $context = [])
627
    {
628
        $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
629
        if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) {
630
            return $dataTransformer->transform($object, $outputClass, $context);
631
        }
632
633
        return $object;
634
    }
635
636
    private function createAttributeValue($attribute, $value, $format = null, array $context = [])
637
    {
638
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
639
        $type = $propertyMetadata->getType();
640
641
        if (null === $type) {
642
            // No type provided, blindly return the value
643
            return $value;
644
        }
645
646
        if (null === $value && $type->isNullable()) {
647
            return $value;
648
        }
649
650
        if (
651
            $type->isCollection() &&
652
            null !== ($collectionValueType = $type->getCollectionValueType()) &&
653
            null !== ($className = $collectionValueType->getClassName()) &&
654
            $this->resourceClassResolver->isResourceClass($className)
655
        ) {
656
            $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
657
            $context['resource_class'] = $resourceClass;
658
659
            return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
660
        }
661
662
        if (
663
            null !== ($className = $type->getClassName()) &&
664
            $this->resourceClassResolver->isResourceClass($className)
665
        ) {
666
            $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
667
            $childContext = $this->createChildContext($context, $attribute, $format);
668
            $childContext['resource_class'] = $resourceClass;
669
670
            return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
671
        }
672
673
        if (
674
            $type->isCollection() &&
675
            null !== ($collectionValueType = $type->getCollectionValueType()) &&
676
            null !== ($className = $collectionValueType->getClassName())
677
        ) {
678
            if (!$this->serializer instanceof DenormalizerInterface) {
679
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
680
            }
681
682
            unset($context['resource_class']);
683
684
            return $this->serializer->denormalize($value, $className.'[]', $format, $context);
685
        }
686
687
        if (null !== $className = $type->getClassName()) {
688
            if (!$this->serializer instanceof DenormalizerInterface) {
689
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
690
            }
691
692
            unset($context['resource_class']);
693
694
            return $this->serializer->denormalize($value, $className, $format, $context);
695
        }
696
697
        if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
698
            return $value;
699
        }
700
701
        $this->validateType($attribute, $type, $value, $format);
702
703
        return $value;
704
    }
705
706
    /**
707
     * Sets a value of the object using the PropertyAccess component.
708
     *
709
     * @param object $object
710
     */
711
    private function setValue($object, string $attributeName, $value)
712
    {
713
        try {
714
            $this->propertyAccessor->setValue($object, $attributeName, $value);
715
        } catch (NoSuchPropertyException $exception) {
716
            // Properties not found are ignored
717
        }
718
    }
719
720
    private function supportsPlainIdentifiers(): bool
721
    {
722
        return $this->allowPlainIdentifiers && null !== $this->itemDataProvider;
723
    }
724
}
725