Completed
Push — master ( d53149...bf867e )
by Antoine
20s queued 11s
created

AbstractItemNormalizer::getDataTransformer()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 3
dl 0
loc 9
rs 10
c 0
b 0
f 0
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\Bridge\Elasticsearch\Serializer\ItemNormalizer;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ApiPlatform\Core\Serializer\ItemNormalizer. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
19
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
20
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
21
use ApiPlatform\Core\Exception\InvalidArgumentException;
22
use ApiPlatform\Core\Exception\InvalidValueException;
23
use ApiPlatform\Core\Exception\ItemNotFoundException;
24
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
25
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
26
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
27
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
28
use ApiPlatform\Core\Util\ClassInfoTrait;
29
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
30
use Symfony\Component\PropertyAccess\PropertyAccess;
31
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
32
use Symfony\Component\PropertyInfo\Type;
33
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
34
use Symfony\Component\Serializer\Exception\RuntimeException;
35
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
36
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
37
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
38
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
39
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
40
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
41
42
/**
43
 * Base item normalizer.
44
 *
45
 * @author Kévin Dunglas <[email protected]>
46
 */
47
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
48
{
49
    use ClassInfoTrait;
50
    use ContextTrait;
51
    use InputOutputMetadataTrait;
52
53
    protected $propertyNameCollectionFactory;
54
    protected $propertyMetadataFactory;
55
    protected $iriConverter;
56
    protected $resourceClassResolver;
57
    protected $propertyAccessor;
58
    protected $localCache = [];
59
    protected $itemDataProvider;
60
    protected $allowPlainIdentifiers;
61
    protected $allowUnmappedClass;
62
    protected $dataTransformers = [];
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, bool $allowUnmappedClass = false)
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
        if (false === $allowUnmappedClass) {
76
            @trigger_error(sprintf('Passing a falsy $allowUnmappedClass flag in %s is deprecated since version 2.4 and will default to true in 3.0.', self::class), E_USER_DEPRECATED);
77
        }
78
79
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable([$this, 'getObjectClass']), $defaultContext);
80
81
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
82
        $this->propertyMetadataFactory = $propertyMetadataFactory;
83
        $this->iriConverter = $iriConverter;
84
        $this->resourceClassResolver = $resourceClassResolver;
85
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
86
        $this->itemDataProvider = $itemDataProvider;
87
        $this->allowPlainIdentifiers = $allowPlainIdentifiers;
88
        $this->dataTransformers = $dataTransformers;
89
        $this->resourceMetadataFactory = $resourceMetadataFactory;
90
        $this->allowUnmappedClass = $allowUnmappedClass;
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function supportsNormalization($data, $format = null)
97
    {
98
        if (!\is_object($data) || $data instanceof \Traversable) {
99
            return false;
100
        }
101
102
        if (false === $this->allowUnmappedClass) {
103
            return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
104
        }
105
106
        return true;
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function hasCacheableSupportsMethod(): bool
113
    {
114
        return true;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function normalize($object, $format = null, array $context = [])
121
    {
122
        try {
123
            $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
124
        } catch (InvalidArgumentException $e) {
125
            $context = $this->initContext(\get_class($object), $context);
126
            $context['api_normalize'] = true;
127
128
            return parent::normalize($object, $format, $context);
129
        }
130
131
        $context = $this->initContext($resourceClass, $context);
132
        $context['api_normalize'] = true;
133
134
        if (isset($context['resources'])) {
135
            $resource = $context['iri'] ?? $this->iriConverter->getIriFromItem($object);
136
            $context['resources'][$resource] = $resource;
137
        }
138
139
        return parent::normalize($object, $format, $context);
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function supportsDenormalization($data, $type, $format = null)
146
    {
147
        if (ItemNormalizer::FORMAT === $format) {
148
            return false;
149
        }
150
151
        if (false === $this->allowUnmappedClass) {
152
            return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
153
        }
154
155
        return true;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function denormalize($data, $class, $format = null, array $context = [])
162
    {
163
        $context['api_denormalize'] = true;
164
        $context['resource_class'] = $class;
165
        $inputClass = $this->getInputClass($class, $context);
166
167
        if (null !== $inputClass && null !== $dataTransformer = $this->getDataTransformer($data, $class, $context)) {
168
            $data = $dataTransformer->transform(
169
                parent::denormalize($data, $inputClass, $format, ['resource_class' => $inputClass] + $context),
170
                $class,
171
                $context
172
            );
173
        }
174
175
        return parent::denormalize($data, $class, $format, $context);
176
    }
177
178
    /**
179
     * Method copy-pasted from symfony/serializer.
180
     * Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263.
181
     *
182
     * {@inheritdoc}
183
     *
184
     * @internal
185
     */
186
    protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
187
    {
188
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
189
            unset($context[static::OBJECT_TO_POPULATE]);
190
191
            return $object;
192
        }
193
194
        if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
195
            if (!isset($data[$mapping->getTypeProperty()])) {
196
                throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
197
            }
198
199
            $type = $data[$mapping->getTypeProperty()];
200
            if (null === ($mappedClass = $mapping->getClassForType($type))) {
201
                throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
202
            }
203
204
            $class = $mappedClass;
205
            $reflectionClass = new \ReflectionClass($class);
206
        }
207
208
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
209
        if ($constructor) {
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
210
            $constructorParameters = $constructor->getParameters();
211
212
            $params = [];
213
            foreach ($constructorParameters as $constructorParameter) {
214
                $paramName = $constructorParameter->name;
215
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
0 ignored issues
show
Unused Code introduced by
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

215
                $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...
216
217
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
218
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
219
                if ($constructorParameter->isVariadic()) {
220
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
221
                        if (!\is_array($data[$paramName])) {
222
                            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));
223
                        }
224
225
                        $params = array_merge($params, $data[$paramName]);
226
                    }
227
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
228
                    $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
229
230
                    // Don't run set for a parameter passed to the constructor
231
                    unset($data[$key]);
232
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
233
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
234
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
235
                    $params[] = $constructorParameter->getDefaultValue();
236
                } else {
237
                    throw new MissingConstructorArgumentsException(
238
                        sprintf(
239
                            'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
240
                            $class,
241
                            $constructorParameter->name
242
                        )
243
                    );
244
                }
245
            }
246
247
            if ($constructor->isConstructor()) {
248
                return $reflectionClass->newInstanceArgs($params);
249
            }
250
251
            return $constructor->invokeArgs(null, $params);
252
        }
253
254
        return new $class();
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
0 ignored issues
show
Unused Code introduced by
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

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

496
    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...
497
    {
498
        @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED);
499
500
        return $context;
501
    }
502
503
    /**
504
     * {@inheritdoc}
505
     *
506
     * @throws NoSuchPropertyException
507
     */
508
    protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
509
    {
510
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
511
512
        try {
513
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
514
        } catch (NoSuchPropertyException $e) {
515
            if (null === $propertyMetadata->isChildInherited()) {
516
                throw $e;
517
            }
518
519
            $attributeValue = null;
520
        }
521
522
        $type = $propertyMetadata->getType();
523
524
        if (
525
            is_iterable($attributeValue) &&
526
            $type &&
527
            $type->isCollection() &&
528
            ($collectionValueType = $type->getCollectionValueType()) &&
529
            ($className = $collectionValueType->getClassName()) &&
530
            $this->resourceClassResolver->isResourceClass($className)
531
        ) {
532
            return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $className, $format, $this->createChildContext($context, $attribute));
533
        }
534
535
        if (
536
            $type &&
537
            ($className = $type->getClassName()) &&
538
            $this->resourceClassResolver->isResourceClass($className)
539
        ) {
540
            return $this->normalizeRelation($propertyMetadata, $attributeValue, $className, $format, $this->createChildContext($context, $attribute));
541
        }
542
543
        unset($context['resource_class']);
544
545
        if ($this->serializer instanceof NormalizerInterface) {
546
            return $this->serializer->normalize($attributeValue, $format, $context);
547
        }
548
        throw new InvalidArgumentException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
549
    }
550
551
    /**
552
     * Normalizes a collection of relations (to-many).
553
     *
554
     * @param iterable $attributeValue
555
     */
556
    protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, string $format = null, array $context): array
557
    {
558
        $value = [];
559
        foreach ($attributeValue as $index => $obj) {
560
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
561
        }
562
563
        return $value;
564
    }
565
566
    /**
567
     * Normalizes a relation as an object if is a Link or as an URI.
568
     *
569
     * @return string|array
570
     */
571
    protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
572
    {
573
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
574
            if (null === $relatedObject) {
575
                unset($context['resource_class']);
576
            } else {
577
                $context['resource_class'] = $resourceClass;
578
            }
579
580
            if ($this->serializer instanceof NormalizerInterface) {
581
                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...
582
            }
583
            throw new InvalidArgumentException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
584
        }
585
586
        $iri = $this->iriConverter->getIriFromItem($relatedObject);
587
        if (isset($context['resources'])) {
588
            $context['resources'][$iri] = $iri;
589
        }
590
        if (isset($context['resources_to_push']) && $propertyMetadata->getAttribute('push', false)) {
591
            $context['resources_to_push'][$iri] = $iri;
592
        }
593
594
        return $iri;
595
    }
596
597
    /**
598
     * Finds the first supported data transformer if any.
599
     */
600
    protected function getDataTransformer($object, string $to, array $context = []): ?DataTransformerInterface
601
    {
602
        foreach ($this->dataTransformers as $dataTransformer) {
603
            if ($dataTransformer->supportsTransformation($object, $to, $context)) {
604
                return $dataTransformer;
605
            }
606
        }
607
608
        return null;
609
    }
610
611
    /**
612
     * For a given resource, it returns an output representation if any
613
     * If not, the resource is returned.
614
     */
615
    protected function transformOutput($object, array $context = [])
616
    {
617
        $outputClass = $this->getOutputClass($this->getObjectClass($object), $context);
618
        if (null !== $outputClass && null !== $dataTransformer = $this->getDataTransformer($object, $outputClass, $context)) {
619
            return $dataTransformer->transform($object, $outputClass, $context);
620
        }
621
622
        return $object;
623
    }
624
}
625