Completed
Push — master ( 2430ab...b72a90 )
by Han Hui
30s queued 15s
created

getClassDiscriminatorResolvedClass()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 4
nop 2
dl 0
loc 16
rs 9.6111
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\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
Bug introduced by
The method setCircularReferenceHandler() does not exist on ApiPlatform\Core\Serializer\AbstractItemNormalizer. ( Ignorable by Annotation )

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

72
            $this->/** @scrutinizer ignore-call */ 
73
                   setCircularReferenceHandler($defaultContext['circular_reference_handler']);

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...
73
        }
74
75
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable([$this, 'getObjectClass']), $defaultContext);
76
77
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
78
        $this->propertyMetadataFactory = $propertyMetadataFactory;
79
        $this->iriConverter = $iriConverter;
80
        $this->resourceClassResolver = $resourceClassResolver;
81
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
82
        $this->itemDataProvider = $itemDataProvider;
83
        $this->allowPlainIdentifiers = $allowPlainIdentifiers;
84
        $this->dataTransformers = $dataTransformers;
85
        $this->resourceMetadataFactory = $resourceMetadataFactory;
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function supportsNormalization($data, $format = null)
92
    {
93
        if (!\is_object($data) || $data instanceof \Traversable) {
94
            return false;
95
        }
96
97
        return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function hasCacheableSupportsMethod(): bool
104
    {
105
        return true;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     *
111
     * @throws LogicException
112
     */
113
    public function normalize($object, $format = null, array $context = [])
114
    {
115
        if ($object !== $transformed = $this->transformOutput($object, $context)) {
116
            if (!$this->serializer instanceof NormalizerInterface) {
117
                throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
118
            }
119
120
            $context['api_normalize'] = true;
121
            $context['api_resource'] = $object;
122
            unset($context['output']);
123
            unset($context['resource_class']);
124
125
            return $this->serializer->normalize($transformed, $format, $context);
126
        }
127
128
        $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null);
129
        $context = $this->initContext($resourceClass, $context);
130
        $iri = $context['iri'] ?? $this->iriConverter->getIriFromItem($object);
131
        $context['iri'] = $iri;
132
        $context['api_normalize'] = true;
133
134
        /*
135
         * When true, converts the normalized data array of a resource into an
136
         * IRI, if the normalized data array is empty.
137
         *
138
         * This is useful when traversing from a non-resource towards an attribute
139
         * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}.
140
         *
141
         * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink}
142
         * should take effect.
143
         */
144
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
145
        unset($context['api_empty_resource_as_iri']);
146
147
        if (isset($context['resources'])) {
148
            $context['resources'][$iri] = $iri;
149
        }
150
151
        $data = parent::normalize($object, $format, $context);
152
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
153
            return $iri;
154
        }
155
156
        return $data;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function supportsDenormalization($data, $type, $format = null)
163
    {
164
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public function denormalize($data, $class, $format = null, array $context = [])
171
    {
172
        if (null === $objectToPopulate = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
173
            $normalizedData = $this->prepareForDenormalization($data);
174
            $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class);
175
        }
176
        $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
177
        $context['api_denormalize'] = true;
178
        $context['resource_class'] = $resourceClass;
179
180
        if (null !== ($inputClass = $this->getInputClass($resourceClass, $context)) && null !== ($dataTransformer = $this->getDataTransformer($data, $resourceClass, $context))) {
181
            $dataTransformerContext = $context;
182
183
            unset($context['input']);
184
            unset($context['resource_class']);
185
186
            if (!$this->serializer instanceof DenormalizerInterface) {
187
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
188
            }
189
            $denormalizedInput = $this->serializer->denormalize($data, $inputClass, $format, $context);
190
            if (!\is_object($denormalizedInput)) {
191
                throw new \UnexpectedValueException('Expected denormalized input to be an object.');
192
            }
193
194
            return $dataTransformer->transform($denormalizedInput, $resourceClass, $dataTransformerContext);
195
        }
196
197
        $supportsPlainIdentifiers = $this->supportsPlainIdentifiers();
198
199
        if (\is_string($data)) {
200
            try {
201
                return $this->iriConverter->getItemFromIri($data, $context + ['fetch_data' => true]);
202
            } catch (ItemNotFoundException $e) {
203
                if (!$supportsPlainIdentifiers) {
204
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
205
                }
206
            } catch (InvalidArgumentException $e) {
207
                if (!$supportsPlainIdentifiers) {
208
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
209
                }
210
            }
211
        }
212
213
        if (!\is_array($data)) {
214
            if (!$supportsPlainIdentifiers) {
215
                throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.', $resourceClass, \gettype($data)));
216
            }
217
218
            $item = $this->itemDataProvider->getItem($resourceClass, $data, null, $context + ['fetch_data' => true]);
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

218
            /** @scrutinizer ignore-call */ 
219
            $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...
219
            if (null === $item) {
220
                throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".', $resourceClass, $data));
221
            }
222
223
            return $item;
224
        }
225
226
        return parent::denormalize($data, $resourceClass, $format, $context);
227
    }
228
229
    /**
230
     * Originally from {@see https://github.com/symfony/symfony/pull/28263}. Refactor after it is merged.
231
     *
232
     * {@inheritdoc}
233
     *
234
     * @internal
235
     */
236
    protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
237
    {
238
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
239
            unset($context[static::OBJECT_TO_POPULATE]);
240
241
            return $object;
242
        }
243
244
        $class = $this->getClassDiscriminatorResolvedClass($data, $class);
245
        $reflectionClass = new \ReflectionClass($class);
246
247
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
248
        if ($constructor) {
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
249
            $constructorParameters = $constructor->getParameters();
250
251
            $params = [];
252
            foreach ($constructorParameters as $constructorParameter) {
253
                $paramName = $constructorParameter->name;
254
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
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

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

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

497
    protected function createRelationSerializationContext(/** @scrutinizer ignore-unused */ string $resourceClass, array $context): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
498
    {
499
        @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED);
500
501
        return $context;
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     *
507
     * @throws UnexpectedValueException
508
     * @throws LogicException
509
     */
510
    protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
511
    {
512
        $context['api_attribute'] = $attribute;
513
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
514
515
        // BC to be removed in 3.0
516
        try {
517
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
518
        } catch (NoSuchPropertyException $e) {
519
            if (!$propertyMetadata->hasChildInherited()) {
0 ignored issues
show
Deprecated Code introduced by
The function ApiPlatform\Core\Metadat...ta::hasChildInherited() has been deprecated: since 2.6, to be removed in 3.0 ( Ignorable by Annotation )

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

519
            if (!/** @scrutinizer ignore-deprecated */ $propertyMetadata->hasChildInherited()) {

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