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

api-platform / core / 14635100171

24 Apr 2025 06:39AM UTC coverage: 8.271% (+0.02%) from 8.252%
14635100171

Pull #6904

github

web-flow
Merge c9cefd82e into a3e5e53ea
Pull Request #6904: feat(graphql): added support for graphql subscriptions to work for actions

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

1999 existing lines in 144 files now uncovered.

13129 of 158728 relevant lines covered (8.27%)

13.6 hits per line

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

82.38
/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
    {
67
        if (!isset($defaultContext['circular_reference_handler'])) {
1,174✔
68
            $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
1,162✔
69
        }
70

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

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

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

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

94
    public function getSupportedTypes(?string $format): array
95
    {
96
        return [
1,052✔
97
            'object' => true,
1,052✔
98
        ];
1,052✔
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
    {
108
        $resourceClass = $context['force_resource_class'] ?? $this->getObjectClass($object);
993✔
109
        if ($outputClass = $this->getOutputClass($context)) {
993✔
110
            if (!$this->serializer instanceof NormalizerInterface) {
27✔
111
                throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
×
112
            }
113

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

119
            return $this->serializer->normalize($object, $format, $context);
27✔
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
124
        if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
993✔
125
            unset($context['operation_name'], $context['operation'], $context['iri']);
10✔
126
        }
127

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

132
        $context['api_normalize'] = true;
993✔
133
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
993✔
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
         */
145
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
993✔
146
        unset($context['api_empty_resource_as_iri']);
993✔
147

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

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

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

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

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

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

167
            return $iri;
×
168
        }
169

170
        if ($this->tagCollector) {
993✔
171
            $this->tagCollector->collect($context);
873✔
172
        }
173

174
        return $data;
993✔
175
    }
176

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

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

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

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

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

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

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

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

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

223
        if (\is_string($data)) {
205✔
224
            try {
225
                return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
3✔
226
            } catch (ItemNotFoundException $e) {
×
227
                throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
228
            } catch (InvalidArgumentException $e) {
×
229
                throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
×
230
            }
231
        }
232

233
        if (!\is_array($data)) {
202✔
UNCOV
234
            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✔
235
        }
236

237
        $previousObject = $this->clone($objectToPopulate);
202✔
238
        $object = parent::denormalize($data, $class, $format, $context);
202✔
239

240
        if (!$this->resourceClassResolver->isResourceClass($class)) {
188✔
241
            return $object;
×
242
        }
243

244
        // Bypass the post-denormalize attribute revert logic if the object could not be
245
        // cloned since we cannot possibly revert any changes made to it.
246
        if (null !== $objectToPopulate && null === $previousObject) {
188✔
247
            return $object;
×
248
        }
249

250
        $options = $this->getFactoryOptions($context);
188✔
251
        $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options));
188✔
252

253
        // Revert attributes that aren't allowed to be changed after a post-denormalize check
254
        foreach (array_keys($data) as $attribute) {
188✔
255
            $attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute;
185✔
256
            if (!\in_array($attribute, $propertyNames, true)) {
185✔
UNCOV
257
                continue;
40✔
258
            }
259

260
            if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
175✔
UNCOV
261
                if (null !== $previousObject) {
1✔
262
                    $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute));
×
263
                } else {
UNCOV
264
                    $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
1✔
UNCOV
265
                    $this->setValue($object, $attribute, $propertyMetadata->getDefault());
1✔
266
                }
267
            }
268
        }
269

270
        return $object;
188✔
271
    }
272

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

UNCOV
286
            return $object;
40✔
287
        }
288

289
        $class = $this->getClassDiscriminatorResolvedClass($data, $class, $context);
167✔
290
        $reflectionClass = new \ReflectionClass($class);
167✔
291

