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

api-platform / core / 8156443696

05 Mar 2024 12:43PM UTC coverage: 57.081% (+0.2%) from 56.922%
8156443696

push

github

web-flow
Merge pull request #6201 from soyuka/merge-main

Merge 3.2

33 of 45 new or added lines in 9 files covered. (73.33%)

109 existing lines in 5 files now uncovered.

9553 of 16736 relevant lines covered (57.08%)

41.05 hits per line

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

55.21
/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\Exception\InvalidArgumentException;
19
use ApiPlatform\Exception\ItemNotFoundException;
20
use ApiPlatform\Metadata\ApiProperty;
21
use ApiPlatform\Metadata\CollectionOperationInterface;
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'])) {
184✔
70
            $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
184✔
71
        }
72

73
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable($this->getObjectClass(...)), $defaultContext);
184✔
74
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
184✔
75
        $this->resourceAccessChecker = $resourceAccessChecker;
184✔
76
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
184✔
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)) {
76✔
85
            return false;
40✔
86
        }
87

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

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

96
    public function getSupportedTypes(?string $format): array
97
    {
98
        return [
88✔
99
            'object' => true,
88✔
100
        ];
88✔
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);
76✔
128
        if ($outputClass = $this->getOutputClass($context)) {
76✔
129
            if (!$this->serializer instanceof NormalizerInterface) {
4✔
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']);
4✔
134
            $context['resource_class'] = $outputClass;
4✔
135
            $context['api_sub_level'] = true;
4✔
136
            $context[self::ALLOW_EXTRA_ATTRIBUTES] = false;
4✔
137

138
            return $this->serializer->normalize($object, $format, $context);
4✔
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) {
76✔
144
            unset($context['operation_name']);
×
145
            unset($context['operation']);
×
146
            unset($context['iri']);
×
147
        }
148

149
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
76✔
150
            $context = $this->initContext($resourceClass, $context);
72✔
151
        }
152

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

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

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

173
        $context['object'] = $object;
76✔
174
        $context['format'] = $format;
76✔
175

176
        $data = parent::normalize($object, $format, $context);
76✔
177

178
        $context['data'] = $data;
72✔
179
        unset($context['property_metadata']);
72✔
180
        unset($context['api_attribute']);
72✔
181

182
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
72✔
183
            $context['data'] = $iri;
×
184

185
            if ($this->tagCollector) {
×
186
                $this->tagCollector->collect($context);
×
187
            }
188

189
            return $iri;
×
190
        }
191

192
        if ($this->tagCollector) {
72✔
193
            $this->tagCollector->collect($context);
44✔
194
        }
195

196
        return $data;
72✔
197
    }
198

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

208
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
12✔
209
    }
210

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

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

223
            unset($context['input'], $context['operation'], $context['operation_name']);
×
224
            $context['resource_class'] = $inputClass;
×
225

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

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

238
        $context['api_denormalize'] = true;
28✔
239

240
        if ($this->resourceClassResolver->isResourceClass($class)) {
28✔
241
            $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
28✔
242
            $context['resource_class'] = $resourceClass;
28✔
243
        }
244

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

255
        if (!\is_array($data)) {
28✔
256
            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);
×
257
        }
258

259
        $previousObject = $this->clone($objectToPopulate);
28✔
260
        $object = parent::denormalize($data, $class, $format, $context);
28✔
261

262
        if (!$this->resourceClassResolver->isResourceClass($class)) {
16✔
263
            return $object;
×
264
        }
265

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

272
        $options = $this->getFactoryOptions($context);
16✔
273
        $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options));
16✔
274

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

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

292
        return $object;
16✔
293
    }
294

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

308
            return $object;
×
309
        }
310

311
        $class = $this->getClassDiscriminatorResolvedClass($data, $class, $context);
28✔
312
        $reflectionClass = new \ReflectionClass($class);
28✔
313

314
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
28✔
315
        if ($constructor) {
28✔
316
            $constructorParameters = $constructor->getParameters();
28✔
317

318
            $params = [];
28✔
319
            foreach ($constructorParameters as $constructorParameter) {
28✔
320
                $paramName = $constructorParameter->name;
×
321
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
×
322

323
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
×
324
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
×
325
                if ($constructorParameter->isVariadic()) {
×
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))) {
×
334
                    $constructorContext = $context;
×
335
                    $constructorContext['deserialization_path'] = $context['deserialization_path'] ?? $key;
×
336
                    try {
337
                        $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $constructorContext, $format);
×
338
                    } catch (NotNormalizableValueException $exception) {
×
339
                        if (!isset($context['not_normalizable_value_exceptions'])) {
×
340
                            throw $exception;
×
341
                        }
342
                        $context['not_normalizable_value_exceptions'][] = $exception;
×
343
                    }
344

345
                    // Don't run set for a parameter passed to the constructor
346
                    unset($data[$key]);
×
347
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
×
348
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
×
349
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
×
350
                    $params[] = $constructorParameter->getDefaultValue();
×
351
                } else {
352
                    if (!isset($context['not_normalizable_value_exceptions'])) {
×
353
                        throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name), 0, null, [$constructorParameter->name]);
×
354
                    }
355

356
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name), $data, ['unknown'], $context['deserialization_path'] ?? null, true);
×
357
                    $context['not_normalizable_value_exceptions'][] = $exception;
×
358
                }
359
            }
360

361
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
28✔
362
                return $reflectionClass->newInstanceWithoutConstructor();
×
363
            }
364

365
            if ($constructor->isConstructor()) {
28✔
366
                return $reflectionClass->newInstanceArgs($params);
28✔
367
            }
368

369
            return $constructor->invokeArgs(null, $params);
×
370
        }
371

372
        return new $class();
×
373
    }
374

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

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

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

390
        return $mappedClass;
×
391
    }
392

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

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

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

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

423
        $allowedAttributes = [];
84✔
424
        foreach ($propertyNames as $propertyName) {
84✔
425
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
84✔
426

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

437
        return $allowedAttributes;
84✔
438
    }
439

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

449
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
88✔
450
    }
451

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

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

471
        return true;
84✔
472
    }
473

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

490
        return true;
16✔
491
    }
492

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

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

522
        if (!$isValid) {
16✔
UNCOV
523
            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);
×
524
        }
525
    }
526

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

538
        $values = [];
8✔
539
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
8✔
540
        $collectionKeyTypes = $type->getCollectionKeyTypes();
8✔
541
        foreach ($value as $index => $obj) {
8✔
542
            // no typehint provided on collection key
543
            if (!$collectionKeyTypes) {
8✔
NEW
544
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext);
×
NEW
545
                continue;
×
546
            }
547

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

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

561
        return $values;
4✔
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
    {
573
        if (\is_string($value)) {
×
574
            try {
575
                return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
×
576
            } catch (ItemNotFoundException $e) {
×
UNCOV
577
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
578
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
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(),
×
UNCOV
587
                    $e
×
588
                );
×
589

590
                return null;
×
591
            } catch (InvalidArgumentException $e) {
×
UNCOV
592
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
593
                    throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
×
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(),
×
UNCOV
602
                    $e
×
603
                );
×
604

UNCOV
605
                return null;
×
606
            }
607
        }
608

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

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

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

UNCOV
621
            return $item;
×
622
        }
623

UNCOV
624
        if (!\is_array($value)) {
×
UNCOV
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

UNCOV
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 = [];
88✔
637
        if (isset($context[self::GROUPS])) {
88✔
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];
12✔
640
        }
641

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

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

658
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
88✔
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;
68✔
669
        $context['property_metadata'] = $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
68✔
670

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

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

677
        foreach ($types as $type) {
68✔
678
            if (
679
                $type->isCollection()
56✔
680
                && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
56✔
681
                && ($className = $collectionValueType->getClassName())
56✔
682
                && $this->resourceClassResolver->isResourceClass($className)
56✔
683
            ) {
684
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
16✔
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()) {
16✔
689
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
690
                        operationName: $itemUriTemplate,
×
691
                        forceCollection: true,
×
UNCOV
692
                        httpOperation: true
×
693
                    );
×
694

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

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

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

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

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

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

714
                return $data;
16✔
715
            }
716

717
            if (
718
                ($className = $type->getClassName())
56✔
719
                && $this->resourceClassResolver->isResourceClass($className)
56✔
720
            ) {
721
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
24✔
722
                unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
24✔
723

724
                if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
24✔
725
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
726
                        operationName: $uriTemplate,
×
UNCOV
727
                        httpOperation: true
×
728
                    );
×
729

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

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

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

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

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

745
                if ($this->tagCollector) {
24✔
746
                    $this->tagCollector->collect($context);
12✔
747
                }
748

749
                return $data;
24✔
750
            }
751

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

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

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

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

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

771
            if ('array' === $type->getBuiltinType()) {
52✔
772
                $childContext = $this->createChildContext($context, $attribute, $format);
20✔
773
                $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true;
20✔
774

775
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
20✔
776

777
                return $this->serializer->normalize($attributeValue, $format, $childContext);
20✔
778
            }
779
        }
780

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

785
        unset($context['resource_class']);
64✔
786
        unset($context['force_resource_class']);
64✔
787

788
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
64✔
789

790
        return $this->serializer->normalize($attributeValue, $format, $context);
60✔
791
    }
792

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

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

UNCOV
813
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
×
814
        }
815

816
        return $value;
16✔
817
    }
818

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

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

838
            return $normalizedRelatedObject;
16✔
839
        }
840

841
        $context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
8✔
842
        $context['data'] = $iri;
8✔
843
        $context['object'] = $relatedObject;
8✔
844
        unset($context['property_metadata']);
8✔
845
        unset($context['api_attribute']);
8✔
846

847
        if ($this->tagCollector) {
8✔
848
            $this->tagCollector->collect($context);
×
849
        } elseif (isset($context['resources'])) {
8✔
UNCOV
850
            $context['resources'][$iri] = $iri;
×
851
        }
852

853
        $push = $propertyMetadata->getPush() ?? false;
8✔
854
        if (isset($context['resources_to_push']) && $push) {
8✔
UNCOV
855
            $context['resources_to_push'][$iri] = $iri;
×
856
        }
857

858
        return $iri;
8✔
859
    }
860

861
    private function createAttributeValue(string $attribute, mixed $value, ?string $format = null, array &$context = []): mixed
862
    {
863
        try {
864
            return $this->createAndValidateAttributeValue($attribute, $value, $format, $context);
28✔
865
        } catch (NotNormalizableValueException $exception) {
12✔
866
            if (!isset($context['not_normalizable_value_exceptions'])) {
8✔
867
                throw $exception;
8✔
868
            }
869
            $context['not_normalizable_value_exceptions'][] = $exception;
×
870

UNCOV
871
            throw $exception;
×
872
        }
873
    }
874

875
    private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed
876
    {
877
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
28✔
878
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
28✔
879
        $isMultipleTypes = \count($types) > 1;
28✔
880

881
        foreach ($types as $type) {
28✔
882
            if (null === $value && ($type->isNullable() || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
28✔
UNCOV
883
                return $value;
×
884
            }
885

886
            $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
28✔
887

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

895
            if (
896
                $type->isCollection()
28✔
897
                && null !== $collectionValueType
28✔
898
                && null !== ($className = $collectionValueType->getClassName())
28✔
899
                && $this->resourceClassResolver->isResourceClass($className)
28✔
900
            ) {
901
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
12✔
902
                $context['resource_class'] = $resourceClass;
12✔
903

904
                return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
12✔
905
            }
906

907
            if (
908
                null !== ($className = $type->getClassName())
20✔
909
                && $this->resourceClassResolver->isResourceClass($className)
20✔
910
            ) {
911
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
8✔
912
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
8✔
913

914
                return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
8✔
915
            }
916

917
            if (
918
                $type->isCollection()
16✔
919
                && null !== $collectionValueType
16✔
920
                && null !== ($className = $collectionValueType->getClassName())
16✔
921
                && \is_array($value)
16✔
922
            ) {
UNCOV
923
                if (!$this->serializer instanceof DenormalizerInterface) {
×
UNCOV
924
                    throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
925
                }
926

927
                unset($context['resource_class']);
×
928

UNCOV
929
                return $this->serializer->denormalize($value, $className.'[]', $format, $context);
×
930
            }
931

932
            if (null !== $className = $type->getClassName()) {
16✔
UNCOV
933
                if (!$this->serializer instanceof DenormalizerInterface) {
×
UNCOV
934
                    throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
935
                }
936

937
                unset($context['resource_class']);
×
938

UNCOV
939
                return $this->serializer->denormalize($value, $className, $format, $context);
×
940
            }
941

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

UNCOV
951
                switch ($type->getBuiltinType()) {
×
952
                    case Type::BUILTIN_TYPE_BOOL:
953
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
954
                        if ('false' === $value || '0' === $value) {
×
955
                            $value = false;
×
UNCOV
956
                        } elseif ('true' === $value || '1' === $value) {
×
UNCOV
957
                            $value = true;
×
958
                        } else {
959
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
960
                            if ($isMultipleTypes) {
×
961
                                break 2;
×
962
                            }
963
                            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);
×
964
                        }
965
                        break;
×
966
                    case Type::BUILTIN_TYPE_INT:
UNCOV
967
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
×
UNCOV
968
                            $value = (int) $value;
×
969
                        } else {
970
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
971
                            if ($isMultipleTypes) {
×
972
                                break 2;
×
973
                            }
974
                            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);
×
975
                        }
976
                        break;
×
977
                    case Type::BUILTIN_TYPE_FLOAT:
UNCOV
978
                        if (is_numeric($value)) {
×
UNCOV
979
                            return (float) $value;
×
980
                        }
981

982
                        switch ($value) {
983
                            case 'NaN':
×
984
                                return \NAN;
×
985
                            case 'INF':
×
986
                                return \INF;
×
UNCOV
987
                            case '-INF':
×
UNCOV
988
                                return -\INF;
×
989
                            default:
990
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
991
                                if ($isMultipleTypes) {
×
992
                                    break 3;
×
993
                                }
UNCOV
994
                                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);
×
995
                        }
996
                }
997
            }
998

999
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
16✔
UNCOV
1000
                return $value;
×
1001
            }
1002

1003
            try {
1004
                $this->validateType($attribute, $type, $value, $format, $context);
16✔
1005

1006
                break;
16✔
1007
            } catch (NotNormalizableValueException $e) {
×
1008
                // union/intersect types: try the next type
UNCOV
1009
                if (!$isMultipleTypes) {
×
UNCOV
1010
                    throw $e;
×
1011
                }
1012
            }
1013
        }
1014

1015
        return $value;
16✔
1016
    }
1017

1018
    /**
1019
     * Sets a value of the object using the PropertyAccess component.
1020
     */
1021
    private function setValue(object $object, string $attributeName, mixed $value): void
1022
    {
1023
        try {
1024
            $this->propertyAccessor->setValue($object, $attributeName, $value);
16✔
1025
        } catch (NoSuchPropertyException) {
4✔
1026
            // Properties not found are ignored
1027
        }
1028
    }
1029
}
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