Passed
Pull Request — master (#2983)
by Kévin
04:30
created

src/Serializer/AbstractItemNormalizer.php (7 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
187
            return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
188
        }
189
190
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
191
192
        if (\is_string($data)) {
193
            try {
194
                return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]);
195
            } catch (ItemNotFoundException $e) {
196
                if (!$supportsPlainIdentifiers) {
197
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
198
                }
199
            } catch (InvalidArgumentException $e) {
200
                if (!$supportsPlainIdentifiers) {
201
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
202
                }
203
            }
204
        }
205
206
        if (!\is_array($data)) {
207
            if (!$supportsPlainIdentifiers) {
208
                throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
209
            }
210
211
            $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);
0 ignored issues
show
The method getItem() does not exist on null. ( Ignorable by Annotation )

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

211
            /** @scrutinizer ignore-call */ 
212
            $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
212
            if (null === $item) {
213
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data));
214
            }
215
216
            return $item;
217
        }
218
219
        return parent::denormalize($data, $resourceClass, $format, $context);
220
    }
221
222
    /**
223
     * Method copy-pasted from symfony/serializer.
224
     * Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263.
225
     *
226
     * {@inheritdoc}
227
     *
228
     * @internal
229
     */
230
    protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
231
    {
232
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
233
            unset($context[static::OBJECT_TO_POPULATE]);
234
235
            return $object;
236
        }
237
238
        if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
239
            if (!isset($data[$mapping->getTypeProperty()])) {
240
                throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
241
            }
242
243
            $type = $data[$mapping->getTypeProperty()];
244
            if (null === ($mappedClass = $mapping->getClassForType($type))) {
245
                throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
246
            }
247
248
            $class = $mappedClass;
249
            $reflectionClass = new \ReflectionClass($class);
250
        }
251
252
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
253
        if ($constructor) {
0 ignored issues
show
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
254
            $constructorParameters = $constructor->getParameters();
255
256
            $params = [];
257
            foreach ($constructorParameters as $constructorParameter) {
258
                $paramName = $constructorParameter->name;
259
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
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

259
                $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...
260
261
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
262
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
263
                if ($constructorParameter->isVariadic()) {
264
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
265
                        if (!\is_array($data[$paramName])) {
266
                            throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name));
267
                        }
268
269
                        $params = array_merge($params, $data[$paramName]);
270
                    }
271
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
272
                    $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
273
274
                    // Don't run set for a parameter passed to the constructor
275
                    unset($data[$key]);
276
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
277
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
278
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
279
                    $params[] = $constructorParameter->getDefaultValue();
280
                } else {
281
                    throw new MissingConstructorArgumentsException(
282
                        sprintf(
283
                            'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
284
                            $class,
285
                            $constructorParameter->name
286
                        )
287
                    );
288
                }
289
            }
290
291
            if ($constructor->isConstructor()) {
292
                return $reflectionClass->newInstanceArgs($params);
293
            }
294
295
            return $constructor->invokeArgs(null, $params);
296
        }
297
298
        return new $class();
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
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

304
    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...
305
    {
306
        return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context);
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     *
312
     * Unused in this context.
313
     */
314
    protected function extractAttributes($object, $format = null, array $context = [])
315
    {
316
        return [];
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
323
    {
324
        $options = $this->getFactoryOptions($context);
325
        $propertyNames = $this->propertyNameCollectionFactory->create($context['resource_class'], $options);
326
327
        $allowedAttributes = [];
328
        foreach ($propertyNames as $propertyName) {
329
            $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options);
330
331
            if (
332
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
333
                (
334
                    isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
335
                    isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
336
                )
337
            ) {
338
                $allowedAttributes[] = $propertyName;
339
            }
340
        }
341
342
        return $allowedAttributes;
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
349
    {
350
        $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
351
    }
352
353
    /**
354
     * Validates the type of the value. Allows using integers as floats for JSON formats.
355
     *
356
     * @throws InvalidArgumentException
357
     */
358
    protected function validateType(string $attribute, Type $type, $value, string $format = null)
359
    {
360
        $builtinType = $type->getBuiltinType();
361
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && false !== strpos($format, 'json')) {
362
            $isValid = \is_float($value) || \is_int($value);
363
        } else {
364
            $isValid = \call_user_func('is_'.$builtinType, $value);
365
        }
366
367
        if (!$isValid) {
368
            throw new InvalidArgumentException(sprintf(
369
                'The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value)
370
            ));
371
        }
372
    }