292
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
167✔
293
        if ($constructor) {
167✔
294
            $constructorParameters = $constructor->getParameters();
75✔
295

296
            $params = [];
75✔
297
            $missingConstructorArguments = [];
75✔
298
            foreach ($constructorParameters as $constructorParameter) {
75✔
299
                $paramName = $constructorParameter->name;
36✔
300
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
36✔
301
                $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
36✔
302
                $attributeContext['deserialization_path'] = $attributeContext['deserialization_path'] ?? $key;
36✔
303

304
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
36✔
305
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
36✔
306
                if ($constructorParameter->isVariadic()) {
36✔
307
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
×
308
                        if (!\is_array($data[$paramName])) {
×
309
                            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));
×
310
                        }
311

312
                        $params[] = $data[$paramName];
×
313
                    }
314
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
36✔
315
                    try {
UNCOV
316
                        $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $attributeContext, $format);
8✔
UNCOV
317
                    } catch (NotNormalizableValueException $exception) {
1✔
UNCOV
318
                        if (!isset($context['not_normalizable_value_exceptions'])) {
1✔
UNCOV
319
                            throw $exception;
1✔
320
                        }
321
                        $context['not_normalizable_value_exceptions'][] = $exception;
×
322
                    }
323

324
                    // Don't run set for a parameter passed to the constructor
UNCOV
325
                    unset($data[$key]);
7✔
326
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
33✔
327
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
×
328
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
33✔
329
                    $params[] = $constructorParameter->getDefaultValue();
33✔
330
                } else {
UNCOV
331
                    if (!isset($context['not_normalizable_value_exceptions'])) {
1✔
UNCOV
332
                        $missingConstructorArguments[] = $constructorParameter->name;
1✔
333
                    }
334

UNCOV
335
                    $constructorParameterType = 'unknown';
1✔
UNCOV
336
                    $reflectionType = $constructorParameter->getType();
1✔
UNCOV
337
                    if ($reflectionType instanceof \ReflectionNamedType) {
1✔
UNCOV
338
                        $constructorParameterType = $reflectionType->getName();
1✔
339
                    }
340

UNCOV
341
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(
1✔
UNCOV
342
                        \sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
1✔
UNCOV
343
                        null,
1✔
UNCOV
344
                        [$constructorParameterType],
1✔
UNCOV
345
                        $attributeContext['deserialization_path'],
1✔
UNCOV
346
                        true
1✔
UNCOV
347
                    );
1✔
UNCOV
348
                    $context['not_normalizable_value_exceptions'][] = $exception;
1✔
349
                }
350
            }
351

352
            if ($missingConstructorArguments) {
74✔
UNCOV
353
                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✔
354
            }
355

356
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
74✔
357
                return $reflectionClass->newInstanceWithoutConstructor();
×
358
            }
359

360
            if ($constructor->isConstructor()) {
74✔
361
                return $reflectionClass->newInstanceArgs($params);
74✔
362
            }
363

364
            return $constructor->invokeArgs(null, $params);
×
365
        }
366

367
        return new $class();
95✔
368
    }
369

370
    protected function getClassDiscriminatorResolvedClass(array $data, string $class, array $context = []): string
371
    {
372
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
170✔
373
            return $class;
170✔
374
        }
375

UNCOV
376
        if (!isset($data[$mapping->getTypeProperty()])) {
1✔
377
            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());
×
378
        }
379

UNCOV
380
        $type = $data[$mapping->getTypeProperty()];
1✔
UNCOV
381
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
1✔
382
            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);
×
383
        }
384

UNCOV
385
        return $mappedClass;
1✔
386
    }
387

388
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array $context, ?string $format = null): mixed
389
    {
UNCOV
390
        return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
8✔
391
    }
392

393
    /**
394
     * {@inheritdoc}
395
     *
396
     * Unused in this context.
397
     *
398
     * @return string[]
399
     */
400
    protected function extractAttributes($object, $format = null, array $context = []): array
401
    {
402
        return [];
×
403
    }
404

405
    /**
406
     * {@inheritdoc}
407
     */
408
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
409
    {
410
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
998✔
411
            return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
27✔
412
        }
