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

api-platform / core / 9663895513

25 Jun 2024 02:01PM UTC coverage: 62.122% (-0.5%) from 62.637%
9663895513

push

github

web-flow
feat(laravel): laravel component (#5882)

* feat(laravel): laravel component

* try to skip laravel

* feat(jsonapi): component

* feat(laravel): json api support (needs review)

* work on relations

* relations (needs toMany) + skolem + IRI to resource

* links handler

* ulid

* validation

* slug post

* remove deprecations

* move classes

* fix tests

* fix tests metadata

* phpstan

* missing class

* fix laravel tests

* fix stan

33 of 77 new or added lines in 20 files covered. (42.86%)

140 existing lines in 15 files now uncovered.

10862 of 17485 relevant lines covered (62.12%)

59.64 hits per line

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

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

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\Serializer;
15

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

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

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

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

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

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

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

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

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

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

115
        return true;
×
116
    }
117

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

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

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

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

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

151
        $context['api_normalize'] = true;
292✔
152
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
292✔
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;
292✔
165
        unset($context['api_empty_resource_as_iri']);
292✔
166

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

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

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

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

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

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

187
            return $iri;
×
188
        }
189

190
        if ($this->tagCollector) {
292✔
191
            $this->tagCollector->collect($context);
240✔
192
        }
193

194
        return $data;
292✔
195
    }
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

290
        return $object;
12✔
291
    }
292

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

306
            return $object;
×
307
        }
308

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

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

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

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

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

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

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

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

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

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

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

375
        return new $class();
×
376
    }
377

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

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

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

393
        return $mappedClass;
×
394
    }
395

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

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

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

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

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

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

440
        return $allowedAttributes;
276✔
441
    }
442

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

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

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

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

474
        return true;
276✔
475
    }
476

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

493
        return true;
12✔
494
    }
495

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

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

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

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

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

551
            // validate collection key typehint
UNCOV
552
            foreach ($collectionKeyTypes as $collectionKeyType) {
×
UNCOV
553
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
×
UNCOV
554
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
×
UNCOV
555
                    continue;
×
556
                }
557

UNCOV
558
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext);
×
UNCOV
559
                continue 2;
×
560
            }
UNCOV
561
            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);
×
562
        }
563

UNCOV
564
        return $values;
×
565
    }
566

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

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

608
                return null;
×
609
            }
610
        }
611

612
        if ($propertyMetadata->isWritableLink()) {
×
613
            $context['api_allow_update'] = true;
×
614

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

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

624
            return $item;
×
625
        }
626

627
        if (!\is_array($value)) {
×
628
            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);
×
629
        }
630

631
        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);
×
632
    }
633

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

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

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

661
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
292✔
662
    }
663

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

674
        if ($context['api_denormalize'] ?? false) {
280✔
675
            return $this->propertyAccessor->getValue($object, $attribute);
×
676
        }
677

678
        $types = $propertyMetadata->getBuiltinTypes() ?? [];
280✔
679

680
        foreach ($types as $type) {
280✔
681
            if (
682
                $type->isCollection()
276✔
683
                && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
276✔
684
                && ($className = $collectionValueType->getClassName())
276✔
685
                && $this->resourceClassResolver->isResourceClass($className)
276✔
686
            ) {
687
                $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
24✔
688

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

698
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
699
                }
700

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

703
                if (!is_iterable($attributeValue)) {
24✔
704
                    throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
705
                }
706

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

709
                $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
24✔
710
                $context['data'] = $data;
24✔
711
                $context['type'] = $type;
24✔
712

713
                if ($this->tagCollector) {
24✔
714
                    $this->tagCollector->collect($context);
24✔
715
                }
716

717
                return $data;
24✔
718
            }
719

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

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

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

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

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

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

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

751
                return $data;
24✔
752
            }
753

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

758
            unset(
276✔
759
                $context['resource_class'],
276✔
760
                $context['force_resource_class'],
276✔
761
            );
276✔
762

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

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

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

773
            if ('array' === $type->getBuiltinType()) {
268✔
774
                if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
44✔
775
                    $context = $this->createOperationContext($context, $className);
8✔
776
                }
777

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

781
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
44✔
782

783
                return $this->serializer->normalize($attributeValue, $format, $childContext);
44✔
784
            }
785
        }
786

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

791
        unset($context['resource_class']);
272✔
792
        unset($context['force_resource_class']);
272✔
793

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

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

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

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

819
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
×
820
        }
821

822
        return $value;
24✔
823
    }
824

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

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

844
            return $normalizedRelatedObject;
16✔
845
        }
846

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

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

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

864
        return $iri;
8✔
865
    }
866

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

877
            throw $exception;
×
878
        }
879
    }
880

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

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

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

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

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

UNCOV
910
                return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
×
911
            }
912

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

UNCOV
920
                return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
×
921
            }
922

923
            if (
924
                $type->isCollection()
12✔
925
                && null !== $collectionValueType
12✔
926
                && null !== ($className = $collectionValueType->getClassName())
12✔
927
                && \is_array($value)
12✔
928
            ) {
929
                if (!$this->serializer instanceof DenormalizerInterface) {
×
930
                    throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
931
                }
932

933
                unset($context['resource_class']);
×
934

935
                return $this->serializer->denormalize($value, $className.'[]', $format, $context);
×
936
            }
937

938
            if (null !== $className = $type->getClassName()) {
12✔
939
                if (!$this->serializer instanceof DenormalizerInterface) {
×
940
                    throw new LogicException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
941
                }
942

943
                unset($context['resource_class']);
×
944

945
                return $this->serializer->denormalize($value, $className, $format, $context);
×
946
            }
947

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

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

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

1005
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
12✔
1006
                return $value;
×
1007
            }
1008

1009
            try {
1010
                $this->validateType($attribute, $type, $value, $format, $context);
12✔
1011

1012
                break;
12✔
1013
            } catch (NotNormalizableValueException $e) {
×
1014
                // union/intersect types: try the next type
1015
                if (!$isMultipleTypes) {
×
1016
                    throw $e;
×
1017
                }
1018
            }
1019
        }
1020

1021
        return $value;
12✔
1022
    }
1023

1024
    /**
1025
     * Sets a value of the object using the PropertyAccess component.
1026
     */
1027
    private function setValue(object $object, string $attributeName, mixed $value): void
1028
    {
1029
        try {
1030
            $this->propertyAccessor->setValue($object, $attributeName, $value);
12✔
UNCOV
1031
        } catch (NoSuchPropertyException) {
×
1032
            // Properties not found are ignored
1033
        }
1034
    }
1035
}
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