|
| 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\Elasticsearch\Serializer; |
| 15 | + |
| 16 | +use ApiPlatform\Metadata\ApiProperty; |
| 17 | +use ApiPlatform\Metadata\HttpOperation; |
| 18 | +use ApiPlatform\Metadata\IriConverterInterface; |
| 19 | +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
| 20 | +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
| 21 | +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; |
| 22 | +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; |
| 23 | +use ApiPlatform\Metadata\ResourceClassResolverInterface; |
| 24 | +use ApiPlatform\Serializer\AbstractItemNormalizer; |
| 25 | +use ApiPlatform\Serializer\TagCollectorInterface; |
| 26 | +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
| 27 | +use Symfony\Component\Serializer\Exception\LogicException; |
| 28 | +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; |
| 29 | +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
| 30 | +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; |
| 31 | + |
| 32 | +/** |
| 33 | + * Item denormalizer for Elasticsearch. |
| 34 | + * |
| 35 | + * @experimental |
| 36 | + * |
| 37 | + * @author Baptiste Meyer <baptiste.meyer@gmail.com> |
| 38 | + */ |
| 39 | +final class ItemDenormalizer extends AbstractItemNormalizer |
| 40 | +{ |
| 41 | + public const FORMAT = 'elasticsearch'; |
| 42 | + |
| 43 | + public function __construct( |
| 44 | + PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, |
| 45 | + PropertyMetadataFactoryInterface $propertyMetadataFactory, |
| 46 | + IriConverterInterface $iriConverter, |
| 47 | + ResourceClassResolverInterface $resourceClassResolver, |
| 48 | + ?PropertyAccessorInterface $propertyAccessor = null, |
| 49 | + ?NameConverterInterface $nameConverter = null, |
| 50 | + ?ClassMetadataFactoryInterface $classMetadataFactory = null, |
| 51 | + array $defaultContext = [], |
| 52 | + ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, |
| 53 | + ?ResourceAccessCheckerInterface $resourceAccessChecker = null, |
| 54 | + ?TagCollectorInterface $tagCollector = null, |
| 55 | + ) { |
| 56 | + parent::__construct( |
| 57 | + $propertyNameCollectionFactory, |
| 58 | + $propertyMetadataFactory, |
| 59 | + $iriConverter, |
| 60 | + $resourceClassResolver, |
| 61 | + $propertyAccessor, |
| 62 | + $nameConverter, |
| 63 | + $classMetadataFactory, |
| 64 | + $defaultContext, |
| 65 | + $resourceMetadataCollectionFactory, |
| 66 | + $resourceAccessChecker, |
| 67 | + $tagCollector |
| 68 | + ); |
| 69 | + } |
| 70 | + |
| 71 | + /** |
| 72 | + * {@inheritdoc} |
| 73 | + */ |
| 74 | + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool |
| 75 | + { |
| 76 | + return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context); |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * {@inheritdoc} |
| 81 | + */ |
| 82 | + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed |
| 83 | + { |
| 84 | + // Handle Elasticsearch document structure with _id and _source |
| 85 | + if (\is_array($data) && \is_string($data['_id'] ?? null) && \is_array($data['_source'] ?? null)) { |
| 86 | + $data = $this->populateIdentifier($data, $type)['_source']; |
| 87 | + } |
| 88 | + |
| 89 | + return parent::denormalize($data, $type, $format, $context); |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Populates the resource identifier with the document identifier if not present in the original JSON document. |
| 94 | + */ |
| 95 | + private function populateIdentifier(array $data, string $class): array |
| 96 | + { |
| 97 | + $identifier = 'id'; |
| 98 | + $resourceMetadata = $this->resourceMetadataCollectionFactory->create($class); |
| 99 | + |
| 100 | + $operation = $resourceMetadata->getOperation(); |
| 101 | + if ($operation instanceof HttpOperation) { |
| 102 | + $uriVariable = $operation->getUriVariables()[0] ?? null; |
| 103 | + |
| 104 | + if ($uriVariable) { |
| 105 | + $identifier = $uriVariable->getIdentifiers()[0] ?? 'id'; |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + $identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier, $class, self::FORMAT); |
| 110 | + |
| 111 | + if (!isset($data['_source'][$identifier])) { |
| 112 | + $data['_source'][$identifier] = $data['_id']; |
| 113 | + } |
| 114 | + |
| 115 | + return $data; |
| 116 | + } |
| 117 | + |
| 118 | + /** |
| 119 | + * {@inheritdoc} |
| 120 | + */ |
| 121 | + public function getSupportedTypes(?string $format): array |
| 122 | + { |
| 123 | + return self::FORMAT === $format ? ['object' => true] : []; |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * {@inheritdoc} |
| 128 | + * |
| 129 | + * For Elasticsearch, we always allow nested documents because that's how ES stores and returns data. |
| 130 | + */ |
| 131 | + protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object |
| 132 | + { |
| 133 | + // For Elasticsearch, always allow nested documents |
| 134 | + $context['api_allow_update'] = true; |
| 135 | + |
| 136 | + if (!$this->serializer instanceof DenormalizerInterface) { |
| 137 | + throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); |
| 138 | + } |
| 139 | + |
| 140 | + $item = $this->serializer->denormalize($value, $className, $format, $context); |
| 141 | + if (!\is_object($item) && null !== $item) { |
| 142 | + throw new \UnexpectedValueException('Expected item to be an object or null.'); |
| 143 | + } |
| 144 | + |
| 145 | + return $item; |
| 146 | + } |
| 147 | +} |
0 commit comments