• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.81
/src/Serializer/AbstractItemNormalizer.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Serializer;
15

16
use ApiPlatform\Metadata\ApiProperty;
17
use ApiPlatform\Metadata\CollectionOperationInterface;
18
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
19
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
20
use ApiPlatform\Metadata\IriConverterInterface;
21
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
24
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
25
use ApiPlatform\Metadata\ResourceClassResolverInterface;
26
use ApiPlatform\Metadata\UrlGeneratorInterface;
27
use ApiPlatform\Metadata\Util\ClassInfoTrait;
28
use ApiPlatform\Metadata\Util\CloneTrait;
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\Encoder\CsvEncoder;
34
use Symfony\Component\Serializer\Encoder\XmlEncoder;
35
use Symfony\Component\Serializer\Exception\LogicException;
36
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
37
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
38
use Symfony\Component\Serializer\Exception\RuntimeException;
39
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
40
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
41
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
42
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
43
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
44
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
45
use Symfony\Component\Serializer\Serializer;
46

47
/**
48
 * Base item normalizer.
49
 *
50
 * @author Kévin Dunglas <dunglas@gmail.com>
51
 */
52
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
53
{
54
    use ClassInfoTrait;
55
    use CloneTrait;
56
    use ContextTrait;
57
    use InputOutputMetadataTrait;
58
    use OperationContextTrait;
59

60
    protected PropertyAccessorInterface $propertyAccessor;
61
    protected array $localCache = [];
62
    protected array $localFactoryOptionsCache = [];
63
    protected ?ResourceAccessCheckerInterface $resourceAccessChecker;
64

65
    public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected IriConverterInterface $iriConverter, protected ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null)
66
    {
UNCOV
67
        if (!isset($defaultContext['circular_reference_handler'])) {
957✔
UNCOV
68
            $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
951✔
69
        }
70

UNCOV
71
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable($this->getObjectClass(...)), $defaultContext);
957✔
UNCOV
72
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
957✔
UNCOV
73
        $this->resourceAccessChecker = $resourceAccessChecker;
957✔
UNCOV
74
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
957✔
75
    }
76

77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
81
    {
UNCOV
82
        if (!\is_object($data) || is_iterable($data)) {
839✔
UNCOV
83
            return false;
327✔
84
        }
85

UNCOV
86
        $class = $context['force_resource_class'] ?? $this->getObjectClass($data);
810✔
UNCOV
87
        if (($context['output']['class'] ?? null) === $class) {
810✔
UNCOV
88
            return true;
21✔
89
        }
90

UNCOV
91
        return $this->resourceClassResolver->isResourceClass($class);
802✔
92
    }
93

94
    public function getSupportedTypes(?string $format): array
95
    {
UNCOV
96
        return [
863✔
UNCOV
97
            'object' => true,
863✔
UNCOV
98
        ];
863✔
99
    }
100

101
    /**
102
     * {@inheritdoc}
103
     *
104
     * @throws LogicException
105
     */
106
    public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
107
    {
UNCOV
108
        $resourceClass = $context['force_resource_class'] ?? $this->getObjectClass($object);
813✔
UNCOV
109
        if ($outputClass = $this->getOutputClass($context)) {
813✔
UNCOV
110
            if (!$this->serializer instanceof NormalizerInterface) {
21✔
111
                throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
×
112
            }
113

UNCOV
114
            unset($context['output'], $context['operation'], $context['operation_name']);
21✔
UNCOV
115
            $context['resource_class'] = $outputClass;
21✔
UNCOV
116
            $context['api_sub_level'] = true;
21✔
UNCOV
117
            $context[self::ALLOW_EXTRA_ATTRIBUTES] = false;
21✔
118

UNCOV
119
            return $this->serializer->normalize($object, $format, $context);
21✔
120
        }
121

122
        // Never remove this, with `application/json` we don't use our AbstractCollectionNormalizer and we need
123
        // to remove the collection operation from our context or we'll introduce security issues
UNCOV
124
        if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
813✔
UNCOV
125
            unset($context['operation_name'], $context['operation'], $context['iri']);
8✔
126
        }
127

UNCOV
128
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
813✔
UNCOV
129
            $context = $this->initContext($resourceClass, $context);
793✔
130
        }
131

UNCOV
132
        $context['api_normalize'] = true;
813✔
UNCOV
133
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
813✔
134

135
        /*
136
         * When true, converts the normalized data array of a resource into an
137
         * IRI, if the normalized data array is empty.
138
         *
139
         * This is useful when traversing from a non-resource towards an attribute
140
         * which is a resource, as we do not have the benefit of {@see ApiProperty::isReadableLink}.
141
         *
142
         * It must not be propagated to resources, as {@see ApiProperty::isReadableLink}
143
         * should take effect.
144
         */
