Issues (332)

src/Serializer/AbstractItemNormalizer.php (1 issue)

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) && method_exists($this, 'setCircularReferenceHandler')) {
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
        if (null === $objectToPopulate = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
173
            $normalizedData = $this->prepareForDenormalization($data);
174
            $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class);
175
        }
176
        $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
177
        $context['api_denormalize'] = true;
178
        $context['resource_class'] = $resourceClass;
179
180
        if (null !== ($inputClass = $this->getInputClass($resourceClass, $context)) && null !== ($dataTransformer = $this->getDataTransformer($data, $resourceClass, $context))) {
181
            $dataTransformerContext = $context;
182
183
            unset($context['input']);
184
            unset($context['resource_class']);
185
186
            if (!$this->serializer instanceof DenormalizerInterface) {
187
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
188
            }
189
            $denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context);
190
            if (!\is_object($denormalizedInput)) {
191
                throw new \UnexpectedValueException('Expected denormalized input to be an object.');
192
            }
193
194
            return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
195
        }
196
197
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
198
199
        if (\is_string($data)) {
200
            try {
201
                return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]);
202
            } catch (ItemNotFoundException $e) {
203
                if (!$supportsPlainIdentifiers) {
204
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
205
                }
206
            } catch (InvalidArgumentException $e) {
207
                if (!$supportsPlainIdentifiers) {
208
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
209
                }
210
            }
211
        }
212
213
        if (!\is_array($data)) {
214
            if (!$supportsPlainIdentifiers) {
215
                throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
216
            }
217
218
            $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);
219
            if (null === $item) {
220
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data));
221
            }
222
223
            return $item;
224
        }
225
226
        return parent::denormalize($data, $resourceClass, $format, $context);
227
    }
228
229
    /**
230
     * Originally from {@see https://github.com/symfony/symfony/pull/28263}. Refactor after it is merged.
231
     *
232
     * {@inheritdoc}
233
     *
234
     * @internal
235
     */
236
    protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
237
    {
238
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
239
            unset($context[static::OBJECT_TO_POPULATE]);
240
241
            return $object;
242
        }
243
244
        $class = $this->getClassDiscriminatorResolvedClass($data, $class);
245
        $reflectionClass = new \ReflectionClass($class);
246
247
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
248
        if ($constructor) {
249
            $constructorParameters = $constructor->getParameters();
250
251
            $params = [];
252
            foreach ($constructorParameters as $constructorParameter) {
253
                $paramName = $constructorParameter->name;
254
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
255
256
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
257
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
258
                if ($constructorParameter->isVariadic()) {
259
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
260
                        if (!\is_array($data[$paramName])) {
261
                            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));
262
                        }
263
264
                        $params = array_merge($params, $data[$paramName]);
265
                    }
266
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
267
                    $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
268
269
                    // Don't run set for a parameter passed to the constructor
270
                    unset($data[$key]);
271
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
272
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
273
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
274
                    $params[] = $constructorParameter->getDefaultValue();
275
                } else {
276
                    throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
277
                }
278
            }
279
280
            if ($constructor->isConstructor()) {
281
                return $reflectionClass->newInstanceArgs($params);
282
            }
283
284
            return $constructor->invokeArgs(null, $params);
285
        }
286
287
        return new $class();
288
    }
289
290
    protected function getClassDiscriminatorResolvedClass(array &$data, string $class): string
291
    {
292
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
293
            return $class;
294
        }
295
296
        if (!isset($data[$mapping->getTypeProperty()])) {
297
            throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
298
        }
299
300
        $type = $data[$mapping->getTypeProperty()];
301
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
302
            throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
303
        }
304
305
        return $mappedClass;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
312
    {
313
        return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context);
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     *
319
     * Unused in this context.
320
     */
321
    protected function extractAttributes($object, $format = null, array $context = [])
322
    {
323
        return [];
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329
    protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
330
    {
331
        $options = $this->getFactoryOptions($context);
332
        $propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options);
333
334
        $allowedAttributes = [];
335
        foreach ($propertyNames as $propertyName) {
336
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options);
337
338
            if (
339
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
340
                (
341
                    isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
342
                    isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
343
                )
344
            ) {
345
                $allowedAttributes[] = $propertyName;
346
            }
347
        }
348
349
        return $allowedAttributes;
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     */
355
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
356
    {
357
        $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
358
    }
359
360
    /**
361
     * Validates the type of the value. Allows using integers as floats for JSON formats.
362
     *
363
     * @throws InvalidArgumentException
364
     */
365
    protected function validateType(string $attribute, Type $type, $value, string $format = null)
366
    {
367
        $builtinType = $type->getBuiltinType();
368
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && false !== strpos($format, 'json')) {
369
            $isValid = \is_float($value) || \is_int($value);
370
        } else {
371
            $isValid = \call_user_func('is_'.$builtinType, $value);
372
        }
373
374
        if (!$isValid) {
375
            throw new InvalidArgumentException(sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value)));
376
        }
377
    }
378
379
    /**
380
     * Denormalizes a collection of objects.
381
     *
382
     * @throws InvalidArgumentException
383
     */
384
    protected function denormalizeCollection(string $attribute, PropertyMetadata $propertyMetadata, Type $type, string $className, $value, ?string $format, array $context): array
385
    {
386
        if (!\is_array($value)) {
387
            throw new InvalidArgumentException(sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)));
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('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyBuiltinType, \gettype($index)));
397
            }
398
399
            $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute, $format));
400
        }
401
402
        return $values;
403
    }
404
405
    /**
406
     * Denormalizes a relation.
407
     *
408
     * @throws LogicException
409
     * @throws UnexpectedValueException
410
     * @throws ItemNotFoundException
411
     *
412
     * @return object|null
413
     */
414
    protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context)
415
    {
416
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
417
418
        if (\is_string($value)) {
419
            try {
420
                return $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]);
421
            } catch (ItemNotFoundException $e) {
422
                if (!$supportsPlainIdentifiers) {
423
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
424
                }
425
            } catch (InvalidArgumentException $e) {
426
                if (!$supportsPlainIdentifiers) {
427
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
428
                }
429
            }
430
        }
431
432
        if ($propertyMetadata->isWritableLink()) {
433
            $context['api_allow_update'] = true;
434
435
            if (!$this->serializer instanceof DenormalizerInterface) {
436
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
437
            }
438
439
            try {
440
                $item = $this->serializer->denormalize($value, $className, $format, $context);
441
                if (!\is_object($item) && null !== $item) {
442
                    throw new \UnexpectedValueException('Expected item to be an object or null.');
443
                }
444
445
                return $item;
446
            } catch (InvalidValueException $e) {
447
                if (!$supportsPlainIdentifiers) {
448
                    throw $e;
449
                }
450
            }
451
        }
452
453
        if (!\is_array($value)) {
454
            if (!$supportsPlainIdentifiers) {
455
                throw new UnexpectedValueException(sprintf('Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value)));
456
            }
457
458
            $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]);
459
            if (null === $item) {
460
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value));
461
            }
462
463
            return $item;
464
        }
465
466
        throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName));
467
    }
468
469
    /**
470
     * Gets the options for the property name collection / property metadata factories.
471
     */
472
    protected function getFactoryOptions(array $context): array
473
    {
474
        $options = [];
475
476
        if (isset($context[self::GROUPS])) {
477
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
478
            $options['serializer_groups'] = (array) $context[self::GROUPS];
479
        }
480
481
        if (isset($context['collection_operation_name'])) {
482
            $options['collection_operation_name'] = $context['collection_operation_name'];
483
        }
484
485
        if (isset($context['item_operation_name'])) {
486
            $options['item_operation_name'] = $context['item_operation_name'];
487
        }
488
489
        return $options;
490
    }
491
492
    /**
493
     * Creates the context to use when serializing a relation.
494
     *
495
     * @deprecated since version 2.1, to be removed in 3.0.
496
     */
497
    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

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