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

api-platform / core / 15250451308

26 May 2025 09:29AM UTC coverage: 26.388% (+0.008%) from 26.38%
15250451308

push

github

soyuka
fix(openapi): `example` and `default` with nullable value not being shown

0 of 3 new or added lines in 2 files covered. (0.0%)

11122 existing lines in 371 files now uncovered.

13009 of 49299 relevant lines covered (26.39%)

44.73 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

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

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

64
    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)
65
    {
UNCOV
66
        if (!isset($defaultContext['circular_reference_handler'])) {
1,208✔
UNCOV
67
            $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
1,196✔
68
        }
69

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

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

UNCOV
85
        $class = $context['force_resource_class'] ?? $this->getObjectClass($data);
1,019✔
UNCOV
86
        if (($context['output']['class'] ?? null) === $class) {
1,019✔
UNCOV
87
            return true;
27✔
88
        }
89

UNCOV
90
        return $this->resourceClassResolver->isResourceClass($class);
1,009✔
91
    }
92

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

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

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

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

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

UNCOV
127
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
1,026✔
UNCOV
128
            $context = $this->initContext($resourceClass, $context);
1,000✔
129
        }
130

UNCOV
131
        $context['api_normalize'] = true;
1,026✔
UNCOV
132
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
1,026✔
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 ApiProperty::isReadableLink}.
140
         *
141
         * It must not be propagated to resources, as {@see ApiProperty::isReadableLink}
142
         * should take effect.
143
         */
UNCOV
144
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
1,026✔
UNCOV
145
        unset($context['api_empty_resource_as_iri']);
1,026✔
146

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

UNCOV
151
        $context['object'] = $object;
1,026✔
UNCOV
152
        $context['format'] = $format;
1,026✔
153

UNCOV
154
        $data = parent::normalize($object, $format, $context);
1,026✔
155

UNCOV
156
        $context['data'] = $data;
1,026✔
UNCOV
157
        unset($context['property_metadata'], $context['api_attribute']);
1,026✔
158

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

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

166
            return $iri;
×
167
        }
168

UNCOV
169
        if ($this->tagCollector) {
1,026✔
UNCOV
170
            $this->tagCollector->collect($context);
906✔
171
        }
172

UNCOV
173
        return $data;
1,026✔
174
    }
175

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

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

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

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

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

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

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

UNCOV
215
        $context['api_denormalize'] = true;
209✔
216

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

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

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

236
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $data), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
237
            }
238
        }
239

UNCOV
240
        if (!\is_array($data)) {
206✔
241
            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✔
242
        }
243

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

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

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

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

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

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

UNCOV
277
        return $object;
190✔
278
    }
279

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

293
            return $object;
40✔
294
        }
295

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

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

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

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

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

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

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

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

UNCOV
359
            if ($missingConstructorArguments) {
78✔
360
                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✔
361
            }
362

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

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

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

UNCOV
374
        return new $class();
95✔
375
    }
376

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

383
        if (!isset($data[$mapping->getTypeProperty()])) {
1✔
384
            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());
×
385
        }
386

387
        $type = $data[$mapping->getTypeProperty()];
1✔
388
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
1✔
389
            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);
×
390
        }
391

392
        return $mappedClass;
1✔
393
    }
394

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

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

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

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

UNCOV
425
        $allowedAttributes = [];
1,006✔
UNCOV
426
        foreach ($propertyNames as $propertyName) {
1,006✔
UNCOV
427
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
995✔
428

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

UNCOV
439
        return $allowedAttributes;
1,006✔
440
    }
441

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

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

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

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

UNCOV
473
        return true;
990✔
474
    }
475

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

UNCOV
492
        return true;
176✔
493
    }
494

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

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

UNCOV
524
        if (!$isValid) {
159✔
525
            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✔
526
        }
527
    }
528

529
    /**
530
     * Denormalizes a collection of objects.
531
     *
532
     * @throws NotNormalizableValueException
533
     */
534
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
535
    {
536
        if (!\is_array($value)) {
13✔
537
            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✔
538
        }
539

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

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

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

562
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
11✔
563
                continue 2;
11✔
564
            }
565
            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✔
566
        }
567

568
        return $values;
11✔
569
    }
570

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

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

594
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $value), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
595
            }
596
        }
597

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

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

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

610
            return $item;
20✔
611
        }
612

613
        if (!\is_array($value)) {
×
614
            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);
×
615
        }
616

617
        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);
×
618
    }
619

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

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

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

UNCOV
647
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
1,031✔
648
    }
649

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

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

UNCOV
664
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
1,005✔
665

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

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

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

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

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

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

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

UNCOV
699
                if ($this->tagCollector) {
209✔
UNCOV
700
                    $this->tagCollector->collect($context);
190✔
701
                }
702

UNCOV
703
                return $data;
209✔
704
            }
705

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

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

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

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

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

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

UNCOV
733
                if ($this->tagCollector) {
283✔
UNCOV
734
                    $this->tagCollector->collect($context);
259✔
735
                }
736

UNCOV
737
                return $data;
283✔
738
            }
739

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
812
        return $value;
190✔
813
    }
814

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

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

UNCOV
834
            return $normalizedRelatedObject;
199✔
835
        }
836

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

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

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

UNCOV
853
        return $iri;
76✔
854
    }
855

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

866
            throw $exception;
×
867
        }
868
    }
869

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

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

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

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

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

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

908
                        continue;
×
909
                    }
910

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

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

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

929
                        continue;
×
930
                    }
931

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

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

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

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

955
                        continue;
×
956
                    }
957

958
                    throw $e;
×
959
                }
960
            }
961

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

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

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

976
                        continue;
1✔
977
                    }
978

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

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

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

1023
                        switch ($value) {
1024
                            case 'NaN':
3✔
1025
                                return \NAN;
1✔
1026
                            case 'INF':
2✔
1027
                                return \INF;
1✔
1028
                            case '-INF':
1✔
1029
                                return -\INF;
1✔
1030
                            default:
1031
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1032
                                if ($isMultipleTypes) {
×
1033
                                    break 3;
×
1034
                                }
1035
                                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);
×
1036
                        }
1037
                }
1038
            }
1039

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

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

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

UNCOV
1056
        if ($denormalizationException) {
157✔
1057
            throw $denormalizationException;
1✔
1058
        }
1059

UNCOV
1060
        return $value;
157✔
1061
    }
1062

1063
    /**
1064
     * Sets a value of the object using the PropertyAccess component.
1065
     */
1066
    private function setValue(object $object, string $attributeName, mixed $value): void
1067
    {
1068
        try {
UNCOV
1069
            $this->propertyAccessor->setValue($object, $attributeName, $value);
172✔
1070
        } catch (NoSuchPropertyException) {
8✔
1071
            // Properties not found are ignored
1072
        }
1073
    }
1074
}
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