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

api-platform / core / 16347491392

17 Jul 2025 02:11PM UTC coverage: 22.039% (+0.03%) from 22.009%
16347491392

push

github

soyuka
Merge 4.1

17 of 23 new or added lines in 7 files covered. (73.91%)

206 existing lines in 8 files now uncovered.

11533 of 52331 relevant lines covered (22.04%)

23.45 hits per line

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

57.54
/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\AccessDeniedException;
19
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
20
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
21
use ApiPlatform\Metadata\IriConverterInterface;
22
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
23
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
24
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
25
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
26
use ApiPlatform\Metadata\ResourceClassResolverInterface;
27
use ApiPlatform\Metadata\UrlGeneratorInterface;
28
use ApiPlatform\Metadata\Util\ClassInfoTrait;
29
use ApiPlatform\Metadata\Util\CloneTrait;
30
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
31
use Symfony\Component\PropertyAccess\PropertyAccess;
32
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
33
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
34
use Symfony\Component\PropertyInfo\Type as LegacyType;
35
use Symfony\Component\Serializer\Encoder\CsvEncoder;
36
use Symfony\Component\Serializer\Encoder\XmlEncoder;
37
use Symfony\Component\Serializer\Exception\LogicException;
38
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
39
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
40
use Symfony\Component\Serializer\Exception\RuntimeException;
41
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
42
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
43
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
44
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
45
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
46
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
47
use Symfony\Component\TypeInfo\Type;
48
use Symfony\Component\TypeInfo\Type\BuiltinType;
49
use Symfony\Component\TypeInfo\Type\CollectionType;
50
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
51
use Symfony\Component\TypeInfo\Type\NullableType;
52
use Symfony\Component\TypeInfo\Type\ObjectType;
53
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
54
use Symfony\Component\TypeInfo\TypeIdentifier;
55

56
/**
57
 * Base item normalizer.
58
 *
59
 * @author Kévin Dunglas <dunglas@gmail.com>
60
 */
61
abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
62
{
63
    use ClassInfoTrait;
64
    use CloneTrait;
65
    use ContextTrait;
66
    use InputOutputMetadataTrait;
67
    use OperationContextTrait;
68

69
    protected PropertyAccessorInterface $propertyAccessor;
70
    protected array $localCache = [];
71
    protected array $localFactoryOptionsCache = [];
72
    protected ?ResourceAccessCheckerInterface $resourceAccessChecker;
73

74
    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)
75
    {
76
        if (!isset($defaultContext['circular_reference_handler'])) {
610✔
77
            $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
598✔
78
        }
79

80
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable($this->getObjectClass(...)), $defaultContext);
610✔
81
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
610✔
82
        $this->resourceAccessChecker = $resourceAccessChecker;
610✔
83
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
610✔
84
    }
85

86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
90
    {
91
        if (!\is_object($data) || is_iterable($data)) {
492✔
92
            return false;
226✔
93
        }
94

95
        $class = $context['force_resource_class'] ?? $this->getObjectClass($data);
480✔
96
        if (($context['output']['class'] ?? null) === $class) {
480✔
97
            return true;
12✔
98
        }
99

100
        return $this->resourceClassResolver->isResourceClass($class);
476✔
101
    }
102

103
    public function getSupportedTypes(?string $format): array
104
    {
105
        return [
502✔
106
            'object' => true,
502✔
107
        ];
502✔
108
    }
109

110
    /**
111
     * {@inheritdoc}
112
     *
113
     * @throws LogicException
114
     */
115
    public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
116
    {
117
        $resourceClass = $context['force_resource_class'] ?? $this->getObjectClass($object);
488✔
118
        if ($outputClass = $this->getOutputClass($context)) {
488✔
119
            if (!$this->serializer instanceof NormalizerInterface) {
12✔
120
                throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
×
121
            }
122

123
            unset($context['output'], $context['operation'], $context['operation_name']);
12✔
124
            $context['resource_class'] = $outputClass;
12✔
125
            $context['api_sub_level'] = true;
12✔
126
            $context[self::ALLOW_EXTRA_ATTRIBUTES] = false;
12✔
127

128
            return $this->serializer->normalize($object, $format, $context);
12✔
129
        }
130

131
        // Never remove this, with `application/json` we don't use our AbstractCollectionNormalizer and we need
132
        // to remove the collection operation from our context or we'll introduce security issues
133
        if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
488✔
134
            unset($context['operation_name'], $context['operation'], $context['iri']);
4✔
135
        }
136

137
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
488✔
138
            $context = $this->initContext($resourceClass, $context);
476✔
139
        }
140

141
        $context['api_normalize'] = true;
488✔
142
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
488✔
143

144
        /*
145
         * When true, converts the normalized data array of a resource into an
146
         * IRI, if the normalized data array is empty.
147
         *
148
         * This is useful when traversing from a non-resource towards an attribute
149
         * which is a resource, as we do not have the benefit of {@see ApiProperty::isReadableLink}.
150
         *
151
         * It must not be propagated to resources, as {@see ApiProperty::isReadableLink}
152
         * should take effect.
153
         */
154
        $emptyResourceAsIri = $context['api_empty_resource_as_iri'] ?? false;
488✔
155
        unset($context['api_empty_resource_as_iri']);
488✔
156

157
        if (!$this->tagCollector && isset($context['resources'])) {
488✔
158
            $context['resources'][$iri] = $iri;
×
159
        }
160

161
        $context['object'] = $object;
488✔
162
        $context['format'] = $format;
488✔
163

164
        $data = parent::normalize($object, $format, $context);
488✔
165

166
        $context['data'] = $data;
488✔
167
        unset($context['property_metadata'], $context['api_attribute']);
488✔
168

169
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
488✔
170
            $context['data'] = $iri;
×
171

172
            if ($this->tagCollector) {
×
173
                $this->tagCollector->collect($context);
×
174
            }
175

176
            return $iri;
×
177
        }
178

179
        if ($this->tagCollector) {
488✔
180
            $this->tagCollector->collect($context);
458✔
181
        }
182

183
        return $data;
488✔
184
    }
185

186
    /**
187
     * {@inheritdoc}
188
     */
189
    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
190
    {
191
        if (($context['input']['class'] ?? null) === $type) {
20✔
192
            return true;
×
193
        }
194

195
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
20✔
196
    }
197

198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function denormalize(mixed $data, string $class, ?string $format = null, array $context = []): mixed
202
    {
203
        $resourceClass = $class;
20✔
204

205
        if ($inputClass = $this->getInputClass($context)) {
20✔
206
            if (!$this->serializer instanceof DenormalizerInterface) {
2✔
207
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
×
208
            }
209

210
            unset($context['input'], $context['operation'], $context['operation_name'], $context['uri_variables']);
2✔
211
            $context['resource_class'] = $inputClass;
2✔
212

213
            try {
214
                return $this->serializer->denormalize($data, $inputClass, $format, $context);
2✔
215
            } catch (NotNormalizableValueException $e) {
×
216
                throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
×
217
            }
218
        }
219

220
        if (null === $objectToPopulate = $this->extractObjectToPopulate($resourceClass, $context, static::OBJECT_TO_POPULATE)) {
20✔
221
            $normalizedData = \is_scalar($data) ? [$data] : $this->prepareForDenormalization($data);
20✔
222
            $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class, $context);
20✔
223
        }
224

225
        $context['api_denormalize'] = true;
20✔
226

227
        if ($this->resourceClassResolver->isResourceClass($class)) {
20✔
228
            $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
20✔
229
            $context['resource_class'] = $resourceClass;
20✔
230
        }
231

232
        if (\is_string($data)) {
20✔
233
            try {
234
                return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
2✔
235
            } catch (ItemNotFoundException $e) {
×
236
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
237
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
238
                }
239

240
                throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
241
            } catch (InvalidArgumentException $e) {
×
242
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
243
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e);
×
244
                }
245

246
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $data), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
247
            }
248
        }
249

250
        if (!\is_array($data)) {
18✔
251
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource must be "array" (nested document) or "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, ['array', 'string'], $context['deserialization_path'] ?? null);
×
252
        }
253

254
        $previousObject = $this->clone($objectToPopulate);
18✔
255
        $object = parent::denormalize($data, $class, $format, $context);
18✔
256

257
        if (!$this->resourceClassResolver->isResourceClass($class)) {
16✔
258
            return $object;
×
259
        }
260

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

267
        $options = $this->getFactoryOptions($context);
16✔
268
        $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options));
16✔
269

270
        $operation = $context['operation'] ?? null;
16✔
271
        $throwOnAccessDenied = $operation?->getExtraProperties()['throw_on_access_denied'] ?? false;
16✔
272
        $securityMessage = $operation?->getSecurityMessage() ?? null;
16✔
273

274
        // Revert attributes that aren't allowed to be changed after a post-denormalize check
275
        foreach (array_keys($data) as $attribute) {
16✔
276
            $attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute;
14✔
277
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
14✔
278
            $attributeExtraProperties = $propertyMetadata->getExtraProperties();
14✔
279
            $throwOnPropertyAccessDenied = $attributeExtraProperties['throw_on_access_denied'] ?? $throwOnAccessDenied;
14✔
280
            if (!\in_array($attribute, $propertyNames, true)) {
14✔
281
                continue;
×
282
            }
283

284
            if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
14✔
285
                if ($throwOnPropertyAccessDenied) {
×
286
                    throw new AccessDeniedException($securityMessage ?? 'Access denied');
×
287
                }
288
                if (null !== $previousObject) {
×
289
                    $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute));
×
290
                } else {
291
                    $this->setValue($object, $attribute, $propertyMetadata->getDefault());
×
292
                }
293
            }
294
        }
295

296
        return $object;
16✔
297
    }
298

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

312
            return $object;
2✔
313
        }
314

315
        $class = $this->getClassDiscriminatorResolvedClass($data, $class, $context);
18✔
316
        $reflectionClass = new \ReflectionClass($class);
18✔
317

318
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
18✔
319
        if ($constructor) {
18✔
320
            $constructorParameters = $constructor->getParameters();
14✔
321

322
            $params = [];
14✔
323
            $missingConstructorArguments = [];
14✔
324
            foreach ($constructorParameters as $constructorParameter) {
14✔
325
                $paramName = $constructorParameter->name;
10✔
326
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
10✔
327
                $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
10✔
328
                $attributeContext['deserialization_path'] = $attributeContext['deserialization_path'] ?? $key;
10✔
329

330
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
10✔
331
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
10✔
332
                if ($constructorParameter->isVariadic()) {
10✔
333
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
×
334
                        if (!\is_array($data[$paramName])) {
×
335
                            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));
×
336
                        }
337

338
                        $params[] = $data[$paramName];
×
339
                    }
340
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
10✔
341
                    try {
342
                        $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $attributeContext, $format);
4✔
343
                    } catch (NotNormalizableValueException $exception) {
2✔
344
                        if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
345
                            throw $exception;
×
346
                        }
347
                        $context['not_normalizable_value_exceptions'][] = $exception;
2✔
348
                    }
349

350
                    // Don't run set for a parameter passed to the constructor
