Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

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

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