413

414
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
973✔
415
        $options = $this->getFactoryOptions($context);
973✔
416
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
973✔
417

418
        $allowedAttributes = [];
973✔
419
        foreach ($propertyNames as $propertyName) {
973✔
420
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
962✔
421

422
            if (
423
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
962✔
424
                && (isset($context['api_normalize']) && $propertyMetadata->isReadable()
962✔
425
                    || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
962✔
426
                )
427
            ) {
428
                $allowedAttributes[] = $propertyName;
952✔
429
            }
430
        }
431

432
        return $allowedAttributes;
973✔
433
    }
434

435
    /**
436
     * {@inheritdoc}
437
     */
438
    protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
439
    {
440
        if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
987✔
441
            return false;
220✔
442
        }
443

444
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
986✔
445
    }
446

447
    /**
448
     * Check if access to the attribute is granted.
449
     */
450
    protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
451
    {
452
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
986✔
453
            return true;
27✔
454
        }
455

456
        $options = $this->getFactoryOptions($context);
961✔
457
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
961✔
458
        $security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy();
961✔
459
        if (null !== $this->resourceAccessChecker && $security) {
961✔
460
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
33✔
461
                'object' => $object,
33✔
462
                'property' => $attribute,
33✔
463
            ]);
33✔
464
        }
465

466
        return true;
957✔
467
    }
468

469
    /**
470
     * Check if access to the attribute is granted.
471
     */
472
    protected function canAccessAttributePostDenormalize(?object $object, ?object $previousObject, string $attribute, array $context = []): bool
473
    {
474
        $options = $this->getFactoryOptions($context);
175✔
475
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
175✔
476
        $security = $propertyMetadata->getSecurityPostDenormalize();
175✔
477
        if ($this->resourceAccessChecker && $security) {
175✔
UNCOV
478
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
4✔
UNCOV
479
                'object' => $object,
4✔
UNCOV
480
                'previous_object' => $previousObject,
4✔
UNCOV
481
                'property' => $attribute,
4✔
UNCOV
482
            ]);
4✔
483
        }
484

485
        return true;
174✔
486
    }
487

488
    /**
489
     * {@inheritdoc}
490
     */
491
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
492
    {
493
        try {
494
            $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
178✔
UNCOV
495
        } catch (NotNormalizableValueException $exception) {
13✔
496
            // Only throw if collecting denormalization errors is disabled.
UNCOV
497
            if (!isset($context['not_normalizable_value_exceptions'])) {
9✔
UNCOV
498
                throw $exception;
9✔
499
            }
500
        }
501
    }
502

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

517
        if (!$isValid) {
159✔
UNCOV
518
            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);
5✔
519
        }
520
    }
521

522
    /**
523
     * Denormalizes a collection of objects.
524
     *
525
     * @throws NotNormalizableValueException
526
     */
527
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
528
    {
UNCOV
529
        if (!\is_array($value)) {
13✔
UNCOV
530
            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✔
531
        }
532

UNCOV
533
        $values = [];
12✔
UNCOV
534
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
12✔
UNCOV
535
        $collectionKeyTypes = $type->getCollectionKeyTypes();
12✔
UNCOV
536
        foreach ($value as $index => $obj) {
12✔
UNCOV
537
            $currentChildContext = $childContext;
12✔
UNCOV
538
            if (isset($childContext['deserialization_path'])) {
12✔
UNCOV
539
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
12✔
540
            }
541

542
            // no typehint provided on collection key
UNCOV
543
            if (!$collectionKeyTypes) {
12✔
544
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
545
                continue;
×
546
            }
547

548
            // validate collection key typehint
UNCOV
549
            foreach ($collectionKeyTypes as $collectionKeyType) {
12✔
UNCOV
550
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
12✔
UNCOV
551
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
12✔
UNCOV
552
                    continue;
1✔
553
                }
554

UNCOV
555
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
11✔
UNCOV
556
                continue 2;
11✔
557
            }
UNCOV
558
            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✔
559
        }
