Completed
Push — master ( a12ac4...ce7fff )
by Alan
20s queued 13s
created

AbstractItemNormalizer::normalizeRelation()   C

Complexity

Conditions 12
Paths 7

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 13
nc 7
nop 5
dl 0
loc 24
rs 6.9666
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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]);
0 ignored issues
show
Bug introduced by
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

214
            /** @scrutinizer ignore-call */ 
215
            $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...
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) {
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
257
            $constructorParameters = $constructor->getParameters();
258
259
            $params = [];
260
            foreach ($constructorParameters as $constructorParameter) {
261
                $paramName = $constructorParameter->name;
262
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
0 ignored issues
show
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

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

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

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

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

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

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