vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 151

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the API Platform project.
  4.  *
  5.  * (c) Kévin Dunglas <dunglas@gmail.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Core\Serializer;
  12. use ApiPlatform\Core\Api\IriConverterInterface;
  13. use ApiPlatform\Core\Api\ResourceClassResolverInterface;
  14. use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
  15. use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
  16. use ApiPlatform\Core\Exception\InvalidArgumentException;
  17. use ApiPlatform\Core\Exception\InvalidValueException;
  18. use ApiPlatform\Core\Exception\ItemNotFoundException;
  19. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  20. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  21. use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
  22. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  23. use ApiPlatform\Core\Util\ClassInfoTrait;
  24. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  25. use Symfony\Component\PropertyAccess\PropertyAccess;
  26. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  27. use Symfony\Component\PropertyInfo\Type;
  28. use Symfony\Component\Serializer\Exception\LogicException;
  29. use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
  30. use Symfony\Component\Serializer\Exception\RuntimeException;
  31. use Symfony\Component\Serializer\Exception\UnexpectedValueException;
  32. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  33. use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
  34. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  35. use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
  36. use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
  37. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  38. /**
  39.  * Base item normalizer.
  40.  *
  41.  * @author Kévin Dunglas <dunglas@gmail.com>
  42.  */
  43. abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
  44. {
  45.     use ClassInfoTrait;
  46.     use ContextTrait;
  47.     use InputOutputMetadataTrait;
  48.     protected $propertyNameCollectionFactory;
  49.     protected $propertyMetadataFactory;
  50.     protected $iriConverter;
  51.     protected $resourceClassResolver;
  52.     protected $propertyAccessor;
  53.     protected $itemDataProvider;
  54.     protected $allowPlainIdentifiers;
  55.     protected $dataTransformers = [];
  56.     protected $localCache = [];
  57.     public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactoryPropertyMetadataFactoryInterface $propertyMetadataFactoryIriConverterInterface $iriConverterResourceClassResolverInterface $resourceClassResolverPropertyAccessorInterface $propertyAccessor nullNameConverterInterface $nameConverter nullClassMetadataFactoryInterface $classMetadataFactory nullItemDataProviderInterface $itemDataProvider nullbool $allowPlainIdentifiers false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory null)
  58.     {
  59.         if (!isset($defaultContext['circular_reference_handler'])) {
  60.             $defaultContext['circular_reference_handler'] = function ($object) {
  61.                 return $this->iriConverter->getIriFromItem($object);
  62.             };
  63.         }
  64.         if (!interface_exists(AdvancedNameConverterInterface::class)) {
  65.             $this->setCircularReferenceHandler($defaultContext['circular_reference_handler']);
  66.         }
  67.         parent::__construct($classMetadataFactory$nameConverternullnull, \Closure::fromCallable([$this'getObjectClass']), $defaultContext);
  68.         $this->propertyNameCollectionFactory $propertyNameCollectionFactory;
  69.         $this->propertyMetadataFactory $propertyMetadataFactory;
  70.         $this->iriConverter $iriConverter;
  71.         $this->resourceClassResolver $resourceClassResolver;
  72.         $this->propertyAccessor $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
  73.         $this->itemDataProvider $itemDataProvider;
  74.         $this->allowPlainIdentifiers $allowPlainIdentifiers;
  75.         $this->dataTransformers $dataTransformers;
  76.         $this->resourceMetadataFactory $resourceMetadataFactory;
  77.     }
  78.     /**
  79.      * {@inheritdoc}
  80.      */
  81.     public function supportsNormalization($data$format null)
  82.     {
  83.         if (!\is_object($data) || $data instanceof \Traversable) {
  84.             return false;
  85.         }
  86.         return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
  87.     }
  88.     /**
  89.      * {@inheritdoc}
  90.      */
  91.     public function hasCacheableSupportsMethod(): bool
  92.     {
  93.         return true;
  94.     }
  95.     /**
  96.      * {@inheritdoc}
  97.      *
  98.      * @throws LogicException
  99.      */
  100.     public function normalize($object$format null, array $context = [])
  101.     {
  102.         if ($object !== $transformed $this->transformOutput($object$context)) {
  103.             if (!$this->serializer instanceof NormalizerInterface) {
  104.                 throw new LogicException('Cannot normalize the output because the injected serializer is not a normalizer');
  105.             }
  106.             $context['api_normalize'] = true;
  107.             $context['api_resource'] = $object;
  108.             unset($context['output']);
  109.             unset($context['resource_class']);
  110.             return $this->serializer->normalize($transformed$format$context);
  111.         }
  112.         $resourceClass $this->resourceClassResolver->getResourceClass($object$context['resource_class'] ?? null);
  113.         $context $this->initContext($resourceClass$context);
  114.         $iri $context['iri'] ?? $this->iriConverter->getIriFromItem($object);
  115.         $context['iri'] = $iri;
  116.         $context['api_normalize'] = true;
  117.         /*
  118.          * When true, converts the normalized data array of a resource into an
  119.          * IRI, if the normalized data array is empty.
  120.          *
  121.          * This is useful when traversing from a non-resource towards an attribute
  122.          * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}.
  123.          *
  124.          * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink}
  125.          * should take effect.
  126.          */
  127.         $emptyResourceAsIri $context['api_empty_resource_as_iri'] ?? false;
  128.         unset($context['api_empty_resource_as_iri']);
  129.         if (isset($context['resources'])) {
  130.             $context['resources'][$iri] = $iri;
  131.         }
  132.         $data parent::normalize($object$format$context);
  133.         if ($emptyResourceAsIri && \is_array($data) && === \count($data)) {
  134.             return $iri;
  135.         }
  136.         return $data;
  137.     }
  138.     /**
  139.      * {@inheritdoc}
  140.      */
  141.     public function supportsDenormalization($data$type$format null)
  142.     {
  143.         return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
  144.     }
  145.     /**
  146.      * {@inheritdoc}
  147.      */
  148.     public function denormalize($data$class$format null, array $context = [])
  149.     {
  150.         $resourceClass $this->resourceClassResolver->getResourceClass(null$class);
  151.         $context['api_denormalize'] = true;
  152.         $context['resource_class'] = $resourceClass;
  153.         if (null !== ($inputClass $this->getInputClass($resourceClass$context)) && null !== ($dataTransformer $this->getDataTransformer($data$resourceClass$context))) {
  154.             $dataTransformerContext $context;
  155.             unset($context['input']);
  156.             unset($context['resource_class']);
  157.             if (!$this->serializer instanceof DenormalizerInterface) {
  158.                 throw new LogicException('Cannot denormalize the input because the injected serializer is not a denormalizer');
  159.             }
  160.             $denormalizedInput $this->serializer->denormalize($data$inputClass$format$context);
  161.             if (!\is_object($denormalizedInput)) {
  162.                 throw new \UnexpectedValueException('Expected denormalized input to be an object.');
  163.             }
  164.             return $dataTransformer->transform($denormalizedInput$resourceClass$dataTransformerContext);
  165.         }
  166.         $supportsPlainIdentifiers $this->supportsPlainIdentifiers();
  167.         if (\is_string($data)) {
  168.             try {
  169.                 return $this->iriConverter->getItemFromIri($data$context + ['fetch_data' => true]);
  170.             } catch (ItemNotFoundException $e) {
  171.                 if (!$supportsPlainIdentifiers) {
  172.                     throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
  173.                 }
  174.             } catch (InvalidArgumentException $e) {
  175.                 if (!$supportsPlainIdentifiers) {
  176.                     throw new UnexpectedValueException(sprintf('Invalid IRI "%s".'$data), $e->getCode(), $e);
  177.                 }
  178.             }
  179.         }
  180.         if (!\is_array($data)) {
  181.             if (!$supportsPlainIdentifiers) {
  182.                 throw new UnexpectedValueException(sprintf('Expected IRI or document for resource "%s", "%s" given.'$resourceClass, \gettype($data)));
  183.             }
  184.             $item $this->itemDataProvider->getItem($resourceClass$datanull$context + ['fetch_data' => true]);
  185.             if (null === $item) {
  186.                 throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".'$resourceClass$data));
  187.             }
  188.             return $item;
  189.         }
  190.         return parent::denormalize($data$resourceClass$format$context);
  191.     }
  192.     /**
  193.      * Method copy-pasted from symfony/serializer.
  194.      * Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263.
  195.      *
  196.      * {@inheritdoc}
  197.      *
  198.      * @internal
  199.      */
  200.     protected function instantiateObject(array &$data$class, array &$context, \ReflectionClass $reflectionClass$allowedAttributesstring $format null)
  201.     {
  202.         if (null !== $object $this->extractObjectToPopulate($class$context, static::OBJECT_TO_POPULATE)) {
  203.             unset($context[static::OBJECT_TO_POPULATE]);
  204.             return $object;
  205.         }
  206.         if ($this->classDiscriminatorResolver && $mapping $this->classDiscriminatorResolver->getMappingForClass($class)) {
  207.             if (!isset($data[$mapping->getTypeProperty()])) {
  208.                 throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"'$mapping->getTypeProperty(), $class));
  209.             }
  210.             $type $data[$mapping->getTypeProperty()];
  211.             if (null === ($mappedClass $mapping->getClassForType($type))) {
  212.                 throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"'$type$class));
  213.             }
  214.             $class $mappedClass;
  215.             $reflectionClass = new \ReflectionClass($class);
  216.         }
  217.         $constructor $this->getConstructor($data$class$context$reflectionClass$allowedAttributes);
  218.         if ($constructor) {
  219.             $constructorParameters $constructor->getParameters();
  220.             $params = [];
  221.             foreach ($constructorParameters as $constructorParameter) {
  222.                 $paramName $constructorParameter->name;
  223.                 $key $this->nameConverter $this->nameConverter->normalize($paramName$class$format$context) : $paramName;
  224.                 $allowed false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName$allowedAttributestrue));
  225.                 $ignored = !$this->isAllowedAttribute($class$paramName$format$context);
  226.                 if ($constructorParameter->isVariadic()) {
  227.                     if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key$data))) {
  228.                         if (!\is_array($data[$paramName])) {
  229.                             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));
  230.                         }
  231.                         $params array_merge($params$data[$paramName]);
  232.                     }
  233.                 } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key$data))) {
  234.                     $params[] = $this->createConstructorArgument($data[$key], $key$constructorParameter$context$format);
  235.                     // Don't run set for a parameter passed to the constructor
  236.                     unset($data[$key]);
  237.                 } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
  238.                     $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
  239.                 } elseif ($constructorParameter->isDefaultValueAvailable()) {
  240.                     $params[] = $constructorParameter->getDefaultValue();
  241.                 } else {
  242.                     throw new MissingConstructorArgumentsException(
  243.                         sprintf(
  244.                             'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
  245.                             $class,
  246.                             $constructorParameter->name
  247.                         )
  248.                     );
  249.                 }
  250.             }
  251.             if ($constructor->isConstructor()) {
  252.                 return $reflectionClass->newInstanceArgs($params);
  253.             }
  254.             return $constructor->invokeArgs(null$params);
  255.         }
  256.         return new $class();
  257.     }
  258.     /**
  259.      * {@inheritdoc}
  260.      */
  261.     protected function createConstructorArgument($parameterDatastring $key, \ReflectionParameter $constructorParameter, array &$contextstring $format null)
  262.     {
  263.         return $this->createAttributeValue($constructorParameter->name$parameterData$format$context);
  264.     }
  265.     /**
  266.      * {@inheritdoc}
  267.      *
  268.      * Unused in this context.
  269.      */
  270.     protected function extractAttributes($object$format null, array $context = [])
  271.     {
  272.         return [];
  273.     }
  274.     /**
  275.      * {@inheritdoc}
  276.      */
  277.     protected function getAllowedAttributes($classOrObject, array $context$attributesAsString false)
  278.     {
  279.         $options $this->getFactoryOptions($context);
  280.         $propertyNames $this->propertyNameCollectionFactory->create($context['resource_class'], $options);
  281.         $allowedAttributes = [];
  282.         foreach ($propertyNames as $propertyName) {
  283.             $propertyMetadata $this->propertyMetadataFactory->create($context['resource_class'], $propertyName$options);
  284.             if (
  285.                 $this->isAllowedAttribute($classOrObject$propertyNamenull$context) &&
  286.                 (
  287.                     isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
  288.                     isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !\is_object($classOrObject) && $propertyMetadata->isInitializable())
  289.                 )
  290.             ) {
  291.                 $allowedAttributes[] = $propertyName;
  292.             }
  293.         }
  294.         return $allowedAttributes;
  295.     }
  296.     /**
  297.      * {@inheritdoc}
  298.      */
  299.     protected function setAttributeValue($object$attribute$value$format null, array $context = [])
  300.     {
  301.         $this->setValue($object$attribute$this->createAttributeValue($attribute$value$format$context));
  302.     }
  303.     /**
  304.      * Validates the type of the value. Allows using integers as floats for JSON formats.
  305.      *
  306.      * @throws InvalidArgumentException
  307.      */
  308.     protected function validateType(string $attributeType $type$valuestring $format null)
  309.     {
  310.         $builtinType $type->getBuiltinType();
  311.         if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && false !== strpos($format'json')) {
  312.             $isValid = \is_float($value) || \is_int($value);
  313.         } else {
  314.             $isValid = \call_user_func('is_'.$builtinType$value);
  315.         }
  316.         if (!$isValid) {
  317.             throw new InvalidArgumentException(sprintf(
  318.                 'The type of the "%s" attribute must be "%s", "%s" given.'$attribute$builtinType, \gettype($value)
  319.             ));
  320.         }
  321.     }
  322.     /**
  323.      * Denormalizes a collection of objects.
  324.      *
  325.      * @throws InvalidArgumentException
  326.      */
  327.     protected function denormalizeCollection(string $attributePropertyMetadata $propertyMetadataType $typestring $className$value, ?string $format, array $context): array
  328.     {
  329.         if (!\is_array($value)) {
  330.             throw new InvalidArgumentException(sprintf(
  331.                 'The type of the "%s" attribute must be "array", "%s" given.'$attribute, \gettype($value)
  332.             ));
  333.         }
  334.         $collectionKeyType $type->getCollectionKeyType();
  335.         $collectionKeyBuiltinType null === $collectionKeyType null $collectionKeyType->getBuiltinType();
  336.         $values = [];
  337.         foreach ($value as $index => $obj) {
  338.             if (null !== $collectionKeyBuiltinType && !\call_user_func('is_'.$collectionKeyBuiltinType$index)) {
  339.                 throw new InvalidArgumentException(sprintf(
  340.                         'The type of the key "%s" must be "%s", "%s" given.',
  341.                         $index$collectionKeyBuiltinType, \gettype($index))
  342.                 );
  343.             }
  344.             $values[$index] = $this->denormalizeRelation($attribute$propertyMetadata$className$obj$format$this->createChildContext($context$attribute$format));
  345.         }
  346.         return $values;
  347.     }
  348.     /**
  349.      * Denormalizes a relation.
  350.      *
  351.      * @throws LogicException
  352.      * @throws UnexpectedValueException
  353.      * @throws ItemNotFoundException
  354.      *
  355.      * @return object|null
  356.      */
  357.     protected function denormalizeRelation(string $attributeNamePropertyMetadata $propertyMetadatastring $className$value, ?string $format, array $context)
  358.     {
  359.         $supportsPlainIdentifiers $this->supportsPlainIdentifiers();
  360.         if (\is_string($value)) {
  361.             try {
  362.                 return $this->iriConverter->getItemFromIri($value$context + ['fetch_data' => true]);
  363.             } catch (ItemNotFoundException $e) {
  364.                 if (!$supportsPlainIdentifiers) {
  365.                     throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
  366.                 }
  367.             } catch (InvalidArgumentException $e) {
  368.                 if (!$supportsPlainIdentifiers) {
  369.                     throw new UnexpectedValueException(sprintf('Invalid IRI "%s".'$value), $e->getCode(), $e);
  370.                 }
  371.             }
  372.         }
  373.         if ($propertyMetadata->isWritableLink()) {
  374.             $context['api_allow_update'] = true;
  375.             if (!$this->serializer instanceof DenormalizerInterface) {
  376.                 throw new LogicException(sprintf('The injected serializer must be an instance of "%s".'DenormalizerInterface::class));
  377.             }
  378.             try {
  379.                 $item $this->serializer->denormalize($value$className$format$context);
  380.                 if (!\is_object($item) && null !== $item) {
  381.                     throw new \UnexpectedValueException('Expected item to be an object or null.');
  382.                 }
  383.                 return $item;
  384.             } catch (InvalidValueException $e) {
  385.                 if (!$supportsPlainIdentifiers) {
  386.                     throw $e;
  387.                 }
  388.             }
  389.         }
  390.         if (!\is_array($value)) {
  391.             if (!$supportsPlainIdentifiers) {
  392.                 throw new UnexpectedValueException(sprintf(
  393.                     'Expected IRI or nested document for attribute "%s", "%s" given.'$attributeName, \gettype($value)
  394.                 ));
  395.             }
  396.             $item $this->itemDataProvider->getItem($className$valuenull$context + ['fetch_data' => true]);
  397.             if (null === $item) {
  398.                 throw new ItemNotFoundException(sprintf('Item not found for resource "%s" with id "%s".'$className$value));
  399.             }
  400.             return $item;
  401.         }
  402.         throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.'$attributeName));
  403.     }
  404.     /**
  405.      * Gets a valid context for property name collection / property metadata factories.
  406.      */
  407.     protected function getFactoryOptions(array $context): array
  408.     {
  409.         $options = [];
  410.         if (isset($context[self::GROUPS])) {
  411.             /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */
  412.             $options['serializer_groups'] = $context[self::GROUPS];
  413.         }
  414.         if (isset($context['collection_operation_name'])) {
  415.             $options['collection_operation_name'] = $context['collection_operation_name'];
  416.         }
  417.         if (isset($context['item_operation_name'])) {
  418.             $options['item_operation_name'] = $context['item_operation_name'];
  419.         }
  420.         return $options;
  421.     }
  422.     /**
  423.      * Creates the context to use when serializing a relation.
  424.      *
  425.      * @deprecated since version 2.1, to be removed in 3.0.
  426.      */
  427.     protected function createRelationSerializationContext(string $resourceClass, array $context): array
  428.     {
  429.         @trigger_error(sprintf('The method %s() is deprecated since 2.1 and will be removed in 3.0.'__METHOD__), E_USER_DEPRECATED);
  430.         return $context;
  431.     }
  432.     /**
  433.      * {@inheritdoc}
  434.      *
  435.      * @throws NoSuchPropertyException
  436.      * @throws LogicException
  437.      */
  438.     protected function getAttributeValue($object$attribute$format null, array $context = [])
  439.     {
  440.         $context['api_attribute'] = $attribute;
  441.         $propertyMetadata $this->propertyMetadataFactory->create($context['resource_class'], $attribute$this->getFactoryOptions($context));
  442.         try {
  443.             $attributeValue $this->propertyAccessor->getValue($object$attribute);
  444.         } catch (NoSuchPropertyException $e) {
  445.             if (!$propertyMetadata->hasChildInherited()) {
  446.                 throw $e;
  447.             }
  448.             $attributeValue null;
  449.         }
  450.         $type $propertyMetadata->getType();
  451.         if (
  452.             is_iterable($attributeValue) &&
  453.             $type &&
  454.             $type->isCollection() &&
  455.             ($collectionValueType $type->getCollectionValueType()) &&
  456.             ($className $collectionValueType->getClassName()) &&
  457.             $this->resourceClassResolver->isResourceClass($className)
  458.         ) {
  459.             $resourceClass $this->resourceClassResolver->getResourceClass($attributeValue$className);
  460.             $childContext $this->createChildContext($context$attribute$format);
  461.             $childContext['resource_class'] = $resourceClass;
  462.             unset($childContext['iri']);
  463.             return $this->normalizeCollectionOfRelations($propertyMetadata$attributeValue$resourceClass$format$childContext);
  464.         }
  465.         if (
  466.             $type &&
  467.             ($className $type->getClassName()) &&
  468.             $this->resourceClassResolver->isResourceClass($className)
  469.         ) {
  470.             $resourceClass $this->resourceClassResolver->getResourceClass($attributeValue$className);
  471.             $childContext $this->createChildContext($context$attribute$format);
  472.             $childContext['resource_class'] = $resourceClass;
  473.             unset($childContext['iri']);
  474.             return $this->normalizeRelation($propertyMetadata$attributeValue$resourceClass$format$childContext);
  475.         }
  476.         if (!$this->serializer instanceof NormalizerInterface) {
  477.             throw new LogicException(sprintf('The injected serializer must be an instance of "%s".'NormalizerInterface::class));
  478.         }
  479.         unset($context['resource_class']);
  480.         return $this->serializer->normalize($attributeValue$format$context);
  481.     }
  482.     /**
  483.      * Normalizes a collection of relations (to-many).
  484.      *
  485.      * @param iterable $attributeValue
  486.      */
  487.     protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata$attributeValuestring $resourceClass, ?string $format, array $context): array
  488.     {
  489.         $value = [];
  490.         foreach ($attributeValue as $index => $obj) {
  491.             $value[$index] = $this->normalizeRelation($propertyMetadata$obj$resourceClass$format$context);
  492.         }
  493.         return $value;
  494.     }
  495.     /**
  496.      * Normalizes a relation as an object if is a Link or as an URI.
  497.      *
  498.      * @throws LogicException
  499.      *
  500.      * @return string|array
  501.      */
  502.     protected function normalizeRelation(PropertyMetadata $propertyMetadata$relatedObjectstring $resourceClass, ?string $format, array $context)
  503.     {
  504.         if (null === $relatedObject || !empty($context['attributes']) || $propertyMetadata->isReadableLink()) {
  505.             if (!$this->serializer instanceof NormalizerInterface) {
  506.                 throw new LogicException(sprintf('The injected serializer must be an instance of "%s".'NormalizerInterface::class));
  507.             }
  508.             return $this->serializer->normalize($relatedObject$format$context);
  509.         }
  510.         $iri $this->iriConverter->getIriFromItem($relatedObject);
  511.         if (isset($context['resources'])) {
  512.             $context['resources'][$iri] = $iri;
  513.         }
  514.         if (isset($context['resources_to_push']) && $propertyMetadata->getAttribute('push'false)) {
  515.             $context['resources_to_push'][$iri] = $iri;
  516.         }
  517.         return $iri;
  518.     }
  519.     /**
  520.      * Finds the first supported data transformer if any.
  521.      *
  522.      * @param object|array $data object on normalize / array on denormalize
  523.      */
  524.     protected function getDataTransformer($datastring $to, array $context = []): ?DataTransformerInterface
  525.     {
  526.         foreach ($this->dataTransformers as $dataTransformer) {
  527.             if ($dataTransformer->supportsTransformation($data$to$context)) {
  528.                 return $dataTransformer;
  529.             }
  530.         }
  531.         return null;
  532.     }
  533.     /**
  534.      * For a given resource, it returns an output representation if any
  535.      * If not, the resource is returned.
  536.      */
  537.     protected function transformOutput($object, array $context = [])
  538.     {
  539.         $outputClass $this->getOutputClass($this->getObjectClass($object), $context);
  540.         if (null !== $outputClass && null !== $dataTransformer $this->getDataTransformer($object$outputClass$context)) {
  541.             return $dataTransformer->transform($object$outputClass$context);
  542.         }
  543.         return $object;
  544.     }
  545.     private function createAttributeValue($attribute$value$format null, array $context = [])
  546.     {
  547.         $propertyMetadata $this->propertyMetadataFactory->create($context['resource_class'], $attribute$this->getFactoryOptions($context));
  548.         $type $propertyMetadata->getType();
  549.         if (null === $type) {
  550.             // No type provided, blindly return the value
  551.             return $value;
  552.         }
  553.         if (null === $value && $type->isNullable()) {
  554.             return $value;
  555.         }
  556.         if (
  557.             $type->isCollection() &&
  558.             null !== ($collectionValueType $type->getCollectionValueType()) &&
  559.             null !== ($className $collectionValueType->getClassName()) &&
  560.             $this->resourceClassResolver->isResourceClass($className)
  561.         ) {
  562.             $resourceClass $this->resourceClassResolver->getResourceClass(null$className);
  563.             $context['resource_class'] = $resourceClass;
  564.             return $this->denormalizeCollection($attribute$propertyMetadata$type$resourceClass$value$format$context);
  565.         }
  566.         if (
  567.             null !== ($className $type->getClassName()) &&
  568.             $this->resourceClassResolver->isResourceClass($className)
  569.         ) {
  570.             $resourceClass $this->resourceClassResolver->getResourceClass(null$className);
  571.             $childContext $this->createChildContext($context$attribute$format);
  572.             $childContext['resource_class'] = $resourceClass;
  573.             return $this->denormalizeRelation($attribute$propertyMetadata$resourceClass$value$format$childContext);
  574.         }
  575.         if (
  576.             $type->isCollection() &&
  577.             null !== ($collectionValueType $type->getCollectionValueType()) &&
  578.             null !== ($className $collectionValueType->getClassName())
  579.         ) {
  580.             if (!$this->serializer instanceof DenormalizerInterface) {
  581.                 throw new LogicException(sprintf('The injected serializer must be an instance of "%s".'DenormalizerInterface::class));
  582.             }
  583.             unset($context['resource_class']);
  584.             return $this->serializer->denormalize($value$className.'[]'$format$context);
  585.         }
  586.         if (null !== $className $type->getClassName()) {
  587.             if (!$this->serializer instanceof DenormalizerInterface) {
  588.                 throw new LogicException(sprintf('The injected serializer must be an instance of "%s".'DenormalizerInterface::class));
  589.             }
  590.             unset($context['resource_class']);
  591.             return $this->serializer->denormalize($value$className$format$context);
  592.         }
  593.         if ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  594.             return $value;
  595.         }
  596.         $this->validateType($attribute$type$value$format);
  597.         return $value;
  598.     }
  599.     /**
  600.      * Sets a value of the object using the PropertyAccess component.
  601.      *
  602.      * @param object $object
  603.      */
  604.     private function setValue($objectstring $attributeName$value)
  605.     {
  606.         try {
  607.             $this->propertyAccessor->setValue($object$attributeName$value);
  608.         } catch (NoSuchPropertyException $exception) {
  609.             // Properties not found are ignored
  610.         }
  611.     }
  612.     private function supportsPlainIdentifiers(): bool
  613.     {
  614.         return $this->allowPlainIdentifiers && null !== $this->itemDataProvider;
  615.     }
  616. }