351
                    unset($data[$key]);
4✔
352
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
8✔
353
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
×
354
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
8✔
355
                    $params[] = $constructorParameter->getDefaultValue();
6✔
356
                } else {
357
                    if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
358
                        $missingConstructorArguments[] = $constructorParameter->name;
×
359
                    }
360

361
                    $constructorParameterType = 'unknown';
2✔
362
                    $reflectionType = $constructorParameter->getType();
2✔
363
                    if ($reflectionType instanceof \ReflectionNamedType) {
2✔
364
                        $constructorParameterType = $reflectionType->getName();
2✔
365
                    }
366

367
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(
2✔
368
                        \sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
2✔
369
                        null,
2✔
370
                        [$constructorParameterType],
2✔
371
                        $attributeContext['deserialization_path'],
2✔
372
                        true
2✔
373
                    );
2✔
374
                    $context['not_normalizable_value_exceptions'][] = $exception;
2✔
375
                }
376
            }
377

378
            if ($missingConstructorArguments) {
14✔
379
                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);
×
380
            }
381

382
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
14✔
383
                return $reflectionClass->newInstanceWithoutConstructor();
2✔
384
            }
385

386
            if ($constructor->isConstructor()) {
12✔
387
                return $reflectionClass->newInstanceArgs($params);
12✔
388
            }
389

390
            return $constructor->invokeArgs(null, $params);
×
391
        }
392

393
        return new $class();
4✔
394
    }
395

396
    protected function getClassDiscriminatorResolvedClass(array $data, string $class, array $context = []): string
397
    {
398
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
20✔
399
            return $class;
20✔
400
        }
401

402
        // @phpstan-ignore-next-line function.alreadyNarrowedType
403
        $defaultType = method_exists($mapping, 'getDefaultType') ? $mapping->getDefaultType() : null;
×
UNCOV
404
        if (!isset($data[$mapping->getTypeProperty()]) && null === $defaultType) {
×
UNCOV
405
            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());
×
406
        }
407

408
        $type = $data[$mapping->getTypeProperty()] ?? $defaultType;
×
UNCOV
409
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
×
UNCOV
410
            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);
×
411
        }
412

UNCOV
413
        return $mappedClass;
×
414
    }
415

416
    protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array $context, ?string $format = null): mixed
417
    {
418
        return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
4✔
419
    }
420

421
    /**
422
     * {@inheritdoc}
423
     *
424
     * Unused in this context.
425
     *
426
     * @return string[]
427
     */
428
    protected function extractAttributes($object, $format = null, array $context = []): array
429
    {
UNCOV
430
        return [];
×
431
    }
432

433
    /**
434
     * {@inheritdoc}
435
     */
436
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
437
    {
438
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
488✔
439
            return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
12✔
440
        }
441

442
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
476✔
443
        $options = $this->getFactoryOptions($context);
476✔
444
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
476✔
445

446
        $allowedAttributes = [];
476✔
447
        foreach ($propertyNames as $propertyName) {
476✔
448
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
468✔
449

450
            if (
451
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
468✔
452
                && (isset($context['api_normalize']) && $propertyMetadata->isReadable()
468✔
453
                    || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
468✔
454
                )
455
            ) {
456
                $allowedAttributes[] = $propertyName;
462✔
457
            }
458
        }
459

460
        return $allowedAttributes;
476✔
461
    }
462

463
    /**
464
     * {@inheritdoc}
465
     */
466
    protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
467
    {
468
        if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
480✔
469
            return false;
16✔
470
        }
471

472
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
480✔
473
    }
474

475
    /**
476
     * Check if access to the attribute is granted.
477
     */
478
    protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
479
    {
480
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
480✔
481
            return true;
12✔
482
        }
483

484
        $options = $this->getFactoryOptions($context);
468✔
485
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
468✔
486
        $security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy();
468✔
487
        if (null !== $this->resourceAccessChecker && $security) {
468✔
488
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
2✔
489
                'object' => $object,
2✔
490
                'property' => $attribute,
2✔
491
            ]);
2✔
492
        }
493

494
        return true;
468✔
495
    }
496

497
    /**
498
     * Check if access to the attribute is granted.
499
     */
500
    protected function canAccessAttributePostDenormalize(?object $object, ?object $previousObject, string $attribute, array $context = []): bool
501
    {
502
        $options = $this->getFactoryOptions($context);
14✔
503
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
14✔
504
        $security = $propertyMetadata->getSecurityPostDenormalize();
14✔
505
        if ($this->resourceAccessChecker && $security) {
14✔
506
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
×
507
                'object' => $object,
×
508
                'previous_object' => $previousObject,
×
UNCOV
509
                'property' => $attribute,
×
UNCOV
510
            ]);
×
511
        }
512

513
        return true;
14✔
514
    }
515

516
    /**
517
     * {@inheritdoc}
518
     */
519
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
520
    {
521
        try {
522
            $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
14✔
523
        } catch (NotNormalizableValueException $exception) {
4✔
524
            // Only throw if collecting denormalization errors is disabled.
525
            if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
UNCOV
526
                throw $exception;
×
527
            }
528
        }
529
    }
530

531
    /**
532
     * @deprecated since 4.1, use "validateAttributeType" instead
533
     *
534
     * Validates the type of the value. Allows using integers as floats for JSON formats.
535
     *
536
     * @throws NotNormalizableValueException
537
     */
538
    protected function validateType(string $attribute, LegacyType $type, mixed $value, ?string $format = null, array $context = []): void
539
    {
540
        trigger_deprecation('api-platform/serializer', '4.1', 'The "%s()" method is deprecated, use "%s::validateAttributeType()" instead.', __METHOD__, self::class);
×
541

542
        $builtinType = $type->getBuiltinType();
×
UNCOV
543
        if (LegacyType::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
×
544
            $isValid = \is_float($value) || \is_int($value);
×
545
        } else {
UNCOV
546
            $isValid = \call_user_func('is_'.$builtinType, $value);
×
547
        }
548

UNCOV
549
        if (!$isValid) {
×
UNCOV
550
            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);
×
551
        }
552
    }
553

554
    /**
555
     * Validates the type of the value. Allows using integers as floats for JSON formats.
556
     *
557
     * @throws NotNormalizableValueException
558
     */
559
    protected function validateAttributeType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void
560
    {
561
        if ($type->isIdentifiedBy(TypeIdentifier::FLOAT) && null !== $format && str_contains($format, 'json')) {
12✔
UNCOV
562
            $isValid = \is_float($value) || \is_int($value);
×
563
        } else {
564
            $isValid = $type->accepts($value);
12✔
565
        }
566

567
        if (!$isValid) {
12✔
568
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $type, \gettype($value)), $value, [(string) $type], $context['deserialization_path'] ?? null);
2✔
569
        }
570
    }
571

572
    /**
573
     * @deprecated since 4.1, use "denormalizeObjectCollection" instead.
574
     *
575
     * Denormalizes a collection of objects.
576
     *
577
     * @throws NotNormalizableValueException
578
     */
579
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, LegacyType $type, string $className, mixed $value, ?string $format, array $context): array
580
    {
581
        trigger_deprecation('api-platform/serializer', '4.1', 'The "%s()" method is deprecated, use "%s::denormalizeObjectCollection()" instead.', __METHOD__, self::class);
×
582

UNCOV
583
        if (!\is_array($value)) {
×
UNCOV
584
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, ['array'], $context['deserialization_path'] ?? null);
×
585
        }
586

587
        $values = [];
×
588
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
×
589
        $collectionKeyTypes = $type->getCollectionKeyTypes();
×
590
        foreach ($value as $index => $obj) {
×
591
            $currentChildContext = $childContext;
×
UNCOV
592
            if (isset($childContext['deserialization_path'])) {
×
UNCOV
593
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
×
594
            }
595

596
            // no typehint provided on collection key
597
            if (!$collectionKeyTypes) {
×
UNCOV
598
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
UNCOV
599
                continue;
×
600
            }
601

602
            // validate collection key typehint
603
            foreach ($collectionKeyTypes as $collectionKeyType) {
×
604
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
×
UNCOV
605
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
×
UNCOV
606
                    continue;
×
607
                }
608

UNCOV
609
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
610
                continue 2;
×
611
            }
UNCOV
612
            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);
×
613
        }
614

UNCOV
615
        return $values;
×
616
    }
617

618
    /**
619
     * Denormalizes a collection of objects.
620
     *
621
     * @throws NotNormalizableValueException
622
     */
623
    protected function denormalizeObjectCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
624
    {
625
        if (!\is_array($value)) {
2✔
626
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, ['array'], $context['deserialization_path'] ?? null);
2✔
627
        }
628

UNCOV
629
        $values = [];
×
630
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
×
631

632
        foreach ($value as $index => $obj) {
×
633
            $currentChildContext = $childContext;
×
UNCOV
634
            if (isset($childContext['deserialization_path'])) {
×
UNCOV
635
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
×
636
            }
637

UNCOV
638
            if ($type instanceof CollectionType) {
×
639
                $collectionKeyType = $type->getCollectionKeyType();
×
640

UNCOV
641
                while ($collectionKeyType instanceof WrappingTypeInterface) {
×
UNCOV
642
                    $collectionKeyType = $type->getWrappedType();
×
643
                }
644

UNCOV
645
                if (!$collectionKeyType->accepts($index)) {
×
UNCOV
646
                    throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $type->getCollectionKeyType(), \gettype($index)), $index, [(string) $collectionKeyType], ($context['deserialization_path'] ?? false) ? \sprintf('key(%s)', $context['deserialization_path']) : null, true);
×
647
                }
648
            }
649

UNCOV
650
            $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
651
        }
652

UNCOV
653
        return $values;
×
654
    }
655

656
    /**
657
     * Denormalizes a relation.
658
     *
659
     * @throws LogicException
660
     * @throws UnexpectedValueException
661
     * @throws NotNormalizableValueException
662
     */
663
    protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object
664
    {
665
        if (\is_string($value)) {
6✔
666
            try {
667
                return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
4✔
668
            } catch (ItemNotFoundException $e) {
2✔
UNCOV
669
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
UNCOV
670
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
671
                }
672

UNCOV
673
                throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
674
            } catch (InvalidArgumentException $e) {
2✔
675
                if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
676
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
2✔
677
                }
678

UNCOV
679
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $value), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
680
            }
681
        }
682

683
        if ($propertyMetadata->isWritableLink()) {
2✔
684
            $context['api_allow_update'] = true;
×
685

UNCOV
686
            if (!$this->serializer instanceof DenormalizerInterface) {
×
UNCOV
687
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
688
            }
689

690
            $item = $this->serializer->denormalize($value, $className, $format, $context);
×
UNCOV
691
            if (!\is_object($item) && null !== $item) {
×
UNCOV
692
                throw new \UnexpectedValueException('Expected item to be an object or null.');
×
693
            }
694

UNCOV
695
            return $item;
×
696
        }
697

698
        if (!\is_array($value)) {
2✔
699
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array" (nested document) or "string" (IRI), "%s" given.', $attributeName, \gettype($value)), $value, ['array', 'string'], $context['deserialization_path'] ?? null, true);
2✔
700
        }
701

UNCOV
702
        throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName), $value, ['array', 'string'], $context['deserialization_path'] ?? null, true);
×
703
    }
704

705
    /**
706
     * Gets the options for the property name collection / property metadata factories.
707
     */
708
    protected function getFactoryOptions(array $context): array
709
    {
710
        $options = [];
488✔
711
        if (isset($context[self::GROUPS])) {
488✔
712
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
713
            $options['serializer_groups'] = (array) $context[self::GROUPS];
188✔
714
        }
715

716
        $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['root_operation_name'] ?? '');
488✔
717
        $suffix = ($context['api_normalize'] ?? '') ? 'n' : '';
488✔
718
        if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey.$suffix])) {
488✔
719
            return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix];
476✔
720
        }
721

722
        // This is a hot spot
723
        if (isset($context['resource_class'])) {
488✔
724
            // Note that the groups need to be read on the root operation
725
            if ($operation = ($context['root_operation'] ?? null)) {
488✔
726
                $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null;
250✔
727
                $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null;
250✔
728
                $options['operation_name'] = $operation->getName();
250✔
729
            }
730
        }
731

732
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
488✔
733
    }
734

735
    /**
736
     * {@inheritdoc}
737
     *
738
     * @throws UnexpectedValueException
739
     */
740
    protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
741
    {
742
        $context['api_attribute'] = $attribute;
474✔
743
        $context['property_metadata'] = $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
474✔
744

745
        if ($context['api_denormalize'] ?? false) {
474✔
746
            return $this->propertyAccessor->getValue($object, $attribute);
2✔
747
        }
748

749
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
474✔
750
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
751

752
            foreach ($types as $type) {
×
753
                if (
754
                    $type->isCollection()
×
755
                    && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
×
UNCOV
756
                    && ($className = $collectionValueType->getClassName())
×
757
                    && $this->resourceClassResolver->isResourceClass($className)
×
758
                ) {
UNCOV
759
                    $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
760

761
                    // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
762
                    // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
763
                    if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
×
764
                        $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
765
                            operationName: $itemUriTemplate,
×
766
                            forceCollection: true,
×
UNCOV
767
                            httpOperation: true
×
768
                        );
×
769

UNCOV
770
                        return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
771
                    }
772

773
                    $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
774

UNCOV
775
                    if (!is_iterable($attributeValue)) {
×
UNCOV
776
                        throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
777
                    }
778

779
                    $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
×
780

781
                    $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
×
UNCOV
782
                    $context['data'] = $data;
×
783
                    $context['type'] = $type;
×
784

UNCOV
785
                    if ($this->tagCollector) {
×
UNCOV
786
                        $this->tagCollector->collect($context);
×
787
                    }
788

UNCOV
789
                    return $data;
×
790
                }
791

792
                if (
UNCOV
793
                    ($className = $type->getClassName())
×
794
                    && $this->resourceClassResolver->isResourceClass($className)
×
795
                ) {
796
                    $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
797
                    unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
×
798
                    if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
×
799
                        $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
800
                            operationName: $uriTemplate,
×
UNCOV
801
                            httpOperation: true
×
802
                        );
×
803

UNCOV
804
                        return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
805
                    }
806

807
                    $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
808

UNCOV
809
                    if (!\is_object($attributeValue) && null !== $attributeValue) {
×
UNCOV
810
                        throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
811
                    }
812

813
                    $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
×
814

815
                    $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
×
UNCOV
816
                    $context['data'] = $data;
×
817
                    $context['type'] = $type;
×
818

UNCOV
819
                    if ($this->tagCollector) {
×
UNCOV
820
                        $this->tagCollector->collect($context);
×
821
                    }
822

UNCOV
823
                    return $data;
×
824
                }
825

UNCOV
826
                if (!$this->serializer instanceof NormalizerInterface) {
×
UNCOV
827
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
828
                }
829

830
                unset(
×
831
                    $context['resource_class'],
×
832
                    $context['force_resource_class'],
×
UNCOV
833
                    $context['uri_variables'],
×
UNCOV
834
                );
×
835

836
                // Anonymous resources
837
                if ($className) {
×
UNCOV
838
                    $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
839
                    $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
840

UNCOV
841
                    return $this->serializer->normalize($attributeValue, $format, $childContext);
×
842
                }
843

844
                if ('array' === $type->getBuiltinType()) {
×
UNCOV
845
                    if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
×
UNCOV
846
                        $context = $this->createOperationContext($context, $className, $propertyMetadata);
×
847
                    }
848

UNCOV
849
                    $childContext = $this->createChildContext($context, $attribute, $format);
×
850
                    $childContext['output']['gen_id'] ??= $propertyMetadata->getGenId() ?? true;
×
851

852
                    $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
853

UNCOV
854
                    return $this->serializer->normalize($attributeValue, $format, $childContext);
×
855
                }
856
            }
857

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

862
            unset(
×
863
                $context['resource_class'],
×
864
                $context['force_resource_class'],
×
UNCOV
865
                $context['uri_variables']
×
866
            );
×
867

868
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
869

UNCOV
870
            return $this->serializer->normalize($attributeValue, $format, $context);
×
871
        }
872

873
        $type = $propertyMetadata->getNativeType();
474✔
874

875
        $nullable = false;
474✔
876
        if ($type instanceof NullableType) {
474✔
877
            $type = $type->getWrappedType();
310✔
878
            $nullable = true;
310✔
879
        }
880

881
        // TODO check every foreach composite to see if null is an issue
882
        $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]);
474✔
883
        $className = null;
474✔
884
        $typeIsResourceClass = function (Type $type) use (&$className): bool {
474✔
885
            return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
472✔
886
        };
474✔
887

888
        foreach ($types as $type) {
474✔
889
            if ($type instanceof CollectionType && $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass)) {
472✔
890
                $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
14✔
891

892
                // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
893
                // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
894
                if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
14✔
895
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
896
                        operationName: $itemUriTemplate,
×
897
                        forceCollection: true,
×
UNCOV
898
                        httpOperation: true
×
899
                    );
×
900

UNCOV
901
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
902
                }
903

904
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
14✔
905

906
                if (!is_iterable($attributeValue)) {
14✔
UNCOV
907
                    throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
908
                }
909

910
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
14✔
911

912
                $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
14✔
913
                $context['data'] = $data;
14✔
914
                $context['type'] = $nullable ? Type::nullable($type) : $type;
14✔
915

916
                if ($this->tagCollector) {
14✔
917
                    $this->tagCollector->collect($context);
14✔
918
                }
919

920
                return $data;
14✔
921
            }
922

923
            if ($type->isSatisfiedBy($typeIsResourceClass)) {
472✔
924
                $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
20✔
925
                unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
20✔
926
                if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
20✔
927
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
928
                        operationName: $uriTemplate,
×
UNCOV
929
                        httpOperation: true
×
930
                    );
×
931

UNCOV
932
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
933
                }
934

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

937
                if (!\is_object($attributeValue) && null !== $attributeValue) {
20✔
UNCOV
938
                    throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
939
                }
940

941
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
20✔
942

943
                $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
20✔
944
                $context['data'] = $data;
20✔
945
                $context['type'] = $nullable ? Type::nullable($type) : $type;
20✔
946

947
                if ($this->tagCollector) {
20✔
948
                    $this->tagCollector->collect($context);
12✔
949
                }
950

951
                return $data;
20✔
952
            }
953

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

958
            unset(
472✔
959
                $context['resource_class'],
472✔
960
                $context['force_resource_class'],
472✔
961
                $context['uri_variables'],
472✔
962
            );
472✔
963

964
            // Anonymous resources
965
            if ($className) {
472✔
966
                $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
188✔
967
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
188✔
968

969
                return $this->serializer->normalize($attributeValue, $format, $childContext);
188✔
970
            }
971

972
            if ($type instanceof CollectionType) {
466✔
973
                if (($subType = $type->getCollectionValueType()) instanceof ObjectType) {
56✔
UNCOV
974
                    $context = $this->createOperationContext($context, $subType->getClassName(), $propertyMetadata);
×
975
                }
976

977
                $childContext = $this->createChildContext($context, $attribute, $format);
56✔
978
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
56✔
979

980
                return $this->serializer->normalize($attributeValue, $format, $childContext);
56✔
981
            }
982
        }
983

984
        if (!$this->serializer instanceof NormalizerInterface) {
468✔
UNCOV
985
            throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
986
        }
987

988
        unset(
468✔
989
            $context['resource_class'],
468✔
990
            $context['force_resource_class'],
468✔
991
            $context['uri_variables']
468✔
992
        );
468✔
993

994
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
468✔
995

996
        return $this->serializer->normalize($attributeValue, $format, $context);
466✔
997
    }
998

999
    /**
1000
     * Normalizes a collection of relations (to-many).
1001
     *
1002
     * @throws UnexpectedValueException
1003
     */
1004
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
1005
    {
1006
        $value = [];
14✔
1007
        foreach ($attributeValue as $index => $obj) {
14✔
1008
            if (!\is_object($obj) && null !== $obj) {
2✔
UNCOV
1009
                throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
×
1010
            }
1011

1012
            // update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources)
1013
            $objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass);
2✔
1014
            $context['resource_class'] = $objResourceClass;
2✔
1015
            if ($this->resourceMetadataCollectionFactory) {
2✔
1016
                $context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation();
2✔
1017
            }
1018

1019
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
2✔
1020
        }
1021

1022
        return $value;
14✔
1023
    }
1024

1025
    /**
1026
     * Normalizes a relation.
1027
     *
1028
     * @throws LogicException
1029
     * @throws UnexpectedValueException
1030
     */
1031
    protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $relatedObject, string $resourceClass, ?string $format, array $context): \ArrayObject|array|string|null
1032
    {
1033
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink() || false === ($context['output']['gen_id'] ?? true)) {
22✔
1034
            if (!$this->serializer instanceof NormalizerInterface) {
14✔
UNCOV
1035
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
1036
            }
1037

1038
            $relatedContext = $this->createOperationContext($context, $resourceClass, $propertyMetadata);
14✔
1039
            $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $relatedContext);
14✔
1040
            if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
14✔
UNCOV
1041
                throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
×
1042
            }
1043

1044
            return $normalizedRelatedObject;
14✔
1045
        }
1046

1047
        $context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
8✔
1048
        $context['data'] = $iri;
8✔
1049
        $context['object'] = $relatedObject;
8✔
1050
        unset($context['property_metadata'], $context['api_attribute']);
8✔
1051

1052
        if ($this->tagCollector) {
8✔
1053
            $this->tagCollector->collect($context);
4✔
1054
        } elseif (isset($context['resources'])) {
4✔
UNCOV
1055
            $context['resources'][$iri] = $iri;
×
1056
        }
1057

1058
        $push = $propertyMetadata->getPush() ?? false;
8✔
1059
        if (isset($context['resources_to_push']) && $push) {
8✔
UNCOV
1060
            $context['resources_to_push'][$iri] = $iri;
×
1061
        }
1062

1063
        return $iri;
8✔
1064
    }
1065

1066
    private function createAttributeValue(string $attribute, mixed $value, ?string $format = null, array &$context = []): mixed
1067
    {
1068
        try {
1069
            return $this->createAndValidateAttributeValue($attribute, $value, $format, $context);
14✔
1070
        } catch (NotNormalizableValueException $exception) {
4✔
1071
            if (!isset($context['not_normalizable_value_exceptions'])) {
2✔
UNCOV
1072
                throw $exception;
×
1073
            }
1074
            $context['not_normalizable_value_exceptions'][] = $exception;
2✔
1075
            throw $exception;
2✔
1076
        }
1077
    }
1078

1079
    private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed
1080
    {
1081
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
16✔
1082
        $denormalizationException = null;
16✔
1083

1084
        $types = [];
16✔
1085
        $type = null;
16✔
1086
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
16✔
UNCOV
1087
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
1088
        } else {
1089
            $type = $propertyMetadata->getNativeType();
16✔
1090
            $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]);
16✔
1091
        }
1092

1093
        $isMultipleTypes = \count($types) > 1;
16✔
1094
        $className = null;
16✔
1095
        $typeIsResourceClass = function (Type $type) use (&$className): bool {
16✔
1096
            return $type instanceof ObjectType ? $this->resourceClassResolver->isResourceClass($className = $type->getClassName()) : false;
16✔
1097
        };
16✔
1098

1099
        $isMultipleTypes = \count($types) > 1;
16✔
1100
        $denormalizationException = null;
16✔
1101

1102
        foreach ($types as $t) {
16✔
1103
            if ($type instanceof Type) {
16✔
1104
                $isNullable = $type->isNullable();
16✔
1105
            } else {
UNCOV
1106
                $isNullable = $t->isNullable();
×
1107
            }
1108

1109
            if (null === $value && ($isNullable || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
16✔
UNCOV
1110
                return $value;
×
1111
            }
1112

1113
            $collectionValueType = null;
16✔
1114

1115
            if ($t instanceof CollectionType) {
16✔
1116
                $collectionValueType = $t->getCollectionValueType();
2✔
1117
            } elseif ($t instanceof LegacyType) {
16✔
UNCOV
1118
                $collectionValueType = $t->getCollectionValueTypes()[0] ?? null;
×
1119
            }
1120

1121
            /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
1122
            // Fix a collection that contains the only one element
1123
            // This is special to xml format only
1124
            if ('xml' === $format && null !== $collectionValueType && (!\is_array($value) || !\is_int(key($value)))) {
16✔
1125
                $isMixedType = $collectionValueType instanceof Type && $collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED);
×
UNCOV
1126
                if (!$isMixedType) {
×
UNCOV
1127
                    $value = [$value];
×
1128
                }
1129
            }
1130

1131
            if (($collectionValueType instanceof Type && $collectionValueType->isSatisfiedBy($typeIsResourceClass))
16✔
1132
                || ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className))
16✔
1133
            ) {
1134
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
2✔
1135
                $context['resource_class'] = $resourceClass;
2✔
1136
                unset($context['uri_variables']);
2✔
1137

1138
                try {
1139
                    return $t instanceof Type
2✔
1140
                        ? $this->denormalizeObjectCollection($attribute, $propertyMetadata, $t, $resourceClass, $value, $format, $context)
2✔
UNCOV
1141
                        : $this->denormalizeCollection($attribute, $propertyMetadata, $t, $resourceClass, $value, $format, $context);
×
1142
                } catch (NotNormalizableValueException $e) {
2✔
1143
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1144
                    if ($isMultipleTypes) {
2✔
1145
                        $denormalizationException ??= $e;
×
1146

UNCOV
1147
                        continue;
×
1148
                    }
1149

1150
                    throw $e;
2✔
1151
                }
1152
            }
1153

1154
            if (
1155
                ($t instanceof Type && $t->isSatisfiedBy($typeIsResourceClass))
16✔
1156
                || ($t instanceof LegacyType && null !== ($className = $t->getClassName()) && $this->resourceClassResolver->isResourceClass($className))
16✔
1157
            ) {
1158
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
6✔
1159
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass, $propertyMetadata), $attribute, $format);
6✔
1160

1161
                try {
1162
                    return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
6✔
1163
                } catch (NotNormalizableValueException $e) {
4✔
1164
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1165
                    if ($isMultipleTypes) {
2✔
1166
                        $denormalizationException ??= $e;
2✔
1167

1168
                        continue;
2✔
1169
                    }
1170

UNCOV
1171
                    throw $e;
×
1172
                }
1173
            }
1174

1175
            if (
1176
                ($t instanceof CollectionType && $collectionValueType instanceof ObjectType)
12✔
1177
                || ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== $collectionValueType->getClassName())
12✔
1178
            ) {
1179
                $className = $collectionValueType->getClassName();
×
UNCOV
1180
                if (!$this->serializer instanceof DenormalizerInterface) {
×
UNCOV
1181
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
1182
                }
1183

UNCOV
1184
                unset($context['resource_class'], $context['uri_variables']);
×
1185

1186
                try {
UNCOV
1187
                    return $this->serializer->denormalize($value, $className.'[]', $format, $context);
×
1188
                } catch (NotNormalizableValueException $e) {
×
1189
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
1190
                    if ($isMultipleTypes) {
×
1191
                        $denormalizationException ??= $e;
×
1192

UNCOV
1193
                        continue;
×
1194
                    }
1195

UNCOV
1196
                    throw $e;
×
1197
                }
1198
            }
1199

1200
            while ($t instanceof WrappingTypeInterface) {
12✔
UNCOV
1201
                $t = $t->getWrappedType();
×
1202
            }
1203

1204
            if (
1205
                $t instanceof ObjectType
12✔
1206
                || ($t instanceof LegacyType && null !== $t->getClassName())
12✔
1207
            ) {
1208
                if (!$this->serializer instanceof DenormalizerInterface) {
2✔
UNCOV
1209
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
1210
                }
1211

1212
                unset($context['resource_class'], $context['uri_variables']);
2✔
1213

1214
                try {
1215
                    return $this->serializer->denormalize($value, $t->getClassName(), $format, $context);
2✔
1216
                } catch (NotNormalizableValueException $e) {
2✔
1217
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1218
                    if ($isMultipleTypes) {
2✔
1219
                        $denormalizationException ??= $e;
2✔
1220

1221
                        continue;
2✔
1222
                    }
1223

UNCOV
1224
                    throw $e;
×
1225
                }
1226
            }
1227

1228
            /* From @see AbstractObjectNormalizer::validateAndDenormalize() */
1229
            // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
1230
            // if a value is meant to be a string, float, int or a boolean value from the serialized representation.
1231
            // That's why we have to transform the values, if one of these non-string basic datatypes is expected.
1232
            if (\is_string($value) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
12✔
1233
                if ('' === $value && $isNullable && (
×
UNCOV
1234
                    ($t instanceof Type && $t->isIdentifiedBy(TypeIdentifier::BOOL, TypeIdentifier::INT, TypeIdentifier::FLOAT))
×
1235
                    || ($t instanceof LegacyType && \in_array($t->getBuiltinType(), [LegacyType::BUILTIN_TYPE_BOOL, LegacyType::BUILTIN_TYPE_INT, LegacyType::BUILTIN_TYPE_FLOAT], true))
×
1236
                )) {
UNCOV
1237
                    return null;
×
1238
                }
1239

UNCOV
1240
                $typeIdentifier = $t instanceof BuiltinType ? $t->getTypeIdentifier() : TypeIdentifier::tryFrom($t->getBuiltinType());
×
1241

1242
                switch ($typeIdentifier) {
1243
                    case TypeIdentifier::BOOL:
×
1244
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
1245
                        if ('false' === $value || '0' === $value) {
×
1246
                            $value = false;
×
UNCOV
1247
                        } elseif ('true' === $value || '1' === $value) {
×
UNCOV
1248
                            $value = true;
×
1249
                        } else {
1250
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
1251
                            if ($isMultipleTypes) {
×
1252
                                break 2;
×
1253
                            }
1254
                            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $className, $value), $value, ['bool'], $context['deserialization_path'] ?? null);
×
1255
                        }
1256
                        break;
×
1257
                    case TypeIdentifier::INT:
×
UNCOV
1258
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
×
UNCOV
1259
                            $value = (int) $value;
×
1260
                        } else {
1261
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
1262
                            if ($isMultipleTypes) {
×
1263
                                break 2;
×
1264
                            }
1265
                            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $className, $value), $value, ['int'], $context['deserialization_path'] ?? null);
×
1266
                        }
1267
                        break;
×
1268
                    case TypeIdentifier::FLOAT:
×
UNCOV
1269
                        if (is_numeric($value)) {
×
UNCOV
1270
                            return (float) $value;
×
1271
                        }
1272

1273
                        switch ($value) {
1274
                            case 'NaN':
×
1275
                                return \NAN;
×
1276
                            case 'INF':
×
1277
                                return \INF;
×
UNCOV
1278
                            case '-INF':
×
UNCOV
1279
                                return -\INF;
×
1280
                            default:
1281
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
1282
                                if ($isMultipleTypes) {
×
1283
                                    break 3;
×
1284
                                }
UNCOV
1285
                                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $className, $value), $value, ['float'], $context['deserialization_path'] ?? null);
×
1286
                        }
1287
                }
1288
            }
1289

1290
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
12✔
UNCOV
1291
                return $value;
×
1292
            }
1293

1294
            try {
1295
                $t instanceof Type
12✔
1296
                    ? $this->validateAttributeType($attribute, $t, $value, $format, $context)
12✔
UNCOV
1297
                    : $this->validateType($attribute, $t, $value, $format, $context);
×
1298

1299
                $denormalizationException = null;
10✔
1300
                break;
10✔
1301
            } catch (NotNormalizableValueException $e) {
2✔
1302
                // union/intersect types: try the next type
1303
                if (!$isMultipleTypes) {
2✔
UNCOV
1304
                    throw $e;
×
1305
                }
1306

1307
                $denormalizationException ??= $e;
2✔
1308
            }
1309
        }
1310

1311
        if ($denormalizationException) {
12✔
1312
            if ($type instanceof Type && $type->isSatisfiedBy(static fn ($type) => $type instanceof BuiltinType) && !$type->isSatisfiedBy($typeIsResourceClass)) {
2✔
1313
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $type, \gettype($value)), $value, array_map(strval(...), $types), $context['deserialization_path'] ?? null);
2✔
1314
            }
1315

1316
            throw $denormalizationException;
2✔
1317
        }
1318

1319
        return $value;
10✔
1320
    }
1321

1322
    /**
1323
     * Sets a value of the object using the PropertyAccess component.
1324
     */
1325
    private function setValue(object $object, string $attributeName, mixed $value): void
1326
    {
1327
        try {
1328
            $this->propertyAccessor->setValue($object, $attributeName, $value);
10✔
UNCOV
1329
        } catch (NoSuchPropertyException) {
×
1330
            // Properties not found are ignored
1331
        }
1332
    }
1333
}
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