560

UNCOV
561
        return $values;
11✔
562
    }
563

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

590
                return null;
×
UNCOV
591
            } catch (InvalidArgumentException $e) {
2✔
UNCOV
592
                if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
UNCOV
593
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
2✔
594
                }
595
                $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType(
×
596
                    $e->getMessage(),
×
597
                    $value,
×
598
                    [$className],
×
599
                    $context['deserialization_path'] ?? null,
×
600
                    true,
×
601
                    $e->getCode(),
×
602
                    $e
×
603
                );
×
604

605
                return null;
×
606
            }
607
        }
608

UNCOV
609
        if ($propertyMetadata->isWritableLink()) {
22✔
UNCOV
610
            $context['api_allow_update'] = true;
22✔
611

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

UNCOV
616
            $item = $this->serializer->denormalize($value, $className, $format, $context);
22✔
UNCOV
617
            if (!\is_object($item) && null !== $item) {
20✔
618
                throw new \UnexpectedValueException('Expected item to be an object or null.');
×
619
            }
620

UNCOV
621
            return $item;
20✔
622
        }
623

624
        if (!\is_array($value)) {
×
625
            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);
×
626
        }
627

628
        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);
×
629
    }
630

631
    /**
632
     * Gets the options for the property name collection / property metadata factories.
633
     */
634
    protected function getFactoryOptions(array $context): array
635
    {
636
        $options = [];
998✔
637
        if (isset($context[self::GROUPS])) {
998✔
638
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
639
            $options['serializer_groups'] = (array) $context[self::GROUPS];
339✔
640
        }
641

642
        $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['root_operation_name'] ?? '');
998✔
643
        $suffix = ($context['api_normalize'] ?? '') ? 'n' : '';
998✔
644
        if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey.$suffix])) {
998✔
645
            return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix];
984✔
646
        }
647

648
        // This is a hot spot
649
        if (isset($context['resource_class'])) {
998✔
650
            // Note that the groups need to be read on the root operation
651
            if ($operation = ($context['root_operation'] ?? null)) {
998✔
652
                $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null;
453✔
653
                $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null;
453✔
654
                $options['operation_name'] = $operation->getName();
453✔
655
            }
656
        }
657

658
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
998✔
659
    }
660

661
    /**
662
     * {@inheritdoc}
663
     *
664
     * @throws UnexpectedValueException
665
     */
666
    protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
667
    {
668
        $context['api_attribute'] = $attribute;
972✔
669
        $context['property_metadata'] = $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
972✔
670

671
        if ($context['api_denormalize'] ?? false) {
972✔
UNCOV
672
            return $this->propertyAccessor->getValue($object, $attribute);
9✔
673
        }
674

675
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
972✔
676

677
        foreach ($types as $type) {
972✔
678
            if (
679
                $type->isCollection()
957✔
680
                && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
957✔
681
                && ($className = $collectionValueType->getClassName())
957✔
682
                && $this->resourceClassResolver->isResourceClass($className)
957✔
683
            ) {
684
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
208✔
685

686
                // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
687
                // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
688
                if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
208✔
UNCOV
689
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
1✔
UNCOV
690
                        operationName: $itemUriTemplate,
1✔
UNCOV
691
                        forceCollection: true,
1✔
UNCOV
692
                        httpOperation: true
1✔
UNCOV
693
                    );
1✔
694

UNCOV
695
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
1✔
696
                }
697

698
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
207✔
699

700
                if (!is_iterable($attributeValue)) {
207✔
701
                    throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
702
                }
703

704
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
207✔
705

706
                $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
207✔
707
                $context['data'] = $data;
207✔
708
                $context['type'] = $type;
207✔
709

710
                if ($this->tagCollector) {
207✔
711
                    $this->tagCollector->collect($context);
188✔
712
                }
713

714
                return $data;
207✔
715
            }
716

717
            if (
718
                ($className = $type->getClassName())
957✔
719
                && $this->resourceClassResolver->isResourceClass($className)
957✔
720
            ) {
721
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
284✔
722
                unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
284✔
723
                if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
284✔
UNCOV
724
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
1✔
UNCOV
725
                        operationName: $uriTemplate,
1✔
UNCOV
726
                        httpOperation: true
1✔
UNCOV
727
                    );
1✔
728

UNCOV
729
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
1✔
730
                }
731

732
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
283✔
733

734
                if (!\is_object($attributeValue) && null !== $attributeValue) {
281✔
735
                    throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
736
                }
737

738
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
281✔
739

740
                $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
281✔
741
                $context['data'] = $data;
281✔
742
                $context['type'] = $type;
281✔
743

744
                if ($this->tagCollector) {
281✔
745
                    $this->tagCollector->collect($context);
257✔
746
                }
747

748
                return $data;
281✔
749
            }
750

751
            if (!$this->serializer instanceof NormalizerInterface) {
950✔
752
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
753
            }
754

755
            unset(
950✔
756
                $context['resource_class'],
950✔
757
                $context['force_resource_class'],
950✔
758
                $context['uri_variables'],
950✔
759
            );
950✔
760

761
            // Anonymous resources
762
            if ($className) {
950✔
763
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
307✔
764
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
307✔
765

766
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
307✔
767

768
                return $this->serializer->normalize($attributeValue, $format, $childContext);
304✔
769
            }
770

771
            if ('array' === $type->getBuiltinType()) {
942✔
772
                if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
208✔
773
                    $context = $this->createOperationContext($context, $className);
6✔
774
                }
775

776
                $childContext = $this->createChildContext($context, $attribute, $format);
208✔
777
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
208✔
778

779
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
208✔
780

781
                return $this->serializer->normalize($attributeValue, $format, $childContext);
208✔
782
            }
783
        }
784

785
        if (!$this->serializer instanceof NormalizerInterface) {
956✔
786
            throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
787
        }
788

789
        unset(
956✔
790
            $context['resource_class'],
956✔
791
            $context['force_resource_class'],
956✔
792
            $context['uri_variables']
956✔
793
        );
956✔
794

795
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
956✔
796

797
        return $this->serializer->normalize($attributeValue, $format, $context);
954✔
798
    }
799

800
    /**
801
     * Normalizes a collection of relations (to-many).
802
     *
803
     * @throws UnexpectedValueException
804
     */
805
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
806
    {
807
        $value = [];
188✔
808
        foreach ($attributeValue as $index => $obj) {
188✔
UNCOV
809
            if (!\is_object($obj) && null !== $obj) {
44✔
810
                throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
×
811
            }
812

813
            // update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources)
UNCOV
814
            $objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass);
44✔
UNCOV
815
            $context['resource_class'] = $objResourceClass;
44✔
UNCOV
816
            if ($this->resourceMetadataCollectionFactory) {
44✔
UNCOV
817
                $context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation();
44✔
818
            }
819

UNCOV
820
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
44✔
821
        }
822

823
        return $value;
188✔
824
    }
825

826
    /**
827
     * Normalizes a relation.
828
     *
829
     * @throws LogicException
830
     * @throws UnexpectedValueException
831
     */
832
    protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $relatedObject, string $resourceClass, ?string $format, array $context): \ArrayObject|array|string|null
833
    {
834
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
244✔
835
            if (!$this->serializer instanceof NormalizerInterface) {
199✔
836
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
837
            }
838

839
            $relatedContext = $this->createOperationContext($context, $resourceClass);
199✔
840
            $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $relatedContext);
199✔
841
            if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
199✔
842
                throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
×
843
            }
844

845
            return $normalizedRelatedObject;
199✔
846
        }
847

848
        $context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
72✔
849
        $context['data'] = $iri;
72✔
850
        $context['object'] = $relatedObject;
72✔
851
        unset($context['property_metadata'], $context['api_attribute']);
72✔
852

853
        if ($this->tagCollector) {
72✔
UNCOV
854
            $this->tagCollector->collect($context);
68✔
855
        } elseif (isset($context['resources'])) {
4✔
856
            $context['resources'][$iri] = $iri;
×
857
        }
858

859
        $push = $propertyMetadata->getPush() ?? false;
72✔
860
        if (isset($context['resources_to_push']) && $push) {
72✔
861
            $context['resources_to_push'][$iri] = $iri;
×
862
        }
863

864
        return $iri;
72✔
865
    }
866

867
    private function createAttributeValue(string $attribute, mixed $value, ?string $format = null, array &$context = []): mixed
868
    {
869
        try {
870
            return $this->createAndValidateAttributeValue($attribute, $value, $format, $context);
178✔
UNCOV
871
        } catch (NotNormalizableValueException $exception) {
13✔
UNCOV
872
            if (!isset($context['not_normalizable_value_exceptions'])) {
9✔
UNCOV
873
                throw $exception;
9✔
874
            }
875
            $context['not_normalizable_value_exceptions'][] = $exception;
×
876

877
            throw $exception;
×
878
        }
879
    }
880

881
    private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed
882
    {
883
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
186✔
884
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
186✔
885
        $isMultipleTypes = \count($types) > 1;
186✔
886
        $denormalizationException = null;
186✔
887

888
        foreach ($types as $type) {
186✔
889
            if (null === $value && ($type->isNullable() || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
184✔
UNCOV
890
                return $value;
2✔
891
            }
892

893
            $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
184✔
894

895
            /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
896
            // Fix a collection that contains the only one element
897
            // This is special to xml format only
898
            if ('xml' === $format && null !== $collectionValueType && (!\is_array($value) || !\is_int(key($value)))) {
184✔
UNCOV
899
                $value = [$value];
1✔
900
            }
901

902
            if (
903
                $type->isCollection()
184✔
904
                && null !== $collectionValueType
184✔
905
                && null !== ($className = $collectionValueType->getClassName())
184✔
906
                && $this->resourceClassResolver->isResourceClass($className)
184✔
907
            ) {
UNCOV
908
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
13✔
UNCOV
909
                $context['resource_class'] = $resourceClass;
13✔
UNCOV
910
                unset($context['uri_variables']);
13✔
911

912
                try {
UNCOV
913
                    return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
13✔
UNCOV
914
                } catch (NotNormalizableValueException $e) {
2✔
915
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
916
                    if ($isMultipleTypes) {
2✔
917
                        $denormalizationException ??= $e;
×
918

919
                        continue;
×
920
                    }
921

UNCOV
922
                    throw $e;
2✔
923
                }
924
            }
925

926
            if (
927
                null !== ($className = $type->getClassName())
181✔
928
                && $this->resourceClassResolver->isResourceClass($className)
181✔
929
            ) {
UNCOV
930
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
47✔
UNCOV
931
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
47✔
932

933
                try {
UNCOV
934
                    return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
47✔
UNCOV
935
                } catch (NotNormalizableValueException $e) {
5✔
936
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
937
                    if ($isMultipleTypes) {
1✔
938
                        $denormalizationException ??= $e;
×
939

940
                        continue;
×
941
                    }
942

UNCOV
943
                    throw $e;
1✔
944
                }
945
            }
946

947
            if (
948
                $type->isCollection()
167✔
949
                && null !== $collectionValueType
167✔
950
                && null !== ($className = $collectionValueType->getClassName())
167✔
951
                && \is_array($value)
167✔
952
            ) {
UNCOV
953
                if (!$this->serializer instanceof DenormalizerInterface) {
3✔
954
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
955
                }
956

UNCOV
957
                unset($context['resource_class'], $context['uri_variables']);
3✔
958

959
                try {
UNCOV
960
                    return $this->serializer->denormalize($value, $className.'[]', $format, $context);
3✔
961
                } catch (NotNormalizableValueException $e) {
×
962
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
963
                    if ($isMultipleTypes) {
×
964
                        $denormalizationException ??= $e;
×
965

966
                        continue;
×
967
                    }
968

969
                    throw $e;
×
970
                }
971
            }
972

973
            if (null !== $className = $type->getClassName()) {
167✔
UNCOV
974
                if (!$this->serializer instanceof DenormalizerInterface) {
14✔
975
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
976
                }
977

UNCOV
978
                unset($context['resource_class'], $context['uri_variables']);
14✔
979

980
                try {
UNCOV
981
                    return $this->serializer->denormalize($value, $className, $format, $context);
14✔
UNCOV
982
                } catch (NotNormalizableValueException $e) {
4✔
983
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
984
                    if ($isMultipleTypes) {
4✔
UNCOV
985
                        $denormalizationException ??= $e;
2✔
986

UNCOV
987
                        continue;
2✔
988
                    }
989

UNCOV
990
                    throw $e;
2✔
991
                }
992
            }
993

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

UNCOV
1003
                switch ($type->getBuiltinType()) {
15✔
1004
                    case Type::BUILTIN_TYPE_BOOL:
15✔
1005
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
UNCOV
1006
                        if ('false' === $value || '0' === $value) {
4✔
UNCOV
1007
                            $value = false;
2✔
UNCOV
1008
                        } elseif ('true' === $value || '1' === $value) {
2✔
UNCOV
1009
                            $value = true;
2✔
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 bool ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null);
×
1016
                        }
UNCOV
1017
                        break;
4✔
1018
                    case Type::BUILTIN_TYPE_INT:
11✔
UNCOV
1019
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
4✔
UNCOV
1020
                            $value = (int) $value;
4✔
1021
                        } else {
1022
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1023
                            if ($isMultipleTypes) {
×
1024
                                break 2;
×
1025
                            }
1026
                            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);
×
1027
                        }
UNCOV
1028
                        break;
4✔
1029
                    case Type::BUILTIN_TYPE_FLOAT:
7✔
UNCOV
1030
                        if (is_numeric($value)) {
4✔
UNCOV
1031
                            return (float) $value;
1✔
1032
                        }
1033

1034
                        switch ($value) {
UNCOV
1035
                            case 'NaN':
3✔
UNCOV
1036
                                return \NAN;
1✔
UNCOV
1037
                            case 'INF':
2✔
UNCOV
1038
                                return \INF;
1✔
UNCOV
1039
                            case '-INF':
1✔
UNCOV
1040
                                return -\INF;
1✔
1041
                            default:
1042
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1043
                                if ($isMultipleTypes) {
×
1044
                                    break 3;
×
1045
                                }
1046
                                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);
×
1047
                        }
1048
                }
1049
            }
1050

1051
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
159✔
1052
                return $value;
×
1053
            }
1054

1055
            try {
1056
                $this->validateType($attribute, $type, $value, $format, $context);
159✔
1057

1058
                break;
155✔
UNCOV
1059
            } catch (NotNormalizableValueException $e) {
5✔
1060
                // union/intersect types: try the next type
UNCOV
1061
                if (!$isMultipleTypes) {
5✔
UNCOV
1062
                    throw $e;
4✔
1063
                }
1064
            }
1065
        }
1066

1067
        if ($denormalizationException) {
157✔
UNCOV
1068
            throw $denormalizationException;
1✔
1069
        }
1070

1071
        return $value;
157✔
1072
    }
1073

1074
    /**
1075
     * Sets a value of the object using the PropertyAccess component.
1076
     */
1077
    private function setValue(object $object, string $attributeName, mixed $value): void
1078
    {
1079
        try {
1080
            $this->propertyAccessor->setValue($object, $attributeName, $value);
170✔
UNCOV
1081
        } catch (NoSuchPropertyException) {
8✔
1082
            // Properties not found are ignored
1083
        }
1084
    }
1085
}
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