Completed
Push — master ( ac8ec4...1a57ac )
by Kévin
04:37 queued 11s
created

src/Serializer/AbstractItemNormalizer.php (4 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']);
0 ignored issues
show
Deprecated Code introduced by
The function Symfony\Component\Serial...cularReferenceHandler() has been deprecated: since Symfony 4.2 ( Ignorable by Annotation )

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

72
            /** @scrutinizer ignore-deprecated */ $this->setCircularReferenceHandler($defaultContext['circular_reference_handler']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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) {
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;
0 ignored issues
show
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $class. ( Ignorable by Annotation )

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

262
                $key = $this->nameConverter ? $this->nameConverter->/** @scrutinizer ignore-call */ normalize($paramName, $class, $format, $context) : $paramName;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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
0 ignored issues
show
The parameter $resourceClass 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

502
    protected function createRelationSerializationContext(/** @scrutinizer ignore-unused */ string $resourceClass, array $context): array

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...
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