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

api-platform / core / 10749392760

07 Sep 2024 06:39AM UTC coverage: 7.646%. Remained the same
10749392760

push

github

soyuka
test: remove hydra prefix from guides

0 of 21 new or added lines in 6 files covered. (0.0%)

6834 existing lines in 225 files now uncovered.

12538 of 163980 relevant lines covered (7.65%)

22.78 hits per line

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

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

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

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

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

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

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

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

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

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

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

UNCOV
128
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
1,923✔
UNCOV
129
            $context = $this->initContext($resourceClass, $context);
1,881✔
130
        }
131

UNCOV
132
        $context['api_normalize'] = true;
1,923✔
UNCOV
133
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
1,923✔
134

135
        /*
136
         * When true, converts the normalized data array of a resource into an
137
         * IRI, if the normalized data array is empty.
138
         *
139
         * This is useful when traversing from a non-resource towards an attribute
140
         * which is a resource, as we do not have the benefit of {@see ApiProperty::isReadableLink}.
141
         *
142
         * It must not be propagated to resources, as {@see ApiProperty::isReadableLink}
143
         * should take effect.
144
         */
UNCOV
145
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
1,923✔
UNCOV
146
        unset($context['api_empty_resource_as_iri']);
1,923✔
147

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

UNCOV
152
        $context['object'] = $object;
1,923✔
UNCOV
153
        $context['format'] = $format;
1,923✔
154

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

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

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

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

167
            return $iri;
×
168
        }
169

UNCOV
170
        if ($this->tagCollector) {
1,923✔
UNCOV
171
            $this->tagCollector->collect($context);
1,644✔
172
        }
173

UNCOV
174
        return $data;
1,923✔
175
    }
176

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

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

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

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

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

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

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

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

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

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

UNCOV
233
        if (!\is_array($data)) {
639✔
UNCOV
234
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource must be "array" (nested document) or "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null);
3✔
235
        }
236

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

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

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

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

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

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

UNCOV
270
        return $object;
598✔
271
    }
272

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

UNCOV
286
            return $object;
137✔
287
        }
288

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

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

UNCOV
296
            $params = [];
231✔
UNCOV
297
            $missingConstructorArguments = [];
231✔
UNCOV
298
            foreach ($constructorParameters as $constructorParameter) {
231✔
UNCOV
299
                $paramName = $constructorParameter->name;
32✔
UNCOV
300
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
32✔
301

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

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

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

UNCOV
335
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name), $data, ['unknown'], $context['deserialization_path'] ?? null, true);
4✔
UNCOV
336
                    $context['not_normalizable_value_exceptions'][] = $exception;
4✔
337
                }
338
            }
339

UNCOV
340
            if ($missingConstructorArguments) {
230✔
UNCOV
341
                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);
3✔
342
            }
343

UNCOV
344
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
230✔
345
                return $reflectionClass->newInstanceWithoutConstructor();
1✔
346
            }
347

UNCOV
348
            if ($constructor->isConstructor()) {
229✔
UNCOV
349
                return $reflectionClass->newInstanceArgs($params);
229✔
350
            }
351

352
            return $constructor->invokeArgs(null, $params);
×
353
        }
354

UNCOV
355
        return new $class();
304✔
356
    }
357

358
    protected function getClassDiscriminatorResolvedClass(array $data, string $class, array $context = []): string
359
    {
UNCOV
360
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
524✔
UNCOV
361
            return $class;
524✔
362
        }
363

UNCOV
364
        if (!isset($data[$mapping->getTypeProperty()])) {
3✔
365
            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());
×
366
        }
367

UNCOV
368
        $type = $data[$mapping->getTypeProperty()];
3✔
UNCOV
369
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
3✔
370
            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);
×
371
        }
372

UNCOV
373
        return $mappedClass;
3✔
374
    }
375

376
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, ?string $format = null): mixed
377
    {
UNCOV
378
        return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
28✔
379
    }
380