UNCOV
145
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
813✔
UNCOV
146
        unset($context['api_empty_resource_as_iri']);
813✔
147

UNCOV
148
        if (!$this->tagCollector && isset($context['resources'])) {
813✔
149
            $context['resources'][$iri] = $iri;
×
150
        }
151

UNCOV
152
        $context['object'] = $object;
813✔
UNCOV
153
        $context['format'] = $format;
813✔
154

UNCOV
155
        $data = parent::normalize($object, $format, $context);
813✔
156

UNCOV
157
        $context['data'] = $data;
813✔
UNCOV
158
        unset($context['property_metadata'], $context['api_attribute']);
813✔
159

UNCOV
160
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
813✔
161
            $context['data'] = $iri;
×
162

163
            if ($this->tagCollector) {
×
164
                $this->tagCollector->collect($context);
×
165
            }
166

167
            return $iri;
×
168
        }
169

UNCOV
170
        if ($this->tagCollector) {
813✔
UNCOV
171
            $this->tagCollector->collect($context);
708✔
172
        }
173

UNCOV
174
        return $data;
813✔
175
    }
176

177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
181
    {
UNCOV
182
        if (($context['input']['class'] ?? null) === $type) {
213✔
183
            return true;
×
184
        }
185

UNCOV
186
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
213✔
187
    }
188

189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function denormalize(mixed $data, string $class, ?string $format = null, array $context = []): mixed
193
    {
UNCOV
194
        $resourceClass = $class;
210✔
195

UNCOV
196
        if ($inputClass = $this->getInputClass($context)) {
210✔
UNCOV
197
            if (!$this->serializer instanceof DenormalizerInterface) {
10✔
198
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
×
199
            }
200

UNCOV
201
            unset($context['input'], $context['operation'], $context['operation_name'], $context['uri_variables']);
10✔
UNCOV
202
            $context['resource_class'] = $inputClass;
10✔
203

204
            try {
UNCOV
205
                return $this->serializer->denormalize($data, $inputClass, $format, $context);
10✔
206
            } catch (NotNormalizableValueException $e) {
1✔
207
                throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
1✔
208
            }
209
        }
210

UNCOV
211
        if (null === $objectToPopulate = $this->extractObjectToPopulate($resourceClass, $context, static::OBJECT_TO_POPULATE)) {
202✔
UNCOV
212
            $normalizedData = \is_scalar($data) ? [$data] : $this->prepareForDenormalization($data);
167✔
UNCOV
213
            $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class, $context);
167✔
214
        }
215

UNCOV
216
        $context['api_denormalize'] = true;
202✔
217

UNCOV
218
        if ($this->resourceClassResolver->isResourceClass($class)) {
202✔
UNCOV
219
            $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
202✔
UNCOV
220
            $context['resource_class'] = $resourceClass;
202✔
221
        }
222

UNCOV
223
        if (\is_string($data)) {
202✔
224
            try {
UNCOV
225
                return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
2✔
226
            } catch (ItemNotFoundException $e) {
×
227
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
228
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
229
                }
230

231
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
232
            } catch (InvalidArgumentException $e) {
×
233
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
234
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
×
235
                }
236

237
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
238
            }
239
        }
240

UNCOV
241
        if (!\is_array($data)) {
200✔
242
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource must be "array" (nested document) or "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null);
1✔
243
        }
244

UNCOV
245
        $previousObject = $this->clone($objectToPopulate);
200✔
UNCOV
246
        $object = parent::denormalize($data, $class, $format, $context);
200✔
247

UNCOV
248
        if (!$this->resourceClassResolver->isResourceClass($class)) {
185✔
249
            return $object;
×
250
        }
251

252
        // Bypass the post-denormalize attribute revert logic if the object could not be
253
        // cloned since we cannot possibly revert any changes made to it.
UNCOV
254
        if (null !== $objectToPopulate && null === $previousObject) {
185✔
255
            return $object;
×
256
        }
257

UNCOV
258
        $options = $this->getFactoryOptions($context);
185✔
UNCOV
259
        $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options));
185✔
260

261
        // Revert attributes that aren't allowed to be changed after a post-denormalize check
UNCOV
262
        foreach (array_keys($data) as $attribute) {
185✔
UNCOV
263
            $attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute;
183✔
UNCOV
264
            if (!\in_array($attribute, $propertyNames, true)) {
183✔
265
                continue;
40✔
266
            }
267

UNCOV
268
            if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
173✔
269
                if (null !== $previousObject) {
1✔
270
                    $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute));
×
271
                } else {
272
                    $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
1✔
273
                    $this->setValue($object, $attribute, $propertyMetadata->getDefault());
1✔
274
                }
275
            }
276
        }
277

UNCOV
278
        return $object;
185✔
279
    }
280

281
    /**
282
     * Method copy-pasted from symfony/serializer.
283
     * Remove it after symfony/serializer version update @see https://github.com/symfony/symfony/pull/28263.
284
     *
285
     * {@inheritdoc}
286
     *
287
     * @internal
288
     */
289
    protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, ?string $format = null): object
290
    {
UNCOV
291
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
200✔
292
            unset($context[static::OBJECT_TO_POPULATE]);
40✔
293

294
            return $object;
40✔
295
        }
296

UNCOV
297
        $class = $this->getClassDiscriminatorResolvedClass($data, $class, $context);
165✔
UNCOV
298
        $reflectionClass = new \ReflectionClass($class);
165✔
299

UNCOV
300
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
165✔
UNCOV
301
        if ($constructor) {
165✔
UNCOV
302
            $constructorParameters = $constructor->getParameters();
74✔
303

UNCOV
304
            $params = [];
74✔
UNCOV
305
            $missingConstructorArguments = [];
74✔
UNCOV
306
            foreach ($constructorParameters as $constructorParameter) {
74✔
UNCOV
307
                $paramName = $constructorParameter->name;
33✔
UNCOV
308
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
33✔
UNCOV
309
                $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
33✔
UNCOV
310
                $attributeContext['deserialization_path'] = $attributeContext['deserialization_path'] ?? $key;
33✔
311

UNCOV
312
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
33✔
UNCOV
313
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
33✔
UNCOV
314
                if ($constructorParameter->isVariadic()) {
33✔
315
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
×
316
                        if (!\is_array($data[$paramName])) {
×
317
                            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));
×
318
                        }
319

320
                        $params[] = $data[$paramName];
×
321
                    }
UNCOV
322
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
33✔
323
                    try {
324
                        $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $attributeContext, $format);
8✔
325
                    } catch (NotNormalizableValueException $exception) {
1✔
326
                        if (!isset($context['not_normalizable_value_exceptions'])) {
1✔
327
                            throw $exception;
1✔
328
                        }
329
                        $context['not_normalizable_value_exceptions'][] = $exception;
×
330
                    }
331

332
                    // Don't run set for a parameter passed to the constructor
333
                    unset($data[$key]);
7✔
UNCOV
334
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
30✔
335
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
×
UNCOV
336
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
30✔
UNCOV
337
                    $params[] = $constructorParameter->getDefaultValue();
30✔
338
                } else {
339
                    if (!isset($context['not_normalizable_value_exceptions'])) {
1✔
340
                        $missingConstructorArguments[] = $constructorParameter->name;
1✔
341
                    }
342

343
                    $constructorParameterType = 'unknown';
1✔
344
                    $reflectionType = $constructorParameter->getType();
1✔
345
                    if ($reflectionType instanceof \ReflectionNamedType) {
1✔
346
                        $constructorParameterType = $reflectionType->getName();
1✔
347
                    }
348

349
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(
1✔
350
                        \sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
1✔
351
                        null,
1✔
352
                        [$constructorParameterType],
1✔
353
                        $attributeContext['deserialization_path'],
1✔
354
                        true
1✔
355
                    );
1✔
356
                    $context['not_normalizable_value_exceptions'][] = $exception;
1✔
357
                }
358
            }
359

UNCOV
360
            if ($missingConstructorArguments) {
73✔
361
                throw new MissingConstructorArgumentsException(\sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments, $class);
1✔
362
            }
363

UNCOV
364
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
73✔
365
                return $reflectionClass->newInstanceWithoutConstructor();
×
366
            }
367

UNCOV
368
            if ($constructor->isConstructor()) {
73✔
UNCOV
369
                return $reflectionClass->newInstanceArgs($params);
73✔
370
            }
371

372
            return $constructor->invokeArgs(null, $params);
×
373
        }
374

UNCOV
375
        return new $class();
94✔
376
    }
377

378
    protected function getClassDiscriminatorResolvedClass(array $data, string $class, array $context = []): string
379
    {
UNCOV
380
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
167✔
UNCOV
381
            return $class;
167✔
382
        }
383

384
        if (!isset($data[$mapping->getTypeProperty()])) {
1✔
385
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty());
×
386
        }
387

388
        $type = $data[$mapping->getTypeProperty()];
1✔
389
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
1✔
390
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
×
391
        }
392

393
        return $mappedClass;
1✔
394
    }
395

396
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array $context, ?string $format = null): mixed
397
    {
398
        return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
8✔
399
    }
400

401
    /**
402
     * {@inheritdoc}
403
     *
404
     * Unused in this context.
405
     *
406
     * @return string[]
407
     */
408
    protected function extractAttributes($object, $format = null, array $context = []): array
409
    {
410
        return [];
×
411
    }
412

413
    /**
414
     * {@inheritdoc}
415
     */
416
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
417
    {
UNCOV
418
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
818✔
UNCOV
419
            return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
21✔
420
        }
421

UNCOV
422
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
799✔
UNCOV
423
        $options = $this->getFactoryOptions($context);
799✔
UNCOV
424
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
799✔
425

UNCOV
426
        $allowedAttributes = [];
799✔
UNCOV
427
        foreach ($propertyNames as $propertyName) {
799✔
UNCOV
428
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
792✔
429

430
            if (
UNCOV
431
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
792✔
UNCOV
432
                && (isset($context['api_normalize']) && $propertyMetadata->isReadable()
792✔
UNCOV
433
                    || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
792✔
434
                )
435
            ) {
UNCOV
436
                $allowedAttributes[] = $propertyName;
785✔
437
            }
438
        }
439

UNCOV
440
        return $allowedAttributes;
799✔
441
    }
442

443
    /**
444
     * {@inheritdoc}
445
     */
446
    protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
447
    {
UNCOV
448
        if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
811✔
UNCOV
449
            return false;
212✔
450
        }
451

UNCOV
452
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
810✔
453
    }
454

455
    /**
456
     * Check if access to the attribute is granted.
457
     */
458
    protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
459
    {
UNCOV
460
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
810✔
UNCOV
461
            return true;
21✔
462
        }
463

UNCOV
464
        $options = $this->getFactoryOptions($context);
791✔
UNCOV
465
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
791✔
UNCOV
466
        $security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy();
791✔
UNCOV
467
        if (null !== $this->resourceAccessChecker && $security) {
791✔
UNCOV
468
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
32✔
UNCOV
469
                'object' => $object,
32✔
UNCOV
470
                'property' => $attribute,
32✔
UNCOV
471
            ]);
32✔
472
        }
473

UNCOV
474
        return true;
787✔
475
    }
476

477
    /**
478
     * Check if access to the attribute is granted.
479
     */
480
    protected function canAccessAttributePostDenormalize(?object $object, ?object $previousObject, string $attribute, array $context = []): bool
481
    {
UNCOV
482
        $options = $this->getFactoryOptions($context);
173✔
UNCOV
483
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
173✔
UNCOV
484
        $security = $propertyMetadata->getSecurityPostDenormalize();
173✔
UNCOV
485
        if ($this->resourceAccessChecker && $security) {
173✔
486
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
4✔
487
                'object' => $object,
4✔
488
                'previous_object' => $previousObject,
4✔
489
                'property' => $attribute,
4✔
490
            ]);
4✔
491
        }
492

UNCOV
493
        return true;
172✔
494
    }
495

496
    /**
497
     * {@inheritdoc}
498
     */
499
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
500
    {
501
        try {
UNCOV
502
            $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
177✔
UNCOV
503
        } catch (NotNormalizableValueException $exception) {
14✔
504
            // Only throw if collecting denormalization errors is disabled.
505
            if (!isset($context['not_normalizable_value_exceptions'])) {
9✔
506
                throw $exception;
9✔
507
            }
508
        }
509
    }
510

511
    /**
512
     * Validates the type of the value. Allows using integers as floats for JSON formats.
513
     *
514
     * @throws NotNormalizableValueException
515
     */
516
    protected function validateType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void
517
    {
UNCOV
518
        $builtinType = $type->getBuiltinType();
156✔
UNCOV
519
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
156✔
520
            $isValid = \is_float($value) || \is_int($value);
1✔
521
        } else {
UNCOV
522
            $isValid = \call_user_func('is_'.$builtinType, $value);
156✔
523
        }
524

UNCOV
525
        if (!$isValid) {
156✔
526
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $builtinType, \gettype($value)), $value, [$builtinType], $context['deserialization_path'] ?? null);
7✔
527
        }
528
    }
529

530
    /**
531
     * Denormalizes a collection of objects.
532
     *
533
     * @throws NotNormalizableValueException
534
     */
535
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
536
    {
537
        if (!\is_array($value)) {
13✔
538
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null);
1✔
539
        }
540

541
        $values = [];
12✔
542
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
12✔
543
        $collectionKeyTypes = $type->getCollectionKeyTypes();
12✔
544
        foreach ($value as $index => $obj) {
12✔
545
            $currentChildContext = $childContext;
12✔
546
            if (isset($childContext['deserialization_path'])) {
12✔
547
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
12✔
548
            }
549

550
            // no typehint provided on collection key
551
            if (!$collectionKeyTypes) {
12✔
552
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
553
                continue;
×
554
            }
555

556
            // validate collection key typehint
557
            foreach ($collectionKeyTypes as $collectionKeyType) {
12✔
558
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
12✔
559
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
12✔
560
                    continue;
1✔
561
                }
562

563
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
11✔
564
                continue 2;
11✔
565
            }
566
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyTypes[0]->getBuiltinType(), \gettype($index)), $index, [$collectionKeyTypes[0]->getBuiltinType()], ($context['deserialization_path'] ?? false) ? \sprintf('key(%s)', $context['deserialization_path']) : null, true);
1✔
567
        }
568

569
        return $values;
11✔
570
    }
571

572
    /**
573
     * Denormalizes a relation.
574
     *
575
     * @throws LogicException
576
     * @throws UnexpectedValueException
577
     * @throws NotNormalizableValueException
578
     */
579
    protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object
580
    {
UNCOV
581
        if (\is_string($value)) {
49✔
582
            try {
UNCOV
583
                return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
28✔
UNCOV
584
            } catch (ItemNotFoundException $e) {
4✔
585
                if (!isset($context['not_normalizable_value_exceptions'])) {
1✔
586
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
1✔
587
                }
588

589
                throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
UNCOV
590
            } catch (InvalidArgumentException $e) {
3✔
591
                if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
592
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
2✔
593
                }
594

595
                throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
596
            }
597
        }
598

599
        if ($propertyMetadata->isWritableLink()) {
22✔
600
            $context['api_allow_update'] = true;
22✔
601

602
            if (!$this->serializer instanceof DenormalizerInterface) {
22✔
603
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
604
            }
605

606
            $item = $this->serializer->denormalize($value, $className, $format, $context);
22✔
607
            if (!\is_object($item) && null !== $item) {
20✔
608
                throw new \UnexpectedValueException('Expected item to be an object or null.');
×
609
            }
610

611
            return $item;
20✔
612
        }
613

614
        if (!\is_array($value)) {
×
615
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array" (nested document) or "string" (IRI), "%s" given.', $attributeName, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
×
616
        }
617

618
        throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
×
619
    }
620

621
    /**
622
     * Gets the options for the property name collection / property metadata factories.
623
     */
624
    protected function getFactoryOptions(array $context): array
625
    {
UNCOV
626
        $options = [];
818✔
UNCOV
627
        if (isset($context[self::GROUPS])) {
818✔
628
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
UNCOV
629
            $options['serializer_groups'] = (array) $context[self::GROUPS];
282✔
630
        }
631

UNCOV
632
        $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['root_operation_name'] ?? '');
818✔
UNCOV
633
        $suffix = ($context['api_normalize'] ?? '') ? 'n' : '';
818✔
UNCOV
634
        if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey.$suffix])) {
818✔
UNCOV
635
            return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix];
810✔
636
        }
637

638
        // This is a hot spot
UNCOV
639
        if (isset($context['resource_class'])) {
818✔
640
            // Note that the groups need to be read on the root operation
UNCOV
641
            if ($operation = ($context['root_operation'] ?? null)) {
818✔
UNCOV
642
                $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null;
350✔
UNCOV
643
                $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null;
350✔
UNCOV
644
                $options['operation_name'] = $operation->getName();
350✔
645
            }
646
        }
647

UNCOV
648
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
818✔
649
    }
650

651
    /**
652
     * {@inheritdoc}
653
     *
654
     * @throws UnexpectedValueException
655
     */
656
    protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
657
    {
UNCOV
658
        $context['api_attribute'] = $attribute;
799✔
UNCOV
659
        $context['property_metadata'] = $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
799✔
660

UNCOV
661
        if ($context['api_denormalize'] ?? false) {
799✔
662
            return $this->propertyAccessor->getValue($object, $attribute);
9✔
663
        }
664

UNCOV
665
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
799✔
666

UNCOV
667
        foreach ($types as $type) {
799✔
668
            if (
UNCOV
669
                $type->isCollection()
785✔
UNCOV
670
                && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
785✔
UNCOV
671
                && ($className = $collectionValueType->getClassName())
785✔
UNCOV
672
                && $this->resourceClassResolver->isResourceClass($className)
785✔
673
            ) {
UNCOV
674
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
203✔
675

676
                // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
677
                // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
UNCOV
678
                if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
203✔
679
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
1✔
680
                        operationName: $itemUriTemplate,
1✔
681
                        forceCollection: true,
1✔
682
                        httpOperation: true
1✔
683
                    );
1✔
684

685
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
1✔
686
                }
687

UNCOV
688
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
202✔
689

UNCOV
690
                if (!is_iterable($attributeValue)) {
202✔
691
                    throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
692
                }
693

UNCOV
694
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
202✔
695

UNCOV
696
                $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
202✔
UNCOV
697
                $context['data'] = $data;
202✔
UNCOV
698
                $context['type'] = $type;
202✔
699

UNCOV
700
                if ($this->tagCollector) {
202✔
UNCOV
701
                    $this->tagCollector->collect($context);
183✔
702
                }
703

UNCOV
704
                return $data;
202✔
705
            }
706

707
            if (
UNCOV
708
                ($className = $type->getClassName())
785✔
UNCOV
709
                && $this->resourceClassResolver->isResourceClass($className)
785✔
710
            ) {
UNCOV
711
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
278✔
UNCOV
712
                unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
278✔
UNCOV
713
                if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
278✔
714
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
1✔
715
                        operationName: $uriTemplate,
1✔
716
                        httpOperation: true
1✔
717
                    );
1✔
718

719
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
1✔
720
                }
721

UNCOV
722
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
277✔
723

UNCOV
724
                if (!\is_object($attributeValue) && null !== $attributeValue) {
275✔
725
                    throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
726
                }
727

UNCOV
728
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
275✔
729

UNCOV
730
                $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
275✔
UNCOV
731
                $context['data'] = $data;
275✔
UNCOV
732
                $context['type'] = $type;
275✔
733

UNCOV
734
                if ($this->tagCollector) {
275✔
UNCOV
735
                    $this->tagCollector->collect($context);
255✔
736
                }
737

UNCOV
738
                return $data;
275✔
739
            }
740

UNCOV
741
            if (!$this->serializer instanceof NormalizerInterface) {
778✔
742
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
743
            }
744

UNCOV
745
            unset(
778✔
UNCOV
746
                $context['resource_class'],
778✔
UNCOV
747
                $context['force_resource_class'],
778✔
UNCOV
748
                $context['uri_variables'],
778✔
UNCOV
749
            );
778✔
750

751
            // Anonymous resources
UNCOV
752
            if ($className) {
778✔
UNCOV
753
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
251✔
UNCOV
754
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
251✔
755

UNCOV
756
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
251✔
757

UNCOV
758
                return $this->serializer->normalize($attributeValue, $format, $childContext);
248✔
759
            }
760

UNCOV
761
            if ('array' === $type->getBuiltinType()) {
771✔
UNCOV
762
                if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
194✔
UNCOV
763
                    $context = $this->createOperationContext($context, $className);
5✔
764
                }
765

UNCOV
766
                $childContext = $this->createChildContext($context, $attribute, $format);
194✔
UNCOV
767
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
194✔
768

UNCOV
769
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
194✔
770

UNCOV
771
                return $this->serializer->normalize($attributeValue, $format, $childContext);
194✔
772
            }
773
        }
774

UNCOV
775
        if (!$this->serializer instanceof NormalizerInterface) {
784✔
776
            throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
777
        }
778

UNCOV
779
        unset(
784✔
UNCOV
780
            $context['resource_class'],
784✔
UNCOV
781
            $context['force_resource_class'],
784✔
UNCOV
782
            $context['uri_variables']
784✔
UNCOV
783
        );
784✔
784

UNCOV
785
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
784✔
786

UNCOV
787
        return $this->serializer->normalize($attributeValue, $format, $context);
783✔
788
    }
789

790
    /**
791
     * Normalizes a collection of relations (to-many).
792
     *
793
     * @throws UnexpectedValueException
794
     */
795
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
796
    {
UNCOV
797
        $value = [];
183✔
UNCOV
798
        foreach ($attributeValue as $index => $obj) {
183✔
UNCOV
799
            if (!\is_object($obj) && null !== $obj) {
45✔
800
                throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
×
801
            }
802

803
            // update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources)
UNCOV
804
            $objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass);
45✔
UNCOV
805
            $context['resource_class'] = $objResourceClass;
45✔
UNCOV
806
            if ($this->resourceMetadataCollectionFactory) {
45✔
UNCOV
807
                $context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation();
45✔
808
            }
809

UNCOV
810
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
45✔
811
        }
812

UNCOV
813
        return $value;
183✔
814
    }
815

816
    /**
817
     * Normalizes a relation.
818
     *
819
     * @throws LogicException
820
     * @throws UnexpectedValueException
821
     */
822
    protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $relatedObject, string $resourceClass, ?string $format, array $context): \ArrayObject|array|string|null
823
    {
UNCOV
824
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
239✔
UNCOV
825
            if (!$this->serializer instanceof NormalizerInterface) {
194✔
826
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
827
            }
828

UNCOV
829
            $relatedContext = $this->createOperationContext($context, $resourceClass);
194✔
UNCOV
830
            $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $relatedContext);
194✔
UNCOV
831
            if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
194✔
832
                throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
×
833
            }
834

UNCOV
835
            return $normalizedRelatedObject;
194✔
836
        }
837

UNCOV
838
        $context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
72✔
UNCOV
839
        $context['data'] = $iri;
72✔
UNCOV
840
        $context['object'] = $relatedObject;
72✔
UNCOV
841
        unset($context['property_metadata'], $context['api_attribute']);
72✔
842

UNCOV
843
        if ($this->tagCollector) {
72✔
UNCOV
844
            $this->tagCollector->collect($context);
70✔
UNCOV
845
        } elseif (isset($context['resources'])) {
2✔
846
            $context['resources'][$iri] = $iri;
×
847
        }
848

UNCOV
849
        $push = $propertyMetadata->getPush() ?? false;
72✔
UNCOV
850
        if (isset($context['resources_to_push']) && $push) {
72✔
851
            $context['resources_to_push'][$iri] = $iri;
×
852
        }
853

UNCOV
854
        return $iri;
72✔
855
    }
856

857
    private function createAttributeValue(string $attribute, mixed $value, ?string $format = null, array &$context = []): mixed
858
    {
859
        try {
UNCOV
860
            return $this->createAndValidateAttributeValue($attribute, $value, $format, $context);
177✔
UNCOV
861
        } catch (NotNormalizableValueException $exception) {
14✔
862
            if (!isset($context['not_normalizable_value_exceptions'])) {
9✔
863
                throw $exception;
9✔
864
            }
865
            $context['not_normalizable_value_exceptions'][] = $exception;
×
866

867
            throw $exception;
×
868
        }
869
    }
870

871
    private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed
872
    {
UNCOV
873
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
185✔
UNCOV
874
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
185✔
UNCOV
875
        $isMultipleTypes = \count($types) > 1;
185✔
UNCOV
876
        $denormalizationException = null;
185✔
877

UNCOV
878
        foreach ($types as $type) {
185✔
UNCOV
879
            if (null === $value && ($type->isNullable() || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
183✔
880
                return $value;
2✔
881
            }
882

UNCOV
883
            $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
183✔
884

885
            /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
886
            // Fix a collection that contains the only one element
887
            // This is special to xml format only
UNCOV
888
            if ('xml' === $format && null !== $collectionValueType && (!\is_array($value) || !\is_int(key($value)))) {
183✔
889
                $value = [$value];
1✔
890
            }
891

892
            if (
UNCOV
893
                $type->isCollection()
183✔
UNCOV
894
                && null !== $collectionValueType
183✔
UNCOV
895
                && null !== ($className = $collectionValueType->getClassName())
183✔
UNCOV
896
                && $this->resourceClassResolver->isResourceClass($className)
183✔
897
            ) {
898
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
13✔
899
                $context['resource_class'] = $resourceClass;
13✔
900
                unset($context['uri_variables']);
13✔
901

902
                try {
903
                    return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
13✔
904
                } catch (NotNormalizableValueException $e) {
2✔
905
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
906
                    if ($isMultipleTypes) {
2✔
907
                        $denormalizationException ??= $e;
×
908

909
                        continue;
×
910
                    }
911

912
                    throw $e;
2✔
913
                }
914
            }
915

916
            if (
UNCOV
917
                null !== ($className = $type->getClassName())
180✔
UNCOV
918
                && $this->resourceClassResolver->isResourceClass($className)
180✔
919
            ) {
UNCOV
920
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
49✔
UNCOV
921
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
49✔
922

923
                try {
UNCOV
924
                    return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
49✔
UNCOV
925
                } catch (NotNormalizableValueException $e) {
6✔
926
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
927
                    if ($isMultipleTypes) {
1✔
928
                        $denormalizationException ??= $e;
×
929

930
                        continue;
×
931
                    }
932

933
                    throw $e;
1✔
934
                }
935
            }
936

937
            if (
UNCOV
938
                $type->isCollection()
164✔
UNCOV
939
                && null !== $collectionValueType
164✔
UNCOV
940
                && null !== ($className = $collectionValueType->getClassName())
164✔
UNCOV
941
                && \is_array($value)
164✔
942
            ) {
943
                if (!$this->serializer instanceof DenormalizerInterface) {
3✔
944
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
945
                }
946

947
                unset($context['resource_class'], $context['uri_variables']);
3✔
948

949
                try {
950
                    return $this->serializer->denormalize($value, $className.'[]', $format, $context);
3✔
951
                } catch (NotNormalizableValueException $e) {
×
952
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
953
                    if ($isMultipleTypes) {
×
954
                        $denormalizationException ??= $e;
×
955

956
                        continue;
×
957
                    }
958

959
                    throw $e;
×
960
                }
961
            }
962

UNCOV
963
            if (null !== $className = $type->getClassName()) {
163✔
964
                if (!$this->serializer instanceof DenormalizerInterface) {
13✔
965
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
966
                }
967

968
                unset($context['resource_class'], $context['uri_variables']);
13✔
969

970
                try {
971
                    return $this->serializer->denormalize($value, $className, $format, $context);
13✔
972
                } catch (NotNormalizableValueException $e) {
3✔
973
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
974
                    if ($isMultipleTypes) {
3✔
975
                        $denormalizationException ??= $e;
1✔
976

977
                        continue;
1✔
978
                    }
979

980
                    throw $e;
2✔
981
                }
982
            }
983

984
            /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
985
            // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
986
            // if a value is meant to be a string, float, int or a boolean value from the serialized representation.
987
            // That's why we have to transform the values, if one of these non-string basic datatypes is expected.
UNCOV
988
            if (\is_string($value) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
160✔
989
                if ('' === $value && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) {
15✔
990
                    return null;
×
991
                }
992

993
                switch ($type->getBuiltinType()) {
15✔
UNCOV
994
                    case Type::BUILTIN_TYPE_BOOL:
15✔
995
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
996
                        if ('false' === $value || '0' === $value) {
4✔
997
                            $value = false;
2✔
998
                        } elseif ('true' === $value || '1' === $value) {
2✔
999
                            $value = true;
2✔
1000
                        } else {
1001
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1002
                            if ($isMultipleTypes) {
×
1003
                                break 2;
×
1004
                            }
1005
                            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null);
×
1006
                        }
1007
                        break;
4✔
UNCOV
1008
                    case Type::BUILTIN_TYPE_INT:
11✔
1009
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
4✔
1010
                            $value = (int) $value;
4✔
1011
                        } else {
1012
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1013
                            if ($isMultipleTypes) {
×
1014
                                break 2;
×
1015
                            }
1016
                            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
×
1017
                        }
1018
                        break;
4✔
UNCOV
1019
                    case Type::BUILTIN_TYPE_FLOAT:
7✔
1020
                        if (is_numeric($value)) {
4✔
1021
                            return (float) $value;
1✔
1022
                        }
1023

1024
                        switch ($value) {
1025
                            case 'NaN':
3✔
1026
                                return \NAN;
1✔
1027
                            case 'INF':
2✔
1028
                                return \INF;
1✔
1029
                            case '-INF':
1✔
1030
                                return -\INF;
1✔
1031
                            default:
1032
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1033
                                if ($isMultipleTypes) {
×
1034
                                    break 3;
×
1035
                                }
1036
                                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null);
×
1037
                        }
1038
                }
1039
            }
1040

UNCOV
1041
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
156✔
1042
                return $value;
×
1043
            }
1044

1045
            try {
UNCOV
1046
                $this->validateType($attribute, $type, $value, $format, $context);
156✔
1047

UNCOV
1048
                break;
152✔
1049
            } catch (NotNormalizableValueException $e) {
7✔
1050
                // union/intersect types: try the next type
1051
                if (!$isMultipleTypes) {
7✔
1052
                    throw $e;
4✔
1053
                }
1054
            }
1055
        }
1056

UNCOV
1057
        if ($denormalizationException) {
154✔
1058
            throw $denormalizationException;
1✔
1059
        }
1060

UNCOV
1061
        return $value;
154✔
1062
    }
1063

1064
    /**
1065
     * Sets a value of the object using the PropertyAccess component.
1066
     */
1067
    private function setValue(object $object, string $attributeName, mixed $value): void
1068
    {
1069
        try {
UNCOV
1070
            $this->propertyAccessor->setValue($object, $attributeName, $value);
168✔
1071
        } catch (NoSuchPropertyException) {
8✔
1072
            // Properties not found are ignored
1073
        }
1074
    }
1075
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc