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

api-platform / core / 12054390631

27 Nov 2024 04:23PM UTC coverage: 62.123% (+0.06%) from 62.06%
12054390631

push

github

web-flow
fix(serializer): use attribute denormalization context for constructor arguments (#6821)

2 of 4 new or added lines in 1 file covered. (50.0%)

167 existing lines in 27 files now uncovered.

11481 of 18481 relevant lines covered (62.12%)

68.37 hits per line

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

51.06
/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\Api\IriConverterInterface as LegacyIriConverterInterface;
17
use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface;
18
use ApiPlatform\Metadata\ApiProperty;
19
use ApiPlatform\Metadata\CollectionOperationInterface;
20
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
21
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
22
use ApiPlatform\Metadata\IriConverterInterface;
23
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
24
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
25
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
26
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
27
use ApiPlatform\Metadata\ResourceClassResolverInterface;
28
use ApiPlatform\Metadata\UrlGeneratorInterface;
29
use ApiPlatform\Metadata\Util\ClassInfoTrait;
30
use ApiPlatform\Metadata\Util\CloneTrait;
31
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
32
use Symfony\Component\PropertyAccess\PropertyAccess;
33
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
34
use Symfony\Component\PropertyInfo\Type;
35
use Symfony\Component\Serializer\Encoder\CsvEncoder;
36
use Symfony\Component\Serializer\Encoder\XmlEncoder;
37
use Symfony\Component\Serializer\Exception\LogicException;
38
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
39
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
40
use Symfony\Component\Serializer\Exception\RuntimeException;
41
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
42
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
43
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
44
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
45
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
46
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
47
use Symfony\Component\Serializer\Serializer;
48

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

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

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

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

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

88
        $class = $context['force_resource_class'] ?? $this->getObjectClass($data);
395✔
89
        if (($context['output']['class'] ?? null) === $class) {
395✔
90
            return true;
16✔
91
        }
92

93
        return $this->resourceClassResolver->isResourceClass($class);
387✔
94
    }
95

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

103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function hasCacheableSupportsMethod(): bool
107
    {
108
        if (method_exists(Serializer::class, 'getSupportedTypes')) {
×
109
            trigger_deprecation(
×
110
                'api-platform/core',
×
111
                '3.1',
×
112
                'The "%s()" method is deprecated, use "getSupportedTypes()" instead.',
×
113
                __METHOD__
×
114
            );
×
115
        }
116

117
        return true;
×
118
    }
119

120
    /**
121
     * {@inheritdoc}
122
     *
123
     * @throws LogicException
124
     */
125
    public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
126
    {
127
        $resourceClass = $context['force_resource_class'] ?? $this->getObjectClass($object);
407✔
128
        if ($outputClass = $this->getOutputClass($context)) {
407✔
129
            if (!$this->serializer instanceof NormalizerInterface) {
16✔
130
                throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
×
131
            }
132

133
            unset($context['output'], $context['operation'], $context['operation_name']);
16✔
134
            $context['resource_class'] = $outputClass;
16✔
135
            $context['api_sub_level'] = true;
16✔
136
            $context[self::ALLOW_EXTRA_ATTRIBUTES] = false;
16✔
137

138
            return $this->serializer->normalize($object, $format, $context);
16✔
139
        }
140

141
        // Never remove this, with `application/json` we don't use our AbstractCollectionNormalizer and we need
142
        // to remove the collection operation from our context or we'll introduce security issues
143
        if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
407✔
144
            unset($context['operation_name'], $context['operation'], $context['iri']);
8✔
145
        }
146

147
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
407✔
148
            $context = $this->initContext($resourceClass, $context);
391✔
149
        }
150

151
        $context['api_normalize'] = true;
407✔
152
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
407✔
153

154
        /*
155
         * When true, converts the normalized data array of a resource into an
156
         * IRI, if the normalized data array is empty.
157
         *
158
         * This is useful when traversing from a non-resource towards an attribute
159
         * which is a resource, as we do not have the benefit of {@see ApiProperty::isReadableLink}.
160
         *
161
         * It must not be propagated to resources, as {@see ApiProperty::isReadableLink}
162
         * should take effect.
163
         */
164
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
407✔
165
        unset($context['api_empty_resource_as_iri']);
407✔
166

167
        if (!$this->tagCollector && isset($context['resources'])) {
407✔
168
            $context['resources'][$iri] = $iri;
×
169
        }
170

171
        $context['object'] = $object;
407✔
172
        $context['format'] = $format;
407✔
173

174
        $data = parent::normalize($object, $format, $context);
407✔
175

176
        $context['data'] = $data;
407✔
177
        unset($context['property_metadata'], $context['api_attribute']);
407✔
178

179
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
407✔
180
            $context['data'] = $iri;
×
181

182
            if ($this->tagCollector) {
×
183
                $this->tagCollector->collect($context);
×
184
            }
185

186
            return $iri;
×
187
        }
188

189
        if ($this->tagCollector) {
407✔
190
            $this->tagCollector->collect($context);
352✔
191
        }
192

193
        return $data;
407✔
194
    }
195

196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
200
    {
201
        if (($context['input']['class'] ?? null) === $type) {
12✔
202
            return true;
×
203
        }
204

205
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
12✔
206
    }
207

208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function denormalize(mixed $data, string $class, ?string $format = null, array $context = []): mixed
212
    {
213
        $resourceClass = $class;
12✔
214

215
        if ($inputClass = $this->getInputClass($context)) {
12✔
216
            if (!$this->serializer instanceof DenormalizerInterface) {
×
217
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
×
218
            }
219

220
            unset($context['input'], $context['operation'], $context['operation_name'], $context['uri_variables']);
×
221
            $context['resource_class'] = $inputClass;
×
222

223
            try {
224
                return $this->serializer->denormalize($data, $inputClass, $format, $context);
×
225
            } catch (NotNormalizableValueException $e) {
×
226
                throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
×
227
            }
228
        }
229

230
        if (null === $objectToPopulate = $this->extractObjectToPopulate($resourceClass, $context, static::OBJECT_TO_POPULATE)) {
12✔
231
            $normalizedData = \is_scalar($data) ? [$data] : $this->prepareForDenormalization($data);
12✔
232
            $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class, $context);
12✔
233
        }
234

235
        $context['api_denormalize'] = true;
12✔
236

237
        if ($this->resourceClassResolver->isResourceClass($class)) {
12✔
238
            $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
12✔
239
            $context['resource_class'] = $resourceClass;
12✔
240
        }
241

242
        if (\is_string($data)) {
12✔
243
            try {
244
                return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
×
245
            } catch (ItemNotFoundException $e) {
×
246
                throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
247
            } catch (InvalidArgumentException $e) {
×
248
                throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
×
249
            }
250
        }
251

252
        if (!\is_array($data)) {
12✔
253
            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);
×
254
        }
255

256
        $previousObject = $this->clone($objectToPopulate);
12✔
257
        $object = parent::denormalize($data, $class, $format, $context);
12✔
258

259
        if (!$this->resourceClassResolver->isResourceClass($class)) {
12✔
260
            return $object;
×
261
        }
262

263
        // Bypass the post-denormalize attribute revert logic if the object could not be
264
        // cloned since we cannot possibly revert any changes made to it.
265
        if (null !== $objectToPopulate && null === $previousObject) {
12✔
266
            return $object;
×
267
        }
268

269
        $options = $this->getFactoryOptions($context);
12✔
270
        $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options));
12✔
271

272
        // Revert attributes that aren't allowed to be changed after a post-denormalize check
273
        foreach (array_keys($data) as $attribute) {
12✔
274
            $attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute;
12✔
275
            if (!\in_array($attribute, $propertyNames, true)) {
12✔
276
                continue;
×
277
            }
278

279
            if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
12✔
280
                if (null !== $previousObject) {
×
281
                    $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute));
×
282
                } else {
283
                    $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
×
284
                    $this->setValue($object, $attribute, $propertyMetadata->getDefault());
×
285
                }
286
            }
287
        }
288

289
        return $object;
12✔
290
    }
291

292
    /**
293
     * Method copy-pasted from symfony/serializer.
294
     * Remove it after symfony/serializer version update @see https://github.com/symfony/symfony/pull/28263.
295
     *
296
     * {@inheritdoc}
297
     *
298
     * @internal
299
     */
300
    protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, ?string $format = null): object
301
    {
302
        if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
12✔
303
            unset($context[static::OBJECT_TO_POPULATE]);
×
304

305
            return $object;
×
306
        }
307

308
        $class = $this->getClassDiscriminatorResolvedClass($data, $class, $context);
12✔
309
        $reflectionClass = new \ReflectionClass($class);
12✔
310

311
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
12✔
312
        if ($constructor) {
12✔
313
            $constructorParameters = $constructor->getParameters();
12✔
314

315
            $params = [];
12✔
316
            $missingConstructorArguments = [];
12✔
317
            foreach ($constructorParameters as $constructorParameter) {
12✔
318
                $paramName = $constructorParameter->name;
12✔
319
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
12✔
320
                $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
12✔
321
                $attributeContext['deserialization_path'] = $attributeContext['deserialization_path'] ?? $key;
12✔
322

323
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
12✔
324
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
12✔
325
                if ($constructorParameter->isVariadic()) {
12✔
326
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
×
327
                        if (!\is_array($data[$paramName])) {
×
328
                            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));
×
329
                        }
330

331
                        $params[] = $data[$paramName];
×
332
                    }
333
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
12✔
334
                    try {
NEW
335
                        $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $attributeContext, $format);
×
336
                    } catch (NotNormalizableValueException $exception) {
×
337
                        if (!isset($context['not_normalizable_value_exceptions'])) {
×
338
                            throw $exception;
×
339
                        }
340
                        $context['not_normalizable_value_exceptions'][] = $exception;
×
341
                    }
342

343
                    // Don't run set for a parameter passed to the constructor
344
                    unset($data[$key]);
×
345
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
12✔
346
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
×
347
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
12✔
348
                    $params[] = $constructorParameter->getDefaultValue();
12✔
349
                } else {
350
                    if (!isset($context['not_normalizable_value_exceptions'])) {
×
351
                        $missingConstructorArguments[] = $constructorParameter->name;
×
352
                    }
353

354
                    $constructorParameterType = 'unknown';
×
355
                    $reflectionType = $constructorParameter->getType();
×
356
                    if ($reflectionType instanceof \ReflectionNamedType) {
×
357
                        $constructorParameterType = $reflectionType->getName();
×
358
                    }
359

360
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(
×
361
                        \sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
×
362
                        null,
×
363
                        [$constructorParameterType],
×
NEW
364
                        $attributeContext['deserialization_path'],
×
365
                        true
×
366
                    );
×
367
                    $context['not_normalizable_value_exceptions'][] = $exception;
×
368
                }
369
            }
370

371
            if ($missingConstructorArguments) {
12✔
372
                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);
×
373
            }
374

375
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
12✔
376
                return $reflectionClass->newInstanceWithoutConstructor();
×
377
            }
378

379
            if ($constructor->isConstructor()) {
12✔
380
                return $reflectionClass->newInstanceArgs($params);
12✔
381
            }
382

383
            return $constructor->invokeArgs(null, $params);
×
384
        }
385

386
        return new $class();
×
387
    }
388

389
    protected function getClassDiscriminatorResolvedClass(array $data, string $class, array $context = []): string
390
    {
391
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
12✔
392
            return $class;
12✔
393
        }
394

395
        if (!isset($data[$mapping->getTypeProperty()])) {
×
396
            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());
×
397
        }
398

399
        $type = $data[$mapping->getTypeProperty()];
×
400
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
×
401
            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);
×
402
        }
403

404
        return $mappedClass;
×
405
    }
406

407
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array $context, ?string $format = null): mixed
408
    {
409
        return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
×
410
    }
411

412
    /**
413
     * {@inheritdoc}
414
     *
415
     * Unused in this context.
416
     *
417
     * @return string[]
418
     */
419
    protected function extractAttributes($object, $format = null, array $context = []): array
420
    {
421
        return [];
×
422
    }
423

424
    /**
425
     * {@inheritdoc}
426
     */
427
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
428
    {
429
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
407✔
430
            return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
16✔
431
        }
432

433
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
391✔
434
        $options = $this->getFactoryOptions($context);
391✔
435
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
391✔
436

437
        $allowedAttributes = [];
391✔
438
        foreach ($propertyNames as $propertyName) {
391✔
439
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
375✔
440

441
            if (
442
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
375✔
443
                && (isset($context['api_normalize']) && $propertyMetadata->isReadable()
375✔
444
                    || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
375✔
445
                )
446
            ) {
447
                $allowedAttributes[] = $propertyName;
363✔
448
            }
449
        }
450

451
        return $allowedAttributes;
391✔
452
    }
453

454
    /**
455
     * {@inheritdoc}
456
     */
457
    protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
458
    {
459
        if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
391✔
460
            return false;
24✔
461
        }
462

463
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
391✔
464
    }
465

466
    /**
467
     * Check if access to the attribute is granted.
468
     */
469
    protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
470
    {
471
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
391✔
472
            return true;
16✔
473
        }
474

475
        $options = $this->getFactoryOptions($context);
375✔
476
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
375✔
477
        $security = $propertyMetadata->getSecurity();
375✔
478
        if (null !== $this->resourceAccessChecker && $security) {
375✔
479
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
×
480
                'object' => $object,
×
481
                'property' => $attribute,
×
482
            ]);
×
483
        }
484

485
        return true;
375✔
486
    }
487

488
    /**
489
     * Check if access to the attribute is granted.
490
     */
491
    protected function canAccessAttributePostDenormalize(?object $object, ?object $previousObject, string $attribute, array $context = []): bool
492
    {
493
        $options = $this->getFactoryOptions($context);
12✔
494
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
12✔
495
        $security = $propertyMetadata->getSecurityPostDenormalize();
12✔
496
        if ($this->resourceAccessChecker && $security) {
12✔
497
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
×
498
                'object' => $object,
×
499
                'previous_object' => $previousObject,
×
500
                'property' => $attribute,
×
501
            ]);
×
502
        }
503

504
        return true;
12✔
505
    }
506

507
    /**
508
     * {@inheritdoc}
509
     */
510
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
511
    {
512
        try {
513
            $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
12✔
514
        } catch (NotNormalizableValueException $exception) {
×
515
            // Only throw if collecting denormalization errors is disabled.
516
            if (!isset($context['not_normalizable_value_exceptions'])) {
×
517
                throw $exception;
×
518
            }
519
        }
520
    }
521

522
    /**
523
     * Validates the type of the value. Allows using integers as floats for JSON formats.
524
     *
525
     * @throws NotNormalizableValueException
526
     */
527
    protected function validateType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void
528
    {
529
        $builtinType = $type->getBuiltinType();
12✔
530
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
12✔
531
            $isValid = \is_float($value) || \is_int($value);
×
532
        } else {
533
            $isValid = \call_user_func('is_'.$builtinType, $value);
12✔
534
        }
535

536
        if (!$isValid) {
12✔
537
            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);
×
538
        }
539
    }
540

541
    /**
542
     * Denormalizes a collection of objects.
543
     *
544
     * @throws NotNormalizableValueException
545
     */
546
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
547
    {
548
        if (!\is_array($value)) {
×
549
            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);
×
550
        }
551

552
        $values = [];
×
553
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
×
554
        $collectionKeyTypes = $type->getCollectionKeyTypes();
×
555
        foreach ($value as $index => $obj) {
×
556
            $currentChildContext = $childContext;
×
557
            if (isset($childContext['deserialization_path'])) {
×
558
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
×
559
            }
560

561
            // no typehint provided on collection key
562
            if (!$collectionKeyTypes) {
×
563
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
564
                continue;
×
565
            }
566

567
            // validate collection key typehint
568
            foreach ($collectionKeyTypes as $collectionKeyType) {
×
569
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
×
570
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
×
571
                    continue;
×
572
                }
573

574
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
575
                continue 2;
×
576
            }
577
            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);
×
578
        }
579

580
        return $values;
×
581
    }
582

583
    /**
584
     * Denormalizes a relation.
585
     *
586
     * @throws LogicException
587
     * @throws UnexpectedValueException
588
     * @throws NotNormalizableValueException
589
     */
590
    protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object
591
    {
592
        if (\is_string($value)) {
×
593
            try {
594
                return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
×
595
            } catch (ItemNotFoundException $e) {
×
596
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
597
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
598
                }
599
                $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType(
×
600
                    $e->getMessage(),
×
601
                    $value,
×
602
                    [$className],
×
603
                    $context['deserialization_path'] ?? null,
×
604
                    true,
×
605
                    $e->getCode(),
×
606
                    $e
×
607
                );
×
608

609
                return null;
×
610
            } catch (InvalidArgumentException $e) {
×
611
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
612
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
×
613
                }
614
                $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType(
×
615
                    $e->getMessage(),
×
616
                    $value,
×
617
                    [$className],
×
618
                    $context['deserialization_path'] ?? null,
×
619
                    true,
×
620
                    $e->getCode(),
×
621
                    $e
×
622
                );
×
623

624
                return null;
×
625
            }
626
        }
627

628
        if ($propertyMetadata->isWritableLink()) {
×
629
            $context['api_allow_update'] = true;
×
630

631
            if (!$this->serializer instanceof DenormalizerInterface) {
×
632
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
633
            }
634

635
            $item = $this->serializer->denormalize($value, $className, $format, $context);
×
636
            if (!\is_object($item) && null !== $item) {
×
637
                throw new \UnexpectedValueException('Expected item to be an object or null.');
×
638
            }
639

640
            return $item;
×
641
        }
642

643
        if (!\is_array($value)) {
×
644
            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);
×
645
        }
646

647
        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);
×
648
    }
649

650
    /**
651
     * Gets the options for the property name collection / property metadata factories.
652
     */
653
    protected function getFactoryOptions(array $context): array
654
    {
655
        $options = [];
407✔
656
        if (isset($context[self::GROUPS])) {
407✔
657
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
658
            $options['serializer_groups'] = (array) $context[self::GROUPS];
224✔
659
        }
660

661
        $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['root_operation_name'] ?? '');
407✔
662
        $suffix = ($context['api_normalize'] ?? '') ? 'n' : '';
407✔
663
        if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey.$suffix])) {
407✔
664
            return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix];
391✔
665
        }
666

667
        // This is a hot spot
668
        if (isset($context['resource_class'])) {
407✔
669
            // Note that the groups need to be read on the root operation
670
            if ($operation = ($context['root_operation'] ?? null)) {
407✔
671
                $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null;
108✔
672
                $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null;
108✔
673
                $options['operation_name'] = $operation->getName();
108✔
674
            }
675
        }
676

677
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
407✔
678
    }
679

680
    /**
681
     * {@inheritdoc}
682
     *
683
     * @throws UnexpectedValueException
684
     */
685
    protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
686
    {
687
        $context['api_attribute'] = $attribute;
379✔
688
        $context['property_metadata'] = $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
379✔
689

690
        if ($context['api_denormalize'] ?? false) {
379✔
691
            return $this->propertyAccessor->getValue($object, $attribute);
×
692
        }
693

694
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
379✔
695

696
        foreach ($types as $type) {
379✔
697
            if (
698
                $type->isCollection()
375✔
699
                && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
375✔
700
                && ($className = $collectionValueType->getClassName())
375✔
701
                && $this->resourceClassResolver->isResourceClass($className)
375✔
702
            ) {
703
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
24✔
704

705
                // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
706
                // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
707
                if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
24✔
708
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
709
                        operationName: $itemUriTemplate,
×
710
                        forceCollection: true,
×
711
                        httpOperation: true
×
712
                    );
×
713

714
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
715
                }
716

717
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
24✔
718

719
                if (!is_iterable($attributeValue)) {
24✔
720
                    throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
721
                }
722

723
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
24✔
724

725
                $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
24✔
726
                $context['data'] = $data;
24✔
727
                $context['type'] = $type;
24✔
728

729
                if ($this->tagCollector) {
24✔
730
                    $this->tagCollector->collect($context);
24✔
731
                }
732

733
                return $data;
24✔
734
            }
735

736
            if (
737
                ($className = $type->getClassName())
375✔
738
                && $this->resourceClassResolver->isResourceClass($className)
375✔
739
            ) {
740
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
24✔
741
                unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
24✔
742

743
                if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
24✔
744
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
745
                        operationName: $uriTemplate,
×
746
                        httpOperation: true
×
747
                    );
×
748

749
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
750
                }
751

752
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
24✔
753

754
                if (!\is_object($attributeValue) && null !== $attributeValue) {
24✔
755
                    throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
756
                }
757

758
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
24✔
759

760
                $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
24✔
761
                $context['data'] = $data;
24✔
762
                $context['type'] = $type;
24✔
763

764
                if ($this->tagCollector) {
24✔
765
                    $this->tagCollector->collect($context);
12✔
766
                }
767

768
                return $data;
24✔
769
            }
770

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

775
            unset(
375✔
776
                $context['resource_class'],
375✔
777
                $context['force_resource_class'],
375✔
778
                $context['uri_variables'],
375✔
779
            );
375✔
780

781
            // Anonymous resources
782
            if ($className) {
375✔
783
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
83✔
784
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
83✔
785

786
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
83✔
787

788
                return $this->serializer->normalize($attributeValue, $format, $childContext);
83✔
789
            }
790

791
            if ('array' === $type->getBuiltinType()) {
367✔
792
                if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
56✔
793
                    $context = $this->createOperationContext($context, $className);
8✔
794
                }
795

796
                $childContext = $this->createChildContext($context, $attribute, $format);
56✔
797
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
56✔
798

799
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
56✔
800

801
                return $this->serializer->normalize($attributeValue, $format, $childContext);
56✔
802
            }
803
        }
804

805
        if (!$this->serializer instanceof NormalizerInterface) {
371✔
806
            throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
807
        }
808

809
        unset(
371✔
810
            $context['resource_class'],
371✔
811
            $context['force_resource_class'],
371✔
812
            $context['uri_variables']
371✔
813
        );
371✔
814

815
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
371✔
816

817
        return $this->serializer->normalize($attributeValue, $format, $context);
371✔
818
    }
819

820
    /**
821
     * Normalizes a collection of relations (to-many).
822
     *
823
     * @throws UnexpectedValueException
824
     */
825
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
826
    {
827
        $value = [];
24✔
828
        foreach ($attributeValue as $index => $obj) {
24✔
829
            if (!\is_object($obj) && null !== $obj) {
×
830
                throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
×
831
            }
832

833
            // update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources)
834
            $objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass);
×
835
            $context['resource_class'] = $objResourceClass;
×
836
            if ($this->resourceMetadataCollectionFactory) {
×
837
                $context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation();
×
838
            }
839

840
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
×
841
        }
842

843
        return $value;
24✔
844
    }
845

846
    /**
847
     * Normalizes a relation.
848
     *
849
     * @throws LogicException
850
     * @throws UnexpectedValueException
851
     */
852
    protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $relatedObject, string $resourceClass, ?string $format, array $context): \ArrayObject|array|string|null
853
    {
854
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
24✔
855
            if (!$this->serializer instanceof NormalizerInterface) {
16✔
856
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
857
            }
858

859
            $relatedContext = $this->createOperationContext($context, $resourceClass);
16✔
860
            $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $relatedContext);
16✔
861
            if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
16✔
862
                throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
×
863
            }
864

865
            return $normalizedRelatedObject;
16✔
866
        }
867

868
        $context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
8✔
869
        $context['data'] = $iri;
8✔
870
        $context['object'] = $relatedObject;
8✔
871
        unset($context['property_metadata'], $context['api_attribute']);
8✔
872

873
        if ($this->tagCollector) {
8✔
874
            $this->tagCollector->collect($context);
×
875
        } elseif (isset($context['resources'])) {
8✔
876
            $context['resources'][$iri] = $iri;
×
877
        }
878

879
        $push = $propertyMetadata->getPush() ?? false;
8✔
880
        if (isset($context['resources_to_push']) && $push) {
8✔
881
            $context['resources_to_push'][$iri] = $iri;
×
882
        }
883

884
        return $iri;
8✔
885
    }
886

887
    private function createAttributeValue(string $attribute, mixed $value, ?string $format = null, array &$context = []): mixed
888
    {
889
        try {
890
            return $this->createAndValidateAttributeValue($attribute, $value, $format, $context);
12✔
891
        } catch (NotNormalizableValueException $exception) {
×
892
            if (!isset($context['not_normalizable_value_exceptions'])) {
×
893
                throw $exception;
×
894
            }
895
            $context['not_normalizable_value_exceptions'][] = $exception;
×
896

897
            throw $exception;
×
898
        }
899
    }
900

901
    private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed
902
    {
903
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
12✔
904
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
12✔
905
        $isMultipleTypes = \count($types) > 1;
12✔
906

907
        foreach ($types as $type) {
12✔
908
            if (null === $value && ($type->isNullable() || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
12✔
909
                return $value;
×
910
            }
911

912
            $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
12✔
913

914
            /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
915
            // Fix a collection that contains the only one element
916
            // This is special to xml format only
917
            if ('xml' === $format && null !== $collectionValueType && (!\is_array($value) || !\is_int(key($value)))) {
12✔
918
                $value = [$value];
×
919
            }
920

921
            if (
922
                $type->isCollection()
12✔
923
                && null !== $collectionValueType
12✔
924
                && null !== ($className = $collectionValueType->getClassName())
12✔
925
                && $this->resourceClassResolver->isResourceClass($className)
12✔
926
            ) {
927
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
×
928
                $context['resource_class'] = $resourceClass;
×
929
                unset($context['uri_variables']);
×
930

931
                return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
×
932
            }
933

934
            if (
935
                null !== ($className = $type->getClassName())
12✔
936
                && $this->resourceClassResolver->isResourceClass($className)
12✔
937
            ) {
938
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
×
939
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
×
940

941
                return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
×
942
            }
943

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

954
                unset($context['resource_class'], $context['uri_variables']);
×
955

956
                return $this->serializer->denormalize($value, $className.'[]', $format, $context);
×
957
            }
958

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

964
                unset($context['resource_class'], $context['uri_variables']);
×
965

966
                return $this->serializer->denormalize($value, $className, $format, $context);
×
967
            }
968

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

978
                switch ($type->getBuiltinType()) {
×
979
                    case Type::BUILTIN_TYPE_BOOL:
980
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
981
                        if ('false' === $value || '0' === $value) {
×
982
                            $value = false;
×
983
                        } elseif ('true' === $value || '1' === $value) {
×
984
                            $value = true;
×
985
                        } else {
986
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
987
                            if ($isMultipleTypes) {
×
988
                                break 2;
×
989
                            }
990
                            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);
×
991
                        }
992
                        break;
×
993
                    case Type::BUILTIN_TYPE_INT:
994
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
×
995
                            $value = (int) $value;
×
996
                        } else {
997
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
998
                            if ($isMultipleTypes) {
×
999
                                break 2;
×
1000
                            }
1001
                            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);
×
1002
                        }
1003
                        break;
×
1004
                    case Type::BUILTIN_TYPE_FLOAT:
1005
                        if (is_numeric($value)) {
×
1006
                            return (float) $value;
×
1007
                        }
1008

1009
                        switch ($value) {
1010
                            case 'NaN':
×
1011
                                return \NAN;
×
1012
                            case 'INF':
×
1013
                                return \INF;
×
1014
                            case '-INF':
×
1015
                                return -\INF;
×
1016
                            default:
1017
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1018
                                if ($isMultipleTypes) {
×
1019
                                    break 3;
×
1020
                                }
1021
                                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);
×
1022
                        }
1023
                }
1024
            }
1025

1026
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
12✔
1027
                return $value;
×
1028
            }
1029

1030
            try {
1031
                $this->validateType($attribute, $type, $value, $format, $context);
12✔
1032

1033
                break;
12✔
1034
            } catch (NotNormalizableValueException $e) {
×
1035
                // union/intersect types: try the next type
1036
                if (!$isMultipleTypes) {
×
1037
                    throw $e;
×
1038
                }
1039
            }
1040
        }
1041

1042
        return $value;
12✔
1043
    }
1044

1045
    /**
1046
     * Sets a value of the object using the PropertyAccess component.
1047
     */
1048
    private function setValue(object $object, string $attributeName, mixed $value): void
1049
    {
1050
        try {
1051
            $this->propertyAccessor->setValue($object, $attributeName, $value);
12✔
1052
        } catch (NoSuchPropertyException) {
×
1053
            // Properties not found are ignored
1054
        }
1055
    }
1056
}
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