Completed
Push — master ( ac8ec4...1a57ac )
by Kévin
04:37 queued 11s
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
            if (!\is_object($denormalizedInput)) {
187
                throw new \UnexpectedValueException('Expected denormalized input to be an object.');
188
            }
189
190
            return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
191
        }
192
193
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
194
195
        if (\is_string($data)) {
196
            try {
197
                return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]);
198
            } catch (ItemNotFoundException $e) {
199
                if (!$supportsPlainIdentifiers) {
200
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
201
                }
202
            } catch (InvalidArgumentException $e) {
203
                if (!$supportsPlainIdentifiers) {
204
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
205
                }
206
            }
207
        }
208
209
        if (!\is_array($data)) {
210
            if (!$supportsPlainIdentifiers) {
211
                throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
212
            }
213
214
            $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);
215
            if (null === $item) {
216
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data));
217
            }
218
219
            return $item;
220
        }
221
222
        return parent::denormalize($data, $resourceClass, $format, $context);
223
    }
224
225
    /**
226
     * Method copy-pasted from symfony/serializer.
227
     * Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263.
228
     *
229
     * {@inheritdoc}
230
     *
231
     * @internal
232
     */
233
    protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
234
    {
235
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
236
            unset($context[static::OBJECT_TO_POPULATE]);
237
238
            return $object;
239
        }
240
241
        if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
242
            if (!isset($data[$mapping->getTypeProperty()])) {
243
                throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
244
            }
245
246
            $type = $data[$mapping->getTypeProperty()];
247
            if (null === ($mappedClass = $mapping->getClassForType($type))) {
248
                throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
249
            }
250
251
            $class = $mappedClass;
252
            $reflectionClass = new \ReflectionClass($class);
253
        }
254
255
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
256
        if ($constructor) {
0 ignored issues
show
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
257
            $constructorParameters = $constructor->getParameters();
258
259
            $params = [];
260
            foreach ($constructorParameters as $constructorParameter) {
261
                $paramName = $constructorParameter->name;
262
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
263
264
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
265
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
266
                if ($constructorParameter->isVariadic()) {
267
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
268
                        if (!\is_array($data[$paramName])) {
269
                            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));
270
                        }
271
272
                        $params = array_merge($params, $data[$paramName]);
273
                    }
274
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
275
                    $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
276
277
                    // Don't run set for a parameter passed to the constructor
278
                    unset($data[$key]);
279
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
280
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
281
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
282
                    $params[] = $constructorParameter->getDefaultValue();
283
                } else {
284
                    throw new MissingConstructorArgumentsException(
285
                        sprintf(
286
                            'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
287
                            $class,
288
                            $constructorParameter->name
289
                        )
290
                    );
291
                }
292
            }
293
294
            if ($constructor->isConstructor()) {
295
                return $reflectionClass->newInstanceArgs($params);
296
            }
297
298
            return $constructor->invokeArgs(null, $params);
299
        }
300
301
        return new $class();
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
0 ignored issues
show
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

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

307
    protected function createConstructorArgument($parameterData, /** @scrutinizer ignore-unused */ string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
308
    {
309
        return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context);
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     *
315
     * Unused in this context.
316
     */
317
    protected function extractAttributes($object, $format = null, array $context = [])
318
    {
319
        return [];
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
326
    {
327
        $options = $this->getFactoryOptions($context);
328
        $propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options);
329
330
        $allowedAttributes = [];
331
        foreach ($propertyNames as $propertyName) {
332
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options);
333
334
            if (
335
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
336
                (
337
                    isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
338
                    isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
339
                )
340
            ) {
341
                $allowedAttributes[] = $propertyName;
342
            }
343
        }
344
345
        return $allowedAttributes;
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     */
351
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
352
    {
353
        $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
354
    }
355
356
    /**
357
     * Validates the type of the value. Allows using integers as floats for JSON formats.
358
     *
359
     * @throws InvalidArgumentException
360
     */
361
    protected function validateType(string $attribute, Type $type, $value, string $format = null)
362
    {
363
        $builtinType = $type->getBuiltinType();
364
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && false !== strpos($format, 'json')) {
365
            $isValid = \is_float($value) || \is_int($value);
366
        } else {
367
            $isValid = \call_user_func('is_'.$builtinType, $value);
368
        }
369
370
        if (!$isValid) {
371
            throw new InvalidArgumentException(sprintf(
372
                'The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value)
373
            ));
374
        }
375
    }
376
377
    /**
378
     * Denormalizes a collection of objects.
379
     *
380
     * @throws InvalidArgumentException
381
     */
382
    protected function denormalizeCollection(string $attribute, PropertyMetadata $propertyMetadata, Type $type, string $className, $value, ?string $format, array $context): array
383
    {
384
        if (!\is_array($value)) {
385
            throw new InvalidArgumentException(sprintf(
386
                'The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)
387
            ));
388
        }
389
390
        $collectionKeyType = $type->getCollectionKeyType();
391
        $collectionKeyBuiltinType = null === $collectionKeyType ? null : $collectionKeyType->getBuiltinType();
392
393
        $values = [];
394
        foreach ($value as $index => $obj) {
395
            if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
396
                throw new InvalidArgumentException(sprintf(
397
                        'The type of the key "%s" must be "%s", "%s" given.',
398
                        $index, $collectionKeyBuiltinType, \gettype($index))
399
                );
400
            }
401
402
            $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute, $format));
403
        }
404
405
        return $values;
406
    }
407
408
    /**
409
     * Denormalizes a relation.
410
     *
411
     * @throws LogicException
412
     * @throws UnexpectedValueException
413
     * @throws ItemNotFoundException
414
     *
415
     * @return object|null
416
     */
417
    protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context)
418
    {
419
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
420
421
        if (\is_string($value)) {
422
            try {
423
                return $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]);
424
            } catch (ItemNotFoundException $e) {
425
                if (!$supportsPlainIdentifiers) {
426
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
427
                }
428
            } catch (InvalidArgumentException $e) {
429
                if (!$supportsPlainIdentifiers) {
430
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
431
                }
432
            }
433
        }
434
435
        if ($propertyMetadata->isWritableLink()) {
436
            $context['api_allow_update'] = true;
437
438
            if (!$this->serializer instanceof DenormalizerInterface) {
439
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
440
            }
441
442
            try {
443
                $item = $this->serializer->denormalize($value, $className, $format, $context);
444
                if (!\is_object($item) && null !== $item) {
445
                    throw new \UnexpectedValueException('Expected item to be an object or null.');
446
                }
447
448
                return $item;
449
            } catch (InvalidValueException $e) {
450
                if (!$supportsPlainIdentifiers) {
451
                    throw $e;
452
                }
453
            }
454
        }
455
456
        if (!\is_array($value)) {
457
            if (!$supportsPlainIdentifiers) {
458
                throw new UnexpectedValueException(sprintf(
459
                    'Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value)
460
                ));
461
            }
462
463
            $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]);
464
            if (null === $item) {
465
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value));
466
            }
467
468
            return $item;
469
        }
470
471
        throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName));
472
    }
473
474
    /**
475
     * Gets a valid context for property name collection / property metadata factories.
476
     */
477
    protected function getFactoryOptions(array $context): array
478
    {
479
        $options = [];
480
481
        if (isset($context[self::GROUPS])) {
482
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
483
            $options['serializer_groups'] = $context[self::GROUPS];
484
        }
485
486
        if (isset($context['collection_operation_name'])) {
487
            $options['collection_operation_name'] = $context['collection_operation_name'];
488
        }
489
490
        if (isset($context['item_operation_name'])) {
491
            $options['item_operation_name'] = $context['item_operation_name'];
492
        }
493
494
        return $options;
495
    }
496
497
    /**
498
     * Creates the context to use when serializing a relation.
499
     *
500
     * @deprecated since version 2.1, to be removed in 3.0.
501
     */
502
    protected function createRelationSerializationContext(string $resourceClass, array $context): array
503
    {
504
        @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED);
505
506
        return $context;
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     *
512
     * @throws NoSuchPropertyException
513
     * @throws LogicException
514
     */
515
    protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
516
    {
517
        $context['api_attribute'] = $attribute;
518
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
519
520
        try {
521
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
522
        } catch (NoSuchPropertyException $e) {
523
            if (!$propertyMetadata->hasChildInherited()) {
524
                throw $e;
525
            }
526
527
            $attributeValue = null;
528
        }
529
530
        $type = $propertyMetadata->getType();
531
532
        if (
533
            is_iterable($attributeValue) &&
534
            $type &&
535
            $type->isCollection() &&
536
            ($collectionValueType = $type->getCollectionValueType()) &&
537
            ($className = $collectionValueType->getClassName()) &&
538
            $this->resourceClassResolver->isResourceClass($className)
539
        ) {
540
            $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
541
            $childContext = $this->createChildContext($context, $attribute, $format);
542
            $childContext['resource_class'] = $resourceClass;
543
            unset($childContext['iri']);
544
545
            return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
546
        }
547
548
        if (
549
            $type &&
550
            ($className = $type->getClassName()) &&
551
            $this->resourceClassResolver->isResourceClass($className)
552
        ) {
553
            $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
554
            $childContext = $this->createChildContext($context, $attribute, $format);
555
            $childContext['resource_class'] = $resourceClass;
556
            unset($childContext['iri']);
557
558
            return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
559
        }
560
561
        if (!$this->serializer instanceof NormalizerInterface) {
562
            throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
563
        }
564
565
        unset($context['resource_class']);
566
567
        return $this->serializer->normalize($attributeValue, $format, $context);
568
    }
569
570
    /**
571
     * Normalizes a collection of relations (to-many).
572
     *
573
     * @param iterable $attributeValue
574
     */
575
    protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, ?string $format, array $context): array
576
    {
577
        $value = [];
578
        foreach ($attributeValue as $index => $obj) {
579
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
580
        }
581
582
        return $value;
583
    }
584
585
    /**
586
     * Normalizes a relation as an object if is a Link or as an URI.
587
     *
588
     * @throws LogicException
589
     *
590
     * @return string|array
591
     */
592
    protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context)
593
    {
594
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
595
            if (!$this->serializer instanceof NormalizerInterface) {
596
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
597
            }
598
599
            return $this->serializer->normalize($relatedObject, $format, $context);
600
        }
601
602
        $iri = $this->iriConverter->getIriFromItem($relatedObject);
603
        if (isset($context['resources'])) {
604
            $context['resources'][$iri] = $iri;
605
        }
606
        if (isset($context['resources_to_push']) && $propertyMetadata->getAttribute('push', false)) {
607
            $context['resources_to_push'][$iri] = $iri;
608
        }
609
610
        return $iri;
611
    }
612
613
    /**
614
     * Finds the first supported data transformer if any.
615
     *
616
     * @param object|array $data object on normalize / array on denormalize
617
     */
618
    protected function getDataTransformer($data, string $to, array $context = []): ?DataTransformerInterface
619
    {
620
        foreach ($this->dataTransformers as $dataTransformer) {
621
            if ($dataTransformer->supportsTransformation($data, $to, $context)) {
622
                return $dataTransformer;
623
            }
624
        }
625
626
        return null;
627
    }
628
629
    /**
630
     * For a given resource, it returns an output representation if any
631
     * If not, the resource is returned.
632
     */
633
    protected function transformOutput($object, array $context = [])
634
    {
635
        $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
636
        if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) {
637
            return $dataTransformer->transform($object, $outputClass, $context);
638
        }
639
640
        return $object;
641
    }
642
643
    private function createAttributeValue($attribute, $value, $format = null, array $context = [])
644
    {
645
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
646
        $type = $propertyMetadata->getType();
647
648
        if (null === $type) {
649
            // No type provided, blindly return the value
650
            return $value;
651
        }
652
653
        if (null === $value && $type->isNullable()) {
654
            return $value;
655
        }
656
657
        if (
658
            $type->isCollection() &&
659
            null !== ($collectionValueType = $type->getCollectionValueType()) &&
660
            null !== ($className = $collectionValueType->getClassName()) &&
661
            $this->resourceClassResolver->isResourceClass($className)
662
        ) {
663
            $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
664
            $context['resource_class'] = $resourceClass;
665
666
            return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
667
        }
668
669
        if (
670
            null !== ($className = $type->getClassName()) &&
671
            $this->resourceClassResolver->isResourceClass($className)
672
        ) {
673
            $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
674
            $childContext = $this->createChildContext($context, $attribute, $format);
675
            $childContext['resource_class'] = $resourceClass;
676
677
            return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
678
        }
679
680
        if (
681
            $type->isCollection() &&
682
            null !== ($collectionValueType = $type->getCollectionValueType()) &&
683
            null !== ($className = $collectionValueType->getClassName())
684
        ) {
685
            if (!$this->serializer instanceof DenormalizerInterface) {
686
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
687
            }
688
689
            unset($context['resource_class']);
690
691
            return $this->serializer->denormalize($value, $className.'[]', $format, $context);
692
        }
693
694
        if (null !== $className = $type->getClassName()) {
695
            if (!$this->serializer instanceof DenormalizerInterface) {
696
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
697
            }
698
699
            unset($context['resource_class']);
700
701
            return $this->serializer->denormalize($value, $className, $format, $context);
702
        }
703
704
        if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
705
            return $value;
706
        }
707
708
        $this->validateType($attribute, $type, $value, $format);
709
710
        return $value;
711
    }
712
713
    /**
714
     * Sets a value of the object using the PropertyAccess component.
715
     *
716
     * @param object $object
717
     */
718
    private function setValue($object, string $attributeName, $value)
719
    {
720
        try {
721
            $this->propertyAccessor->setValue($object, $attributeName, $value);
722
        } catch (NoSuchPropertyException $exception) {
723
            // Properties not found are ignored
724
        }
725
    }
726
727
    private function supportsPlainIdentifiers(): bool
728
    {
729
        return $this->allowPlainIdentifiers && null !== $this->itemDataProvider;
730
    }
731
}
732