381
    /**
382
     * {@inheritdoc}
383
     *
384
     * Unused in this context.
385
     *
386
     * @return string[]
387
     */
388
    protected function extractAttributes($object, $format = null, array $context = []): array
389
    {
390
        return [];
×
391
    }
392

393
    /**
394
     * {@inheritdoc}
395
     */
396
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
397
    {
UNCOV
398
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
1,942✔
UNCOV
399
            return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
46✔
400
        }
401

UNCOV
402
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
1,903✔
UNCOV
403
        $options = $this->getFactoryOptions($context);
1,903✔
UNCOV
404
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
1,903✔
405

UNCOV
406
        $allowedAttributes = [];
1,903✔
UNCOV
407
        foreach ($propertyNames as $propertyName) {
1,903✔
UNCOV
408
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
1,894✔
409

410
            if (
UNCOV
411
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
1,894✔
UNCOV
412
                && (isset($context['api_normalize']) && $propertyMetadata->isReadable()
1,894✔
UNCOV
413
                    || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
1,894✔
414
                )
415
            ) {
UNCOV
416
                $allowedAttributes[] = $propertyName;
1,882✔
417
            }
418
        }
419

UNCOV
420
        return $allowedAttributes;
1,903✔
421
    }
422

423
    /**
424
     * {@inheritdoc}
425
     */
426
    protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
427
    {
UNCOV
428
        if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
1,933✔
UNCOV
429
            return false;
306✔
430
        }
431

UNCOV
432
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
1,930✔
433
    }
434

435
    /**
436
     * Check if access to the attribute is granted.
437
     */
438
    protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
439
    {
UNCOV
440
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
1,930✔
UNCOV
441
            return true;
46✔
442
        }
443

UNCOV
444
        $options = $this->getFactoryOptions($context);
1,891✔
UNCOV
445
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
1,891✔
UNCOV
446
        $security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy();
1,891✔
UNCOV
447
        if (null !== $this->resourceAccessChecker && $security) {
1,891✔
UNCOV
448
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
93✔
UNCOV
449
                'object' => $object,
93✔
UNCOV
450
                'property' => $attribute,
93✔
UNCOV
451
            ]);
93✔
452
        }
453

UNCOV
454
        return true;
1,879✔
455
    }
456

457
    /**
458
     * Check if access to the attribute is granted.
459
     */
460
    protected function canAccessAttributePostDenormalize(?object $object, ?object $previousObject, string $attribute, array $context = []): bool
461
    {
UNCOV
462
        $options = $this->getFactoryOptions($context);
551✔
UNCOV
463
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
551✔
UNCOV
464
        $security = $propertyMetadata->getSecurityPostDenormalize();
551✔
UNCOV
465
        if ($this->resourceAccessChecker && $security) {
551✔
UNCOV
466
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
14✔
UNCOV
467
                'object' => $object,
14✔
UNCOV
468
                'previous_object' => $previousObject,
14✔
UNCOV
469
                'property' => $attribute,
14✔
UNCOV
470
            ]);
14✔
471
        }
472

UNCOV
473
        return true;
548✔
474
    }
475

476
    /**
477
     * {@inheritdoc}
478
     */
479
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
480
    {
481
        try {
UNCOV
482
            $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
555✔
UNCOV
483
        } catch (NotNormalizableValueException $exception) {
41✔
484
            // Only throw if collecting denormalization errors is disabled.
UNCOV
485
            if (!isset($context['not_normalizable_value_exceptions'])) {
29✔
UNCOV
486
                throw $exception;
28✔
487
            }
488
        }
489
    }
490

491
    /**
492
     * Validates the type of the value. Allows using integers as floats for JSON formats.
493
     *
494
     * @throws NotNormalizableValueException
495
     */
496
    protected function validateType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void
497
    {
UNCOV
498
        $builtinType = $type->getBuiltinType();
485✔
UNCOV
499
        if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
485✔
UNCOV
500
            $isValid = \is_float($value) || \is_int($value);
3✔
501
        } else {
UNCOV
502
            $isValid = \call_user_func('is_'.$builtinType, $value);
485✔
503
        }
504

UNCOV
505
        if (!$isValid) {
485✔
UNCOV
506
            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);
21✔
507
        }
508
    }
509

510
    /**
511
     * Denormalizes a collection of objects.
512
     *
513
     * @throws NotNormalizableValueException
514
     */
515
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
516
    {
UNCOV
517
        if (!\is_array($value)) {
44✔
UNCOV
518
            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✔
519
        }
520

UNCOV
521
        $values = [];
40✔
UNCOV
522
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
40✔
UNCOV
523
        $collectionKeyTypes = $type->getCollectionKeyTypes();
40✔
UNCOV
524
        foreach ($value as $index => $obj) {
40✔
UNCOV
525
            $currentChildContext = $childContext;
40✔
UNCOV
526
            if (isset($childContext['deserialization_path'])) {
40✔
UNCOV
527
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
40✔
528
            }
529

530
            // no typehint provided on collection key
UNCOV
531
            if (!$collectionKeyTypes) {
40✔
532
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
533
                continue;
×
534
            }
535

536
            // validate collection key typehint
UNCOV
537
            foreach ($collectionKeyTypes as $collectionKeyType) {
40✔
UNCOV
538
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
40✔
UNCOV
539
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
40✔
UNCOV
540
                    continue;
3✔
541
                }
542

UNCOV
543
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
37✔
UNCOV
544
                continue 2;
37✔
545
            }
UNCOV
546
            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);
3✔
547
        }
548

UNCOV
549
        return $values;
37✔
550
    }
551

552
    /**
553
     * Denormalizes a relation.
554
     *
555
     * @throws LogicException
556
     * @throws UnexpectedValueException
557
     * @throws NotNormalizableValueException
558
     */
559
    protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object
560
    {
UNCOV
561
        if (\is_string($value)) {
154✔
562
            try {
UNCOV
563
                return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
84✔
UNCOV
564
            } catch (ItemNotFoundException $e) {
9✔
UNCOV
565
                if (!isset($context['not_normalizable_value_exceptions'])) {
3✔
UNCOV
566
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
3✔
567
                }
568
                $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType(
×
569
                    $e->getMessage(),
×
570
                    $value,
×
571
                    [$className],
×
572
                    $context['deserialization_path'] ?? null,
×
573
                    true,
×
574
                    $e->getCode(),
×
575
                    $e
×
576
                );
×
577

578
                return null;
×
UNCOV
579
            } catch (InvalidArgumentException $e) {
6✔
UNCOV
580
                if (!isset($context['not_normalizable_value_exceptions'])) {
6✔
UNCOV
581
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
6✔
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
            }
595
        }
596

UNCOV
597
        if ($propertyMetadata->isWritableLink()) {
73✔
UNCOV
598
            $context['api_allow_update'] = true;
71✔
599

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

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

UNCOV
609
            return $item;
65✔
610
        }
611

612
        if (!\is_array($value)) {
2✔
613
            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);
1✔
614
        }
615

616
        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);
1✔
617
    }
618

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
698
                if ($this->tagCollector) {
612✔
UNCOV
699
                    $this->tagCollector->collect($context);
554✔
700
                }
701

UNCOV
702
                return $data;
612✔
703
            }
704

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

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

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

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

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

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

UNCOV
732
                if ($this->tagCollector) {
852✔
UNCOV
733
                    $this->tagCollector->collect($context);
803✔
734
                }
735

UNCOV
736
                return $data;
852✔
737
            }
738

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

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

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

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

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

UNCOV
759
            if ('array' === $type->getBuiltinType()) {
1,821✔
UNCOV
760
                if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
542✔
UNCOV
761
                    $context = $this->createOperationContext($context, $className);
11✔
762
                }
763

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

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

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

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

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

UNCOV
783
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
1,861✔
784

UNCOV
785
        return $this->serializer->normalize($attributeValue, $format, $context);
1,860✔
786
    }
787

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

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

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

UNCOV
811
        return $value;
554✔
812
    }
813

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

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

UNCOV
833
            return $normalizedRelatedObject;
598✔
834
        }
835

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

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

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

UNCOV
852
        return $iri;
238✔
853
    }
854

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

865
            throw $exception;
1✔
866
        }
867
    }
868

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

UNCOV
875
        foreach ($types as $type) {
582✔
UNCOV
876
            if (null === $value && ($type->isNullable() || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
572✔
UNCOV
877
                return $value;
7✔
878
            }
879

UNCOV
880
            $collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
571✔
881

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

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

UNCOV
899
                return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context);
44✔
900
            }
901

902
            if (
UNCOV
903
                null !== ($className = $type->getClassName())
559✔
UNCOV
904
                && $this->resourceClassResolver->isResourceClass($className)
559✔
905
            ) {
UNCOV
906
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
150✔
UNCOV
907
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format);
150✔
908

UNCOV
909
                return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
150✔
910
            }
911

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

UNCOV
922
                unset($context['resource_class'], $context['uri_variables']);
9✔
923

UNCOV
924
                return $this->serializer->denormalize($value, $className.'[]', $format, $context);
9✔
925
            }
926

UNCOV
927
            if (null !== $className = $type->getClassName()) {
510✔
UNCOV
928
                if (!$this->serializer instanceof DenormalizerInterface) {
44✔
929
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
930
                }
931

UNCOV
932
                unset($context['resource_class'], $context['uri_variables']);
44✔
933

UNCOV
934
                return $this->serializer->denormalize($value, $className, $format, $context);
44✔
935
            }
936

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

UNCOV
946
                switch ($type->getBuiltinType()) {
45✔
UNCOV
947
                    case Type::BUILTIN_TYPE_BOOL:
45✔
948
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
UNCOV
949
                        if ('false' === $value || '0' === $value) {
12✔
UNCOV
950
                            $value = false;
6✔
UNCOV
951
                        } elseif ('true' === $value || '1' === $value) {
6✔
UNCOV
952
                            $value = true;
6✔
953
                        } else {
954
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
955
                            if ($isMultipleTypes) {
×
956
                                break 2;
×
957
                            }
958
                            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);
×
959
                        }
UNCOV
960
                        break;
12✔
UNCOV
961
                    case Type::BUILTIN_TYPE_INT:
33✔
UNCOV
962
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
12✔
UNCOV
963
                            $value = (int) $value;
12✔
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 int ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
×
970
                        }
UNCOV
971
                        break;
12✔
UNCOV
972
                    case Type::BUILTIN_TYPE_FLOAT:
21✔
UNCOV
973
                        if (is_numeric($value)) {
12✔
UNCOV
974
                            return (float) $value;
3✔
975
                        }
976

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

UNCOV
994
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
485✔
995
                return $value;
×
996
            }
997

998
            try {
UNCOV
999
                $this->validateType($attribute, $type, $value, $format, $context);
485✔
1000

UNCOV
1001
                break;
473✔
UNCOV
1002
            } catch (NotNormalizableValueException $e) {
21✔
1003
                // union/intersect types: try the next type
UNCOV
1004
                if (!$isMultipleTypes) {
21✔
UNCOV
1005
                    throw $e;
12✔
1006
                }
1007
            }
1008
        }
1009

UNCOV
1010
        return $value;
483✔
1011
    }
1012

1013
    /**
1014
     * Sets a value of the object using the PropertyAccess component.
1015
     */
1016
    private function setValue(object $object, string $attributeName, mixed $value): void
1017
    {
1018
        try {
UNCOV
1019
            $this->propertyAccessor->setValue($object, $attributeName, $value);
530✔
UNCOV
1020
        } catch (NoSuchPropertyException) {
25✔
1021
            // Properties not found are ignored
1022
        }
1023
    }
1024
}
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