373
374
    /**
375
     * Denormalizes a collection of objects.
376
     *
377
     * @throws InvalidArgumentException
378
     */
379
    protected function denormalizeCollection(string $attribute, PropertyMetadata $propertyMetadata, Type $type, string $className, $value, ?string $format, array $context): array
380
    {
381
        if (!\is_array($value)) {
382
            throw new InvalidArgumentException(sprintf(
383
                'The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)
384
            ));
385
        }
386
387
        $collectionKeyType = $type->getCollectionKeyType();
388
        $collectionKeyBuiltinType = null === $collectionKeyType ? null : $collectionKeyType->getBuiltinType();
389
390
        $values = [];
391
        foreach ($value as $index => $obj) {
392
            if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
393
                throw new InvalidArgumentException(sprintf(
394
                        'The type of the key "%s" must be "%s", "%s" given.',
395
                        $index, $collectionKeyBuiltinType, \gettype($index))
396
                );
397
            }
398
399
            $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $this->createChildContext($context, $attribute, $format));
400
        }
401
402
        return $values;
403
    }
404
405
    /**
406
     * Denormalizes a relation.
407
     *
408
     * @throws LogicException
409
     * @throws UnexpectedValueException
410
     * @throws ItemNotFoundException
411
     *
412
     * @return object|null
413
     */
414
    protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, ?string $format, array $context)
415
    {
416
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
417
418
        if (\is_string($value)) {
419
            try {
420
                return $this->iriConverter->getItemFromIri($value, $context + ['fetch_data' => true]);
421
            } catch (ItemNotFoundException $e) {
422
                if (!$supportsPlainIdentifiers) {
423
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
424
                }
425
            } catch (InvalidArgumentException $e) {
426
                if (!$supportsPlainIdentifiers) {
427
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
428
                }
429
            }
430
        }
431
432
        if ($propertyMetadata->isWritableLink()) {
433
            $context['api_allow_update'] = true;
434
435
            if (!$this->serializer instanceof DenormalizerInterface) {
436
                throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
437
            }
438
439
            try {
440
                return $this->serializer->denormalize($value, $className, $format, $context);
441
            } catch (InvalidValueException $e) {
442
                if (!$supportsPlainIdentifiers) {
443
                    throw $e;
444
                }
445
            }
446
        }
447
448
        if (!\is_array($value)) {
449
            if (!$supportsPlainIdentifiers) {
450
                throw new UnexpectedValueException(sprintf(
451
                    'Expected IRI or nested document for attribute "%s", "%s" given.', $attributeName, \gettype($value)
452
                ));
453
            }
454
455
            $item = $this->itemDataProvider->getItem($className, $value, null, $context + ['fetch_data' => true]);
456
            if (null === $item) {
457
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $className, $value));
458
            }
459
460
            return $item;
461
        }
462
463
        throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName));
464
    }
465
466
    /**
467
     * Gets a valid context for property metadata factories.
468
     *
469
     * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php
470
     */
471
    protected function getFactoryOptions(array $context): array
472
    {
473
        $options = [];
474
475
        if (isset($context[self::GROUPS])) {
476
            $options['serializer_groups'] = $context[self::GROUPS];
477
        }
478
479
        if (isset($context['collection_operation_name'])) {
480
            $options['collection_operation_name'] = $context['collection_operation_name'];
481
        }
482
483
        if (isset($context['item_operation_name'])) {
484
            $options['item_operation_name'] = $context['item_operation_name'];
485
        }
486
487
        return $options;
488
    }
489
490
    /**
491
     * Creates the context to use when serializing a relation.
492
     *
493
     * @deprecated since version 2.1, to be removed in 3.0.
494
     */
495
    protected function createRelationSerializationContext(string $resourceClass, array $context): array
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

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