Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
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 metadata factories.
476
     *
477
     * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php
478
     */
479
    protected function getFactoryOptions(array $context): array
480
    {
481
        $options = [];
482
483
        if (isset($context[self::GROUPS])) {
484
            $options['serializer_groups'] = $context[self::GROUPS];
485
        }
486
487
        if (isset($context['collection_operation_name'])) {
488
            $options['collection_operation_name'] = $context['collection_operation_name'];
489
        }
490
491
        if (isset($context['item_operation_name'])) {
492
            $options['item_operation_name'] = $context['item_operation_name'];
493
        }
494
495
        return $options;
496
    }
497
498
    /**
499
     * Creates the context to use when serializing a relation.
500
     *
501
     * @deprecated since version 2.1, to be removed in 3.0.
502
     */
503
    protected function createRelationSerializationContext(string $resourceClass, array $context): array
504
    {
505
        @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED);
506
507
        return $context;
508
    }
509
510
    /**
511
     * {@inheritdoc}
512
     *
513
     * @throws NoSuchPropertyException
514
     * @throws LogicException
515
     */
516
    protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
517
    {
518
        $context['api_attribute'] = $attribute;
519
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
520
521
        try {
522
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
523
        } catch (NoSuchPropertyException $e) {
524
            if (!$propertyMetadata->hasChildInherited()) {
525
                throw $e;
526
            }
527
528
            $attributeValue = null;
529
        }
530
531
        $type = $propertyMetadata->getType();
532
533
        if (
534
            is_iterable($attributeValue) &&
535
            $type &&
536
            $type->isCollection() &&
537
            ($collectionValueType = $type->getCollectionValueType()) &&
538
            ($className = $collectionValueType->getClassName()) &&
539
            $this->resourceClassResolver->isResourceClass($className)
540
        ) {
541
            $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
542
            $childContext = $this->createChildContext($context, $attribute, $format);
543
            $childContext['resource_class'] = $resourceClass;
544
            unset($childContext['iri']);
545
546
            return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
547
        }
548
549
        if (
550
            $type &&
551
            ($className = $type->getClassName()) &&
552
            $this->resourceClassResolver->isResourceClass($className)
553
        ) {
554
            $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
555
            $childContext = $this->createChildContext($context, $attribute, $format);
556
            $childContext['resource_class'] = $resourceClass;
557
            unset($childContext['iri']);
558
559
            return $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
560
        }
561
562
        if (!$this->serializer instanceof NormalizerInterface) {
563
            throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
564
        }
565
566
        unset($context['resource_class']);
567
568
        return $this->serializer->normalize($attributeValue, $format, $context);
569
    }
570
571
    /**
572
     * Normalizes a collection of relations (to-many).
573
     *
574
     * @param iterable $attributeValue
575
     */
576
    protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, ?string $format, array $context): array
577
    {
578
        $value = [];
579
        foreach ($attributeValue as $index => $obj) {
580
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
581
        }
582
583
        return $value;
584
    }
585
586
    /**
587
     * Normalizes a relation as an object if is a Link or as an URI.
588
     *
589
     * @throws LogicException
590
     *
591
     * @return string|array
592
     */
593
    protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, ?string $format, array $context)
594
    {
595
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
596
            if (!$this->serializer instanceof NormalizerInterface) {
597
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
598
            }
599
600
            return $this->serializer->normalize($relatedObject, $format, $context);
601
        }
602
603
        $iri = $this->iriConverter->getIriFromItem($relatedObject);
604
        if (isset($context['resources'])) {
605
            $context['resources'][$iri] = $iri;
606
        }
607
        if (isset($context['resources_to_push']) && $propertyMetadata->getAttribute('push', false)) {
608
            $context['resources_to_push'][$iri] = $iri;
609
        }
610
611
        return $iri;
612
    }
613
614
    /**
615
     * Finds the first supported data transformer if any.
616
     *
617
     * @param object|array $data object on normalize / array on denormalize
618
     */
619
    protected function getDataTransformer($data, string $to, array $context = []): ?DataTransformerInterface
620
    {
621
        foreach ($this->dataTransformers as $dataTransformer) {
622
            if ($dataTransformer->supportsTransformation($data, $to, $context)) {
623
                return $dataTransformer;
624
            }
625
        }
626
627
        return null;
628
    }
629
630
    /**
631
     * For a given resource, it returns an output representation if any
632
     * If not, the resource is returned.
633
     */
634
    protected function transformOutput($object, array $context = [])
635
    {
636
        $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
637
        if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) {
638
            return $dataTransformer->transform($object, $outputClass, $context);
639
        }
640
641
        return $object;
642
    }
643
644
    private function createAttributeValue($attribute, $value, $format = null, array $context = [])
645
    {
646
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
647
        $type = $propertyMetadata->getType();
648
649
        if (null === $type) {
650
            // No type provided, blindly return the value
651
            return $value;
652
        }
653
654
        if (null === $value && $type->isNullable()) {
655
            return $value;
656
        }
657
658
        if (
659
            $type->isCollection() &&
660
            null !== ($collectionValueType = $type->getCollectionValueType()) &&
661
            null !== ($className = $collectionValueType->getClassName()) &&
662
            $this->resourceClassResolver->isResourceClass($className)
663
        ) {
664
            $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
665
            $context['resource_class'] = $resourceClass;
666
667
            return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
668
        }
669
670
        if (
671
            null !== ($className = $type->getClassName()) &&
672
            $this->resourceClassResolver->isResourceClass($className)
673
        ) {
674
            $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
675
            $childContext = $this->createChildContext($context, $attribute, $format);
676
            $childContext['resource_class'] = $resourceClass;
677
678
            return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
679
        }
680
681
        if (
682
            $type->isCollection() &&
683
            null !== ($collectionValueType = $type->getCollectionValueType()) &&
684
            null !== ($className = $collectionValueType->getClassName())
685
        ) {
686
            if (!$this->serializer instanceof DenormalizerInterface) {
687
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
688
            }
689
690
            unset($context['resource_class']);
691
692
            return $this->serializer->denormalize($value, $className.'[]', $format, $context);
693
        }
694
695
        if (null !== $className = $type->getClassName()) {
696
            if (!$this->serializer instanceof DenormalizerInterface) {
697
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
698
            }
699
700
            unset($context['resource_class']);
701
702
            return $this->serializer->denormalize($value, $className, $format, $context);
703
        }
704
705
        if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
706
            return $value;
707
        }
708
709
        $this->validateType($attribute, $type, $value, $format);
710
711
        return $value;
712
    }
713
714
    /**
715
     * Sets a value of the object using the PropertyAccess component.
716
     *
717
     * @param object $object
718
     */
719
    private function setValue($object, string $attributeName, $value)
720
    {
721
        try {
722
            $this->propertyAccessor->setValue($object, $attributeName, $value);
723
        } catch (NoSuchPropertyException $exception) {
724
            // Properties not found are ignored
725
        }
726
    }
727
728
    private function supportsPlainIdentifiers(): bool
729
    {
730
        return $this->allowPlainIdentifiers && null !== $this->itemDataProvider;
731
    }
732
}
733