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

api-platform / core / 17562281609

08 Sep 2025 07:47PM UTC coverage: 0.0% (-22.6%) from 22.604%
17562281609

Pull #7374

github

web-flow
Merge 0c2b4a90a into 6db55be8c
Pull Request #7374: fix(jsonld): various json streamer fixes

0 of 33 new or added lines in 2 files covered. (0.0%)

12082 existing lines in 401 files now uncovered.

0 of 52792 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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
     * Flag to control whether to one relation with the value `null` should be output
70
     * when normalizing or omitted.
71
     */
72
    public const SKIP_NULL_TO_ONE_RELATIONS = 'skip_null_to_one_relations';
73

74
    protected PropertyAccessorInterface $propertyAccessor;
75
    protected array $localCache = [];
76
    protected array $localFactoryOptionsCache = [];
77
    protected ?ResourceAccessCheckerInterface $resourceAccessChecker;
78

79
    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)
80
    {
UNCOV
81
        if (!isset($defaultContext['circular_reference_handler'])) {
×
UNCOV
82
            $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object);
×
83
        }
84

UNCOV
85
        parent::__construct($classMetadataFactory, $nameConverter, null, null, \Closure::fromCallable($this->getObjectClass(...)), $defaultContext);
×
UNCOV
86
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
×
UNCOV
87
        $this->resourceAccessChecker = $resourceAccessChecker;
×
UNCOV
88
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
×
89
    }
90

91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
95
    {
UNCOV
96
        if (!\is_object($data) || is_iterable($data)) {
×
UNCOV
97
            return false;
×
98
        }
99

UNCOV
100
        $class = $context['force_resource_class'] ?? $this->getObjectClass($data);
×
UNCOV
101
        if (($context['output']['class'] ?? null) === $class) {
×
UNCOV
102
            return true;
×
103
        }
104

UNCOV
105
        return $this->resourceClassResolver->isResourceClass($class);
×
106
    }
107

108
    public function getSupportedTypes(?string $format): array
109
    {
UNCOV
110
        return [
×
UNCOV
111
            'object' => true,
×
UNCOV
112
        ];
×
113
    }
114

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

UNCOV
128
            unset($context['output'], $context['operation'], $context['operation_name']);
×
UNCOV
129
            $context['resource_class'] = $outputClass;
×
UNCOV
130
            $context['api_sub_level'] = true;
×
UNCOV
131
            $context[self::ALLOW_EXTRA_ATTRIBUTES] = false;
×
132

UNCOV
133
            return $this->serializer->normalize($object, $format, $context);
×
134
        }
135

136
        // Never remove this, with `application/json` we don't use our AbstractCollectionNormalizer and we need
137
        // to remove the collection operation from our context or we'll introduce security issues
UNCOV
138
        if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
×
UNCOV
139
            unset($context['operation_name'], $context['operation'], $context['iri']);
×
140
        }
141

UNCOV
142
        if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
×
UNCOV
143
            $context = $this->initContext($resourceClass, $context);
×
144
        }
145

UNCOV
146
        $context['api_normalize'] = true;
×
UNCOV
147
        $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL, $context['operation'] ?? null, $context);
×
148

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

UNCOV
162
        if (!$this->tagCollector && isset($context['resources'])) {
×
163
            $context['resources'][$iri] = $iri;
×
164
        }
165

UNCOV
166
        $context['object'] = $object;
×
UNCOV
167
        $context['format'] = $format;
×
168

UNCOV
169
        $data = parent::normalize($object, $format, $context);
×
170

UNCOV
171
        $context['data'] = $data;
×
UNCOV
172
        unset($context['property_metadata'], $context['api_attribute']);
×
173

UNCOV
174
        if ($emptyResourceAsIri && \is_array($data) && 0 === \count($data)) {
×
175
            $context['data'] = $iri;
×
176

177
            if ($this->tagCollector) {
×
178
                $this->tagCollector->collect($context);
×
179
            }
180

181
            return $iri;
×
182
        }
183

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

UNCOV
188
        return $data;
×
189
    }
190

191
    /**
192
     * {@inheritdoc}
193
     */
194
    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
195
    {
UNCOV
196
        if (($context['input']['class'] ?? null) === $type) {
×
197
            return true;
×
198
        }
199

UNCOV
200
        return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
×
201
    }
202

203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function denormalize(mixed $data, string $class, ?string $format = null, array $context = []): mixed
207
    {
UNCOV
208
        $resourceClass = $class;
×
209

UNCOV
210
        if ($inputClass = $this->getInputClass($context)) {
×
UNCOV
211
            if (!$this->serializer instanceof DenormalizerInterface) {
×
212
                throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
×
213
            }
214

UNCOV
215
            unset($context['input'], $context['operation'], $context['operation_name'], $context['uri_variables']);
×
UNCOV
216
            $context['resource_class'] = $inputClass;
×
217

218
            try {
UNCOV
219
                return $this->serializer->denormalize($data, $inputClass, $format, $context);
×
220
            } catch (NotNormalizableValueException $e) {
×
221
                throw new UnexpectedValueException('The input data is misformatted.', $e->getCode(), $e);
×
222
            }
223
        }
224

UNCOV
225
        if (null === $objectToPopulate = $this->extractObjectToPopulate($resourceClass, $context, static::OBJECT_TO_POPULATE)) {
×
UNCOV
226
            $normalizedData = \is_scalar($data) ? [$data] : $this->prepareForDenormalization($data);
×
UNCOV
227
            $class = $this->getClassDiscriminatorResolvedClass($normalizedData, $class, $context);
×
228
        }
229

UNCOV
230
        $context['api_denormalize'] = true;
×
231

UNCOV
232
        if ($this->resourceClassResolver->isResourceClass($class)) {
×
UNCOV
233
            $resourceClass = $this->resourceClassResolver->getResourceClass($objectToPopulate, $class);
×
UNCOV
234
            $context['resource_class'] = $resourceClass;
×
235
        }
236

UNCOV
237
        if (\is_string($data)) {
×
238
            try {
UNCOV
239
                return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]);
×
240
            } catch (ItemNotFoundException $e) {
×
241
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
242
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
243
                }
244

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

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

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

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

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

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

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

UNCOV
275
        $operation = $context['operation'] ?? null;
×
UNCOV
276
        $throwOnAccessDenied = $operation?->getExtraProperties()['throw_on_access_denied'] ?? false;
×
UNCOV
277
        $securityMessage = $operation?->getSecurityMessage() ?? null;
×
278

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

UNCOV
289
            if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) {
×
290
                if ($throwOnPropertyAccessDenied) {
×
291
                    throw new AccessDeniedException($securityMessage ?? 'Access denied');
×
292
                }
293
                if (null !== $previousObject) {
×
294
                    $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute));
×
295
                } else {
296
                    $this->setValue($object, $attribute, $propertyMetadata->getDefault());
×
297
                }
298
            }
299
        }
300

UNCOV
301
        return $object;
×
302
    }
303

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

UNCOV
317
            return $object;
×
318
        }
319

UNCOV
320
        $class = $this->getClassDiscriminatorResolvedClass($data, $class, $context);
×
UNCOV
321
        $reflectionClass = new \ReflectionClass($class);
×
322

UNCOV
323
        $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
×
UNCOV
324
        if ($constructor) {
×
UNCOV
325
            $constructorParameters = $constructor->getParameters();
×
326

UNCOV
327
            $params = [];
×
UNCOV
328
            $missingConstructorArguments = [];
×
UNCOV
329
            foreach ($constructorParameters as $constructorParameter) {
×
UNCOV
330
                $paramName = $constructorParameter->name;
×
UNCOV
331
                $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
×
UNCOV
332
                $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
×
UNCOV
333
                $attributeContext['deserialization_path'] = $attributeContext['deserialization_path'] ?? $key;
×
334

UNCOV
335
                $allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
×
UNCOV
336
                $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
×
UNCOV
337
                if ($constructorParameter->isVariadic()) {
×
338
                    if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
×
339
                        if (!\is_array($data[$paramName])) {
×
340
                            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));
×
341
                        }
342

343
                        $params[] = $data[$paramName];
×
344
                    }
UNCOV
345
                } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
×
346
                    try {
UNCOV
347
                        $params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $attributeContext, $format);
×
UNCOV
348
                    } catch (NotNormalizableValueException $exception) {
×
UNCOV
349
                        if (!isset($context['not_normalizable_value_exceptions'])) {
×
350
                            throw $exception;
×
351
                        }
UNCOV
352
                        $context['not_normalizable_value_exceptions'][] = $exception;
×
353
                    }
354

355
                    // Don't run set for a parameter passed to the constructor
UNCOV
356
                    unset($data[$key]);
×
UNCOV
357
                } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
×
358
                    $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
×
UNCOV
359
                } elseif ($constructorParameter->isDefaultValueAvailable()) {
×
UNCOV
360
                    $params[] = $constructorParameter->getDefaultValue();
×
361
                } else {
UNCOV
362
                    if (!isset($context['not_normalizable_value_exceptions'])) {
×
363
                        $missingConstructorArguments[] = $constructorParameter->name;
×
364
                    }
365

UNCOV
366
                    $constructorParameterType = 'unknown';
×
UNCOV
367
                    $reflectionType = $constructorParameter->getType();
×
UNCOV
368
                    if ($reflectionType instanceof \ReflectionNamedType) {
×
UNCOV
369
                        $constructorParameterType = $reflectionType->getName();
×
370
                    }
371

UNCOV
372
                    $exception = NotNormalizableValueException::createForUnexpectedDataType(
×
UNCOV
373
                        \sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
×
UNCOV
374
                        null,
×
UNCOV
375
                        [$constructorParameterType],
×
UNCOV
376
                        $attributeContext['deserialization_path'],
×
UNCOV
377
                        true
×
UNCOV
378
                    );
×
UNCOV
379
                    $context['not_normalizable_value_exceptions'][] = $exception;
×
380
                }
381
            }
382

UNCOV
383
            if ($missingConstructorArguments) {
×
384
                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);
×
385
            }
386

UNCOV
387
            if (\count($context['not_normalizable_value_exceptions'] ?? []) > 0) {
×
UNCOV
388
                return $reflectionClass->newInstanceWithoutConstructor();
×
389
            }
390

UNCOV
391
            if ($constructor->isConstructor()) {
×
UNCOV
392
                return $reflectionClass->newInstanceArgs($params);
×
393
            }
394

395
            return $constructor->invokeArgs(null, $params);
×
396
        }
397

UNCOV
398
        return new $class();
×
399
    }
400

401
    protected function getClassDiscriminatorResolvedClass(array $data, string $class, array $context = []): string
402
    {
UNCOV
403
        if (null === $this->classDiscriminatorResolver || (null === $mapping = $this->classDiscriminatorResolver->getMappingForClass($class))) {
×
UNCOV
404
            return $class;
×
405
        }
406

407
        // @phpstan-ignore-next-line function.alreadyNarrowedType
408
        $defaultType = method_exists($mapping, 'getDefaultType') ? $mapping->getDefaultType() : null;
×
409
        if (!isset($data[$mapping->getTypeProperty()]) && null === $defaultType) {
×
410
            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());
×
411
        }
412

413
        $type = $data[$mapping->getTypeProperty()] ?? $defaultType;
×
414
        if (null === ($mappedClass = $mapping->getClassForType($type))) {
×
415
            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);
×
416
        }
417

418
        return $mappedClass;
×
419
    }
420

421
    protected function createConstructorArgument(mixed $parameterData, string $key, \ReflectionParameter $constructorParameter, array $context, ?string $format = null): mixed
422
    {
UNCOV
423
        return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
×
424
    }
425

426
    /**
427
     * {@inheritdoc}
428
     *
429
     * Unused in this context.
430
     *
431
     * @param object      $object
432
     * @param string|null $format
433
     *
434
     * @return string[]
435
     */
436
    protected function extractAttributes($object, $format = null, array $context = []): array
437
    {
438
        return [];
×
439
    }
440

441
    /**
442
     * {@inheritdoc}
443
     */
444
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
445
    {
UNCOV
446
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
×
UNCOV
447
            return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
×
448
        }
449

UNCOV
450
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
×
UNCOV
451
        $options = $this->getFactoryOptions($context);
×
UNCOV
452
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
×
453

UNCOV
454
        $allowedAttributes = [];
×
UNCOV
455
        foreach ($propertyNames as $propertyName) {
×
UNCOV
456
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
×
457

458
            if (
UNCOV
459
                $this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
×
UNCOV
460
                && (isset($context['api_normalize']) && $propertyMetadata->isReadable()
×
UNCOV
461
                    || isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
×
462
                )
463
            ) {
UNCOV
464
                $allowedAttributes[] = $propertyName;
×
465
            }
466
        }
467

UNCOV
468
        return $allowedAttributes;
×
469
    }
470

471
    /**
472
     * {@inheritdoc}
473
     */
474
    protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
475
    {
UNCOV
476
        if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
×
UNCOV
477
            return false;
×
478
        }
479

UNCOV
480
        return $this->canAccessAttribute(\is_object($classOrObject) ? $classOrObject : null, $attribute, $context);
×
481
    }
482

483
    /**
484
     * Check if access to the attribute is granted.
485
     */
486
    protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
487
    {
UNCOV
488
        if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
×
UNCOV
489
            return true;
×
490
        }
491

UNCOV
492
        $options = $this->getFactoryOptions($context);
×
UNCOV
493
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
×
UNCOV
494
        $security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy();
×
UNCOV
495
        if (null !== $this->resourceAccessChecker && $security) {
×
UNCOV
496
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
×
UNCOV
497
                'object' => $object,
×
UNCOV
498
                'property' => $attribute,
×
UNCOV
499
            ]);
×
500
        }
501

UNCOV
502
        return true;
×
503
    }
504

505
    /**
506
     * Check if access to the attribute is granted.
507
     */
508
    protected function canAccessAttributePostDenormalize(?object $object, ?object $previousObject, string $attribute, array $context = []): bool
509
    {
UNCOV
510
        $options = $this->getFactoryOptions($context);
×
UNCOV
511
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
×
UNCOV
512
        $security = $propertyMetadata->getSecurityPostDenormalize();
×
UNCOV
513
        if ($this->resourceAccessChecker && $security) {
×
514
            return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
×
515
                'object' => $object,
×
516
                'previous_object' => $previousObject,
×
517
                'property' => $attribute,
×
518
            ]);
×
519
        }
520

UNCOV
521
        return true;
×
522
    }
523

524
    /**
525
     * {@inheritdoc}
526
     */
527
    protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
528
    {
529
        try {
UNCOV
530
            $this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
×
UNCOV
531
        } catch (NotNormalizableValueException $exception) {
×
532
            // Only throw if collecting denormalization errors is disabled.
UNCOV
533
            if (!isset($context['not_normalizable_value_exceptions'])) {
×
534
                throw $exception;
×
535
            }
536
        }
537
    }
538

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

550
        $builtinType = $type->getBuiltinType();
×
551
        if (LegacyType::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) {
×
552
            $isValid = \is_float($value) || \is_int($value);
×
553
        } else {
554
            $isValid = \call_user_func('is_'.$builtinType, $value);
×
555
        }
556

557
        if (!$isValid) {
×
558
            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);
×
559
        }
560
    }
561

562
    /**
563
     * Validates the type of the value. Allows using integers as floats for JSON formats.
564
     *
565
     * @throws NotNormalizableValueException
566
     */
567
    protected function validateAttributeType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void
568
    {
UNCOV
569
        if ($type->isIdentifiedBy(TypeIdentifier::FLOAT) && null !== $format && str_contains($format, 'json')) {
×
570
            $isValid = \is_float($value) || \is_int($value);
×
571
        } else {
UNCOV
572
            $isValid = $type->accepts($value);
×
573
        }
574

UNCOV
575
        if (!$isValid) {
×
UNCOV
576
            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);
×
577
        }
578
    }
579

580
    /**
581
     * @deprecated since 4.1, use "denormalizeObjectCollection" instead.
582
     *
583
     * Denormalizes a collection of objects.
584
     *
585
     * @throws NotNormalizableValueException
586
     */
587
    protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, LegacyType $type, string $className, mixed $value, ?string $format, array $context): array
588
    {
589
        trigger_deprecation('api-platform/serializer', '4.1', 'The "%s()" method is deprecated, use "%s::denormalizeObjectCollection()" instead.', __METHOD__, self::class);
×
590

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

595
        $values = [];
×
596
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
×
597
        $collectionKeyTypes = $type->getCollectionKeyTypes();
×
598
        foreach ($value as $index => $obj) {
×
599
            $currentChildContext = $childContext;
×
600
            if (isset($childContext['deserialization_path'])) {
×
601
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
×
602
            }
603

604
            // no typehint provided on collection key
605
            if (!$collectionKeyTypes) {
×
606
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
607
                continue;
×
608
            }
609

610
            // validate collection key typehint
611
            foreach ($collectionKeyTypes as $collectionKeyType) {
×
612
                $collectionKeyBuiltinType = $collectionKeyType->getBuiltinType();
×
613
                if (!\call_user_func('is_'.$collectionKeyBuiltinType, $index)) {
×
614
                    continue;
×
615
                }
616

617
                $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
618
                continue 2;
×
619
            }
620
            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);
×
621
        }
622

623
        return $values;
×
624
    }
625

626
    /**
627
     * Denormalizes a collection of objects.
628
     *
629
     * @throws NotNormalizableValueException
630
     */
631
    protected function denormalizeObjectCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array
632
    {
UNCOV
633
        if (!\is_array($value)) {
×
UNCOV
634
            throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, ['array'], $context['deserialization_path'] ?? null);
×
635
        }
636

637
        $values = [];
×
638
        $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format);
×
639

640
        foreach ($value as $index => $obj) {
×
641
            $currentChildContext = $childContext;
×
642
            if (isset($childContext['deserialization_path'])) {
×
643
                $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]";
×
644
            }
645

646
            if ($type instanceof CollectionType) {
×
647
                $collectionKeyType = $type->getCollectionKeyType();
×
648

649
                while ($collectionKeyType instanceof WrappingTypeInterface) {
×
650
                    $collectionKeyType = $type->getWrappedType();
×
651
                }
652

653
                if (!$collectionKeyType->accepts($index)) {
×
654
                    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);
×
655
                }
656
            }
657

658
            $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext);
×
659
        }
660

661
        return $values;
×
662
    }
663

664
    /**
665
     * Denormalizes a relation.
666
     *
667
     * @throws LogicException
668
     * @throws UnexpectedValueException
669
     * @throws NotNormalizableValueException
670
     */
671
    protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object
672
    {
UNCOV
673
        if (\is_string($value)) {
×
674
            try {
UNCOV
675
                return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]);
×
UNCOV
676
            } catch (ItemNotFoundException $e) {
×
677
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
678
                    throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
×
679
                }
680

681
                throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
UNCOV
682
            } catch (InvalidArgumentException $e) {
×
UNCOV
683
                if (!isset($context['not_normalizable_value_exceptions'])) {
×
UNCOV
684
                    throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e);
×
685
                }
686

687
                throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $value), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
×
688
            }
689
        }
690

UNCOV
691
        if ($propertyMetadata->isWritableLink()) {
×
UNCOV
692
            $context['api_allow_update'] = true;
×
693

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

UNCOV
698
            $item = $this->serializer->denormalize($value, $className, $format, $context);
×
UNCOV
699
            if (!\is_object($item) && null !== $item) {
×
700
                throw new \UnexpectedValueException('Expected item to be an object or null.');
×
701
            }
702

UNCOV
703
            return $item;
×
704
        }
705

UNCOV
706
        if (!\is_array($value)) {
×
UNCOV
707
            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);
×
708
        }
709

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

713
    /**
714
     * Gets the options for the property name collection / property metadata factories.
715
     */
716
    protected function getFactoryOptions(array $context): array
717
    {
UNCOV
718
        $options = [];
×
UNCOV
719
        if (isset($context[self::GROUPS])) {
×
720
            /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
UNCOV
721
            $options['serializer_groups'] = (array) $context[self::GROUPS];
×
722
        }
723

UNCOV
724
        $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['root_operation_name'] ?? '');
×
UNCOV
725
        $suffix = ($context['api_normalize'] ?? '') ? 'n' : '';
×
UNCOV
726
        if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey.$suffix])) {
×
UNCOV
727
            return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix];
×
728
        }
729

730
        // This is a hot spot
UNCOV
731
        if (isset($context['resource_class'])) {
×
732
            // Note that the groups need to be read on the root operation
UNCOV
733
            if ($operation = ($context['root_operation'] ?? null)) {
×
UNCOV
734
                $options['normalization_groups'] = $operation->getNormalizationContext()['groups'] ?? null;
×
UNCOV
735
                $options['denormalization_groups'] = $operation->getDenormalizationContext()['groups'] ?? null;
×
UNCOV
736
                $options['operation_name'] = $operation->getName();
×
737
            }
738
        }
739

UNCOV
740
        return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
×
741
    }
742

743
    /**
744
     * {@inheritdoc}
745
     *
746
     * @throws UnexpectedValueException
747
     */
748
    protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
749
    {
UNCOV
750
        $context['api_attribute'] = $attribute;
×
UNCOV
751
        $context['property_metadata'] = $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
×
752

UNCOV
753
        if ($context['api_denormalize'] ?? false) {
×
UNCOV
754
            return $this->propertyAccessor->getValue($object, $attribute);
×
755
        }
756

UNCOV
757
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
×
758
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
759

760
            foreach ($types as $type) {
×
761
                if (
762
                    $type->isCollection()
×
763
                    && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null)
×
764
                    && ($className = $collectionValueType->getClassName())
×
765
                    && $this->resourceClassResolver->isResourceClass($className)
×
766
                ) {
767
                    $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
768

769
                    // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
770
                    // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
771
                    if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
×
772
                        $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
773
                            operationName: $itemUriTemplate,
×
774
                            forceCollection: true,
×
775
                            httpOperation: true
×
776
                        );
×
777

778
                        return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
779
                    }
780

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

783
                    if (!is_iterable($attributeValue)) {
×
784
                        throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
785
                    }
786

787
                    $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
×
788

789
                    $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
×
790
                    $context['data'] = $data;
×
791
                    $context['type'] = $type;
×
792

793
                    if ($this->tagCollector) {
×
794
                        $this->tagCollector->collect($context);
×
795
                    }
796

797
                    return $data;
×
798
                }
799

800
                if (
801
                    ($className = $type->getClassName())
×
802
                    && $this->resourceClassResolver->isResourceClass($className)
×
803
                ) {
804
                    $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
805
                    unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
×
806
                    if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
×
807
                        $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
808
                            operationName: $uriTemplate,
×
809
                            httpOperation: true
×
810
                        );
×
811

812
                        return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
813
                    }
814

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

817
                    if (!\is_object($attributeValue) && null !== $attributeValue) {
×
818
                        throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
819
                    }
820

821
                    $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
×
822

823
                    $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
×
824
                    $context['data'] = $data;
×
825
                    $context['type'] = $type;
×
826

827
                    if ($this->tagCollector) {
×
828
                        $this->tagCollector->collect($context);
×
829
                    }
830

831
                    return $data;
×
832
                }
833

834
                if (!$this->serializer instanceof NormalizerInterface) {
×
835
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
836
                }
837

838
                unset(
×
839
                    $context['resource_class'],
×
840
                    $context['force_resource_class'],
×
841
                    $context['uri_variables'],
×
842
                );
×
843

844
                // Anonymous resources
845
                if ($className) {
×
846
                    $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
847
                    $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
848

849
                    return $this->serializer->normalize($attributeValue, $format, $childContext);
×
850
                }
851

852
                if ('array' === $type->getBuiltinType()) {
×
853
                    if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) {
×
854
                        $context = $this->createOperationContext($context, $className, $propertyMetadata);
×
855
                    }
856

857
                    $childContext = $this->createChildContext($context, $attribute, $format);
×
858
                    $childContext['output']['gen_id'] ??= $propertyMetadata->getGenId() ?? true;
×
859

860
                    $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
861

862
                    return $this->serializer->normalize($attributeValue, $format, $childContext);
×
863
                }
864
            }
865

866
            if (!$this->serializer instanceof NormalizerInterface) {
×
867
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
868
            }
869

870
            unset(
×
871
                $context['resource_class'],
×
872
                $context['force_resource_class'],
×
873
                $context['uri_variables']
×
874
            );
×
875

876
            $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
877

878
            return $this->serializer->normalize($attributeValue, $format, $context);
×
879
        }
880

UNCOV
881
        $type = $propertyMetadata->getNativeType();
×
882

UNCOV
883
        $nullable = false;
×
UNCOV
884
        if ($type instanceof NullableType) {
×
UNCOV
885
            $type = $type->getWrappedType();
×
UNCOV
886
            $nullable = true;
×
887
        }
888

889
        // TODO check every foreach composite to see if null is an issue
UNCOV
890
        $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]);
×
UNCOV
891
        $className = null;
×
UNCOV
892
        $typeIsResourceClass = function (Type $type) use (&$className): bool {
×
UNCOV
893
            return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
×
UNCOV
894
        };
×
895

UNCOV
896
        foreach ($types as $type) {
×
UNCOV
897
            if ($type instanceof CollectionType && $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass)) {
×
UNCOV
898
                $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
899

900
                // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
901
                // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content
UNCOV
902
                if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
×
903
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
904
                        operationName: $itemUriTemplate,
×
905
                        forceCollection: true,
×
906
                        httpOperation: true
×
907
                    );
×
908

909
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
910
                }
911

UNCOV
912
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
913

UNCOV
914
                if (!is_iterable($attributeValue)) {
×
915
                    throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.');
×
916
                }
917

UNCOV
918
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
×
919

UNCOV
920
                $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
×
UNCOV
921
                $context['data'] = $data;
×
UNCOV
922
                $context['type'] = $nullable ? Type::nullable($type) : $type;
×
923

UNCOV
924
                if ($this->tagCollector) {
×
UNCOV
925
                    $this->tagCollector->collect($context);
×
926
                }
927

UNCOV
928
                return $data;
×
929
            }
930

UNCOV
931
            if ($type->isSatisfiedBy($typeIsResourceClass)) {
×
UNCOV
932
                $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
UNCOV
933
                unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']);
×
UNCOV
934
                if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) {
×
935
                    $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation(
×
936
                        operationName: $uriTemplate,
×
937
                        httpOperation: true
×
938
                    );
×
939

940
                    return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
×
941
                }
942

UNCOV
943
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
944

UNCOV
945
                if (!\is_object($attributeValue) && null !== $attributeValue) {
×
946
                    throw new UnexpectedValueException('Unexpected non-object value for to-one relation.');
×
947
                }
948

UNCOV
949
                $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
×
950

UNCOV
951
                $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
×
UNCOV
952
                $context['data'] = $data;
×
UNCOV
953
                $context['type'] = $nullable ? Type::nullable($type) : $type;
×
954

UNCOV
955
                if ($this->tagCollector) {
×
UNCOV
956
                    $this->tagCollector->collect($context);
×
957
                }
958

UNCOV
959
                return $data;
×
960
            }
961

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

UNCOV
966
            unset(
×
UNCOV
967
                $context['resource_class'],
×
UNCOV
968
                $context['force_resource_class'],
×
UNCOV
969
                $context['uri_variables'],
×
UNCOV
970
            );
×
971

972
            // Anonymous resources
UNCOV
973
            if ($className) {
×
UNCOV
974
                $childContext = $this->createChildContext($this->createOperationContext($context, $className, $propertyMetadata), $attribute, $format);
×
UNCOV
975
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
976

UNCOV
977
                return $this->serializer->normalize($attributeValue, $format, $childContext);
×
978
            }
979

UNCOV
980
            if ($type instanceof CollectionType) {
×
UNCOV
981
                if (($subType = $type->getCollectionValueType()) instanceof ObjectType) {
×
982
                    $context = $this->createOperationContext($context, $subType->getClassName(), $propertyMetadata);
×
983
                }
984

UNCOV
985
                $childContext = $this->createChildContext($context, $attribute, $format);
×
UNCOV
986
                $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
987

UNCOV
988
                return $this->serializer->normalize($attributeValue, $format, $childContext);
×
989
            }
990
        }
991

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

UNCOV
996
        unset(
×
UNCOV
997
            $context['resource_class'],
×
UNCOV
998
            $context['force_resource_class'],
×
UNCOV
999
            $context['uri_variables']
×
UNCOV
1000
        );
×
1001

UNCOV
1002
        $attributeValue = $this->propertyAccessor->getValue($object, $attribute);
×
1003

UNCOV
1004
        return $this->serializer->normalize($attributeValue, $format, $context);
×
1005
    }
1006

1007
    /**
1008
     * Normalizes a collection of relations (to-many).
1009
     *
1010
     * @throws UnexpectedValueException
1011
     */
1012
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
1013
    {
UNCOV
1014
        $value = [];
×
UNCOV
1015
        foreach ($attributeValue as $index => $obj) {
×
UNCOV
1016
            if (!\is_object($obj) && null !== $obj) {
×
1017
                throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
×
1018
            }
1019

1020
            // update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources)
UNCOV
1021
            $objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass);
×
UNCOV
1022
            $context['resource_class'] = $objResourceClass;
×
UNCOV
1023
            if ($this->resourceMetadataCollectionFactory) {
×
UNCOV
1024
                $context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation();
×
1025
            }
1026

UNCOV
1027
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
×
1028
        }
1029

UNCOV
1030
        return $value;
×
1031
    }
1032

1033
    /**
1034
     * Normalizes a relation.
1035
     *
1036
     * @throws LogicException
1037
     * @throws UnexpectedValueException
1038
     */
1039
    protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $relatedObject, string $resourceClass, ?string $format, array $context): \ArrayObject|array|string|null
1040
    {
UNCOV
1041
        if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink() || false === ($context['output']['gen_id'] ?? true)) {
×
UNCOV
1042
            if (!$this->serializer instanceof NormalizerInterface) {
×
1043
                throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
×
1044
            }
1045

UNCOV
1046
            $relatedContext = $this->createOperationContext($context, $resourceClass, $propertyMetadata);
×
UNCOV
1047
            $normalizedRelatedObject = $this->serializer->normalize($relatedObject, $format, $relatedContext);
×
UNCOV
1048
            if (!\is_string($normalizedRelatedObject) && !\is_array($normalizedRelatedObject) && !$normalizedRelatedObject instanceof \ArrayObject && null !== $normalizedRelatedObject) {
×
1049
                throw new UnexpectedValueException('Expected normalized relation to be an IRI, array, \ArrayObject or null');
×
1050
            }
1051

UNCOV
1052
            return $normalizedRelatedObject;
×
1053
        }
1054

UNCOV
1055
        $context['iri'] = $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context);
×
UNCOV
1056
        $context['data'] = $iri;
×
UNCOV
1057
        $context['object'] = $relatedObject;
×
UNCOV
1058
        unset($context['property_metadata'], $context['api_attribute']);
×
1059

UNCOV
1060
        if ($this->tagCollector) {
×
UNCOV
1061
            $this->tagCollector->collect($context);
×
UNCOV
1062
        } elseif (isset($context['resources'])) {
×
1063
            $context['resources'][$iri] = $iri;
×
1064
        }
1065

UNCOV
1066
        $push = $propertyMetadata->getPush() ?? false;
×
UNCOV
1067
        if (isset($context['resources_to_push']) && $push) {
×
1068
            $context['resources_to_push'][$iri] = $iri;
×
1069
        }
1070

UNCOV
1071
        return $iri;
×
1072
    }
1073

1074
    private function createAttributeValue(string $attribute, mixed $value, ?string $format = null, array &$context = []): mixed
1075
    {
1076
        try {
UNCOV
1077
            return $this->createAndValidateAttributeValue($attribute, $value, $format, $context);
×
UNCOV
1078
        } catch (NotNormalizableValueException $exception) {
×
UNCOV
1079
            if (!isset($context['not_normalizable_value_exceptions'])) {
×
1080
                throw $exception;
×
1081
            }
UNCOV
1082
            $context['not_normalizable_value_exceptions'][] = $exception;
×
UNCOV
1083
            throw $exception;
×
1084
        }
1085
    }
1086

1087
    private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed
1088
    {
UNCOV
1089
        $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context));
×
1090

UNCOV
1091
        $type = null;
×
UNCOV
1092
        if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
×
1093
            $types = $propertyMetadata->getBuiltinTypes() ?? [];
×
1094
        } else {
UNCOV
1095
            $type = $propertyMetadata->getNativeType();
×
UNCOV
1096
            $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]);
×
1097
        }
1098

UNCOV
1099
        $className = null;
×
UNCOV
1100
        $typeIsResourceClass = function (Type $type) use (&$className): bool {
×
UNCOV
1101
            return $type instanceof ObjectType ? $this->resourceClassResolver->isResourceClass($className = $type->getClassName()) : false;
×
UNCOV
1102
        };
×
1103

UNCOV
1104
        $isMultipleTypes = \count($types) > 1;
×
UNCOV
1105
        $denormalizationException = null;
×
1106

UNCOV
1107
        foreach ($types as $t) {
×
UNCOV
1108
            if ($type instanceof Type) {
×
UNCOV
1109
                $isNullable = $type->isNullable();
×
1110
            } else {
1111
                $isNullable = $t->isNullable();
×
1112
            }
1113

UNCOV
1114
            if (null === $value && ($isNullable || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) {
×
1115
                return $value;
×
1116
            }
1117

UNCOV
1118
            $collectionValueType = null;
×
1119

UNCOV
1120
            if ($t instanceof CollectionType) {
×
UNCOV
1121
                $collectionValueType = $t->getCollectionValueType();
×
UNCOV
1122
            } elseif ($t instanceof LegacyType) {
×
1123
                $collectionValueType = $t->getCollectionValueTypes()[0] ?? null;
×
1124
            }
1125

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

UNCOV
1136
            if (($collectionValueType instanceof Type && $collectionValueType->isSatisfiedBy($typeIsResourceClass))
×
UNCOV
1137
                || ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className))
×
1138
            ) {
UNCOV
1139
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
×
UNCOV
1140
                $context['resource_class'] = $resourceClass;
×
UNCOV
1141
                unset($context['uri_variables']);
×
1142

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

1152
                        continue;
×
1153
                    }
1154

UNCOV
1155
                    throw $e;
×
1156
                }
1157
            }
1158

1159
            if (
UNCOV
1160
                ($t instanceof Type && $t->isSatisfiedBy($typeIsResourceClass))
×
UNCOV
1161
                || ($t instanceof LegacyType && null !== ($className = $t->getClassName()) && $this->resourceClassResolver->isResourceClass($className))
×
1162
            ) {
UNCOV
1163
                $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className);
×
UNCOV
1164
                $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass, $propertyMetadata), $attribute, $format);
×
1165

1166
                try {
UNCOV
1167
                    return $this->denormalizeRelation($attribute, $propertyMetadata, $resourceClass, $value, $format, $childContext);
×
UNCOV
1168
                } catch (NotNormalizableValueException $e) {
×
1169
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
1170
                    if ($isMultipleTypes) {
×
UNCOV
1171
                        $denormalizationException ??= $e;
×
1172

UNCOV
1173
                        continue;
×
1174
                    }
1175

1176
                    throw $e;
×
1177
                }
1178
            }
1179

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

1189
                unset($context['resource_class'], $context['uri_variables']);
×
1190

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

1198
                        continue;
×
1199
                    }
1200

1201
                    throw $e;
×
1202
                }
1203
            }
1204

UNCOV
1205
            while ($t instanceof WrappingTypeInterface) {
×
1206
                $t = $t->getWrappedType();
×
1207
            }
1208

1209
            if (
UNCOV
1210
                $t instanceof ObjectType
×
UNCOV
1211
                || ($t instanceof LegacyType && null !== $t->getClassName())
×
1212
            ) {
UNCOV
1213
                if (!$this->serializer instanceof DenormalizerInterface) {
×
1214
                    throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
×
1215
                }
1216

UNCOV
1217
                unset($context['resource_class'], $context['uri_variables']);
×
1218

1219
                try {
UNCOV
1220
                    return $this->serializer->denormalize($value, $t->getClassName(), $format, $context);
×
UNCOV
1221
                } catch (NotNormalizableValueException $e) {
×
1222
                    // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
UNCOV
1223
                    if ($isMultipleTypes) {
×
UNCOV
1224
                        $denormalizationException ??= $e;
×
1225

UNCOV
1226
                        continue;
×
1227
                    }
1228

1229
                    throw $e;
×
1230
                }
1231
            }
1232

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

1245
                $typeIdentifier = $t instanceof BuiltinType ? $t->getTypeIdentifier() : TypeIdentifier::tryFrom($t->getBuiltinType());
×
1246

1247
                switch ($typeIdentifier) {
1248
                    case TypeIdentifier::BOOL:
×
1249
                        // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
1250
                        if ('false' === $value || '0' === $value) {
×
1251
                            $value = false;
×
1252
                        } elseif ('true' === $value || '1' === $value) {
×
1253
                            $value = true;
×
1254
                        } else {
1255
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1256
                            if ($isMultipleTypes) {
×
1257
                                break 2;
×
1258
                            }
1259
                            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);
×
1260
                        }
1261
                        break;
×
1262
                    case TypeIdentifier::INT:
×
1263
                        if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) {
×
1264
                            $value = (int) $value;
×
1265
                        } else {
1266
                            // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1267
                            if ($isMultipleTypes) {
×
1268
                                break 2;
×
1269
                            }
1270
                            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);
×
1271
                        }
1272
                        break;
×
1273
                    case TypeIdentifier::FLOAT:
×
1274
                        if (is_numeric($value)) {
×
1275
                            return (float) $value;
×
1276
                        }
1277

1278
                        switch ($value) {
1279
                            case 'NaN':
×
1280
                                return \NAN;
×
1281
                            case 'INF':
×
1282
                                return \INF;
×
1283
                            case '-INF':
×
1284
                                return -\INF;
×
1285
                            default:
1286
                                // union/intersect types: try the next type, if not valid, an exception will be thrown at the end
1287
                                if ($isMultipleTypes) {
×
1288
                                    break 3;
×
1289
                                }
1290
                                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);
×
1291
                        }
1292
                }
1293
            }
1294

UNCOV
1295
            if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
×
1296
                return $value;
×
1297
            }
1298

1299
            try {
UNCOV
1300
                $t instanceof Type
×
UNCOV
1301
                    ? $this->validateAttributeType($attribute, $t, $value, $format, $context)
×
1302
                    : $this->validateType($attribute, $t, $value, $format, $context);
×
1303

UNCOV
1304
                $denormalizationException = null;
×
UNCOV
1305
                break;
×
UNCOV
1306
            } catch (NotNormalizableValueException $e) {
×
1307
                // union/intersect types: try the next type
UNCOV
1308
                if (!$isMultipleTypes) {
×
1309
                    throw $e;
×
1310
                }
1311

UNCOV
1312
                $denormalizationException ??= $e;
×
1313
            }
1314
        }
1315

UNCOV
1316
        if ($denormalizationException) {
×
UNCOV
1317
            if ($type instanceof Type && $type->isSatisfiedBy(static fn ($type) => $type instanceof BuiltinType) && !$type->isSatisfiedBy($typeIsResourceClass)) {
×
UNCOV
1318
                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);
×
1319
            }
1320

UNCOV
1321
            throw $denormalizationException;
×
1322
        }
1323

UNCOV
1324
        return $value;
×
1325
    }
1326

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