From f22fa73f41663f2c6a2391d3c1b8623098a51a0d Mon Sep 17 00:00:00 2001 From: Vincent Chalamon Date: Tue, 20 Dec 2022 14:23:29 +0100 Subject: [PATCH] fix(elasticsearch): elasticsearch BC --- .../Extension/AbstractFilterExtension.php | 58 ++++++++++++- .../ConstantScoreFilterExtension.php | 35 +++++++- .../DataProvider/Extension/SortExtension.php | 84 +++++++++++++++++- .../Extension/SortFilterExtension.php | 30 ++++++- .../Serializer/DocumentNormalizer.php | 22 ----- .../Serializer/ItemNormalizer.php | 86 ++++++++++++++++++- .../Serializer/DocumentNormalizer.php | 2 - .../Serializer/ItemNormalizer.php | 2 - .../Bundle/Resources/config/elasticsearch.xml | 16 ---- .../Resources/config/legacy/elasticsearch.xml | 12 +++ .../Resources/config/v3/elasticsearch.xml | 16 ++++ src/deprecation.php | 2 - 12 files changed, 306 insertions(+), 59 deletions(-) delete mode 100644 src/Core/Bridge/Elasticsearch/Serializer/DocumentNormalizer.php diff --git a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/AbstractFilterExtension.php b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/AbstractFilterExtension.php index 4db73e50a48..e01d219795c 100644 --- a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/AbstractFilterExtension.php +++ b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/AbstractFilterExtension.php @@ -13,10 +13,62 @@ namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension; -class_exists(\ApiPlatform\Elasticsearch\Extension\AbstractFilterExtension::class); +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use Psr\Container\ContainerInterface; -if (false) { - class AbstractFilterExtension extends \ApiPlatform\Elasticsearch\Extension\AbstractFilterExtension +/** + * Abstract class for easing the implementation of a filter extension. + * + * @experimental + * + * @author Baptiste Meyer + */ +abstract class AbstractFilterExtension implements RequestBodySearchCollectionExtensionInterface +{ + private $resourceMetadataFactory; + private $filterLocator; + + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, ContainerInterface $filterLocator) + { + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->filterLocator = $filterLocator; + } + + /** + * {@inheritdoc} + */ + public function applyToCollection(array $requestBody, string $resourceClass, ?string $operationName = null, array $context = []): array { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true); + + if (!$resourceFilters) { + return $requestBody; + } + + $context['filters'] = $context['filters'] ?? []; + $clauseBody = []; + + foreach ($resourceFilters as $filterId) { + if ($this->filterLocator->has($filterId) && is_a($filter = $this->filterLocator->get($filterId), $this->getFilterInterface())) { + $clauseBody = $filter->apply($clauseBody, $resourceClass, $operationName, $context); + } + } + + if (!$clauseBody) { + return $requestBody; + } + + return $this->alterRequestBody($requestBody, $clauseBody); } + + /** + * Gets the related filter interface. + */ + abstract protected function getFilterInterface(): string; + + /** + * Alters the request body. + */ + abstract protected function alterRequestBody(array $requestBody, array $clauseBody): array; } diff --git a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtension.php b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtension.php index 34c17757c37..8ec9326ec7a 100644 --- a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtension.php +++ b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtension.php @@ -13,10 +13,39 @@ namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension; -class_exists(\ApiPlatform\Elasticsearch\Extension\ConstantScoreFilterExtension::class); +use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\ConstantScoreFilterInterface; -if (false) { - final class ConstantScoreFilterExtension extends \ApiPlatform\Elasticsearch\Extension\ConstantScoreFilterExtension +/** + * Applies filter clauses while executing a constant score query. + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html + * + * @experimental + * + * @author Baptiste Meyer + */ +final class ConstantScoreFilterExtension extends AbstractFilterExtension +{ + /** + * {@inheritdoc} + */ + protected function getFilterInterface(): string { + return ConstantScoreFilterInterface::class; + } + + /** + * {@inheritdoc} + */ + protected function alterRequestBody(array $requestBody, array $clauseBody): array + { + $requestBody['query'] = $requestBody['query'] ?? []; + $requestBody['query'] += [ + 'constant_score' => [ + 'filter' => $clauseBody, + ], + ]; + + return $requestBody; } } diff --git a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php index efaac85fe8f..06ee807d54c 100644 --- a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php +++ b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php @@ -13,10 +13,88 @@ namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension; -class_exists(\ApiPlatform\Elasticsearch\Extension\SortExtension::class); +use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface; +use ApiPlatform\Core\Bridge\Elasticsearch\Util\FieldDatatypeTrait; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -if (false) { - final class SortExtension extends \ApiPlatform\Elasticsearch\Extension\SortExtension +/** + * Applies selected sorting while querying resource collection. + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html + * + * @experimental + * + * @author Baptiste Meyer + */ +final class SortExtension implements RequestBodySearchCollectionExtensionInterface +{ + use FieldDatatypeTrait; + + private $defaultDirection; + private $identifierExtractor; + private $resourceMetadataFactory; + private $nameConverter; + + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IdentifierExtractorInterface $identifierExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?NameConverterInterface $nameConverter = null, ?string $defaultDirection = null) + { + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->identifierExtractor = $identifierExtractor; + $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->resourceClassResolver = $resourceClassResolver; + $this->nameConverter = $nameConverter; + $this->defaultDirection = $defaultDirection; + } + + /** + * {@inheritdoc} + */ + public function applyToCollection(array $requestBody, string $resourceClass, ?string $operationName = null, array $context = []): array + { + $orders = []; + + if ( + null !== ($defaultOrder = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('order')) + && \is_array($defaultOrder) + ) { + foreach ($defaultOrder as $property => $direction) { + if (\is_int($property)) { + $property = $direction; + $direction = 'asc'; + } + + $orders[] = $this->getOrder($resourceClass, $property, $direction); + } + } elseif (null !== $this->defaultDirection) { + $orders[] = $this->getOrder( + $resourceClass, + $this->identifierExtractor->getIdentifierFromResourceClass($resourceClass), + $this->defaultDirection + ); + } + + if (!$orders) { + return $requestBody; + } + + $requestBody['sort'] = array_merge_recursive($requestBody['sort'] ?? [], $orders); + + return $requestBody; + } + + private function getOrder(string $resourceClass, string $property, string $direction): array { + $order = ['order' => strtolower($direction)]; + + if (null !== $nestedPath = $this->getNestedFieldPath($resourceClass, $property)) { + $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath, $resourceClass); + $order['nested'] = ['path' => $nestedPath]; + } + + $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property, $resourceClass); + + return [$property => $order]; } } diff --git a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtension.php b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtension.php index 41d41d46eee..a752d486ad0 100644 --- a/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtension.php +++ b/src/Core/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtension.php @@ -13,10 +13,34 @@ namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension; -class_exists(\ApiPlatform\Elasticsearch\Extension\SortFilterExtension::class); +use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\SortFilterInterface; -if (false) { - final class SortFilterExtension extends \ApiPlatform\Elasticsearch\Extension\SortFilterExtension +/** + * Applies filters on the sort parameter while querying resource collection. + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html + * + * @experimental + * + * @author Baptiste Meyer + */ +final class SortFilterExtension extends AbstractFilterExtension +{ + /** + * {@inheritdoc} + */ + protected function getFilterInterface(): string { + return SortFilterInterface::class; + } + + /** + * {@inheritdoc} + */ + protected function alterRequestBody(array $requestBody, array $clauseBody): array + { + $requestBody['sort'] = array_merge_recursive($requestBody['sort'] ?? [], $clauseBody); + + return $requestBody; } } diff --git a/src/Core/Bridge/Elasticsearch/Serializer/DocumentNormalizer.php b/src/Core/Bridge/Elasticsearch/Serializer/DocumentNormalizer.php deleted file mode 100644 index eb59f592f40..00000000000 --- a/src/Core/Bridge/Elasticsearch/Serializer/DocumentNormalizer.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Core\Bridge\Elasticsearch\Serializer; - -class_exists(\ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer::class); - -if (false) { - final class DocumentNormalizer extends \ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer - { - } -} diff --git a/src/Core/Bridge/Elasticsearch/Serializer/ItemNormalizer.php b/src/Core/Bridge/Elasticsearch/Serializer/ItemNormalizer.php index d263305ffe6..f364e8729ae 100644 --- a/src/Core/Bridge/Elasticsearch/Serializer/ItemNormalizer.php +++ b/src/Core/Bridge/Elasticsearch/Serializer/ItemNormalizer.php @@ -13,10 +13,90 @@ namespace ApiPlatform\Core\Bridge\Elasticsearch\Serializer; -class_exists(\ApiPlatform\Elasticsearch\Serializer\ItemNormalizer::class); +use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -if (false) { - final class ItemNormalizer extends \ApiPlatform\Elasticsearch\Serializer\ItemNormalizer +/** + * Item denormalizer for Elasticsearch. + * + * @experimental + * + * @author Baptiste Meyer + */ +final class ItemNormalizer extends ObjectNormalizer +{ + public const FORMAT = 'elasticsearch'; + + private $identifierExtractor; + + public function __construct(IdentifierExtractorInterface $identifierExtractor, ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) + { + parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext); + + $this->identifierExtractor = $identifierExtractor; + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null): bool + { + return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format); + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function denormalize($data, $class, $format = null, array $context = []) { + if (\is_string($data['_id'] ?? null) && \is_array($data['_source'] ?? null)) { + $data = $this->populateIdentifier($data, $class)['_source']; + } + + return parent::denormalize($data, $class, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null): bool + { + // prevent the use of lower priority normalizers (e.g. serializer.normalizer.object) for this format + return self::FORMAT === $format; + } + + /** + * {@inheritdoc} + * + * @throws LogicException + * + * @return mixed + */ + public function normalize($object, $format = null, array $context = []) + { + throw new LogicException(sprintf('%s is a write-only format.', self::FORMAT)); + } + + /** + * Populates the resource identifier with the document identifier if not present in the original JSON document. + */ + private function populateIdentifier(array $data, string $class): array + { + $identifier = $this->identifierExtractor->getIdentifierFromResourceClass($class); + $identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier, $class, self::FORMAT); + + if (!isset($data['_source'][$identifier])) { + $data['_source'][$identifier] = $data['_id']; + } + + return $data; } } diff --git a/src/Elasticsearch/Serializer/DocumentNormalizer.php b/src/Elasticsearch/Serializer/DocumentNormalizer.php index eb60aa71a1b..fb0cabe4de9 100644 --- a/src/Elasticsearch/Serializer/DocumentNormalizer.php +++ b/src/Elasticsearch/Serializer/DocumentNormalizer.php @@ -119,5 +119,3 @@ private function populateIdentifier(array $data, string $class): array return $data; } } - -class_alias(DocumentNormalizer::class, \ApiPlatform\Core\Bridge\Elasticsearch\Serializer\DocumentNormalizer::class); diff --git a/src/Elasticsearch/Serializer/ItemNormalizer.php b/src/Elasticsearch/Serializer/ItemNormalizer.php index 3e055bde29a..cad183402f3 100644 --- a/src/Elasticsearch/Serializer/ItemNormalizer.php +++ b/src/Elasticsearch/Serializer/ItemNormalizer.php @@ -111,5 +111,3 @@ public function setSerializer(SerializerInterface $serializer) $this->decorated->setSerializer($serializer); } } - -class_alias(ItemNormalizer::class, \ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer::class); diff --git a/src/Symfony/Bundle/Resources/config/elasticsearch.xml b/src/Symfony/Bundle/Resources/config/elasticsearch.xml index 974d95b0427..339b1c62b5b 100644 --- a/src/Symfony/Bundle/Resources/config/elasticsearch.xml +++ b/src/Symfony/Bundle/Resources/config/elasticsearch.xml @@ -45,22 +45,6 @@ - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml b/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml index a68c67a78bd..cab9dfd381b 100644 --- a/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml +++ b/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml @@ -5,6 +5,18 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/v3/elasticsearch.xml b/src/Symfony/Bundle/Resources/config/v3/elasticsearch.xml index 72b02844000..f5c335c36b2 100644 --- a/src/Symfony/Bundle/Resources/config/v3/elasticsearch.xml +++ b/src/Symfony/Bundle/Resources/config/v3/elasticsearch.xml @@ -6,6 +6,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/deprecation.php b/src/deprecation.php index 671b8e595cc..712603d4c95 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -85,8 +85,6 @@ class_alias($interfaceName, $oldInterfaceName); ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata::class => ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata::class, ApiPlatform\Core\Bridge\Elasticsearch\Serializer\NameConverter\InnerFieldsNameConverter::class => ApiPlatform\Elasticsearch\Serializer\NameConverter\InnerFieldsNameConverter::class, - ApiPlatform\Core\Bridge\Elasticsearch\Serializer\DocumentNormalizer::class => ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer::class, - ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer::class => ApiPlatform\Elasticsearch\Serializer\ItemNormalizer::class, ApiPlatform\Core\Bridge\Elasticsearch\Util\FieldDatatypeTrait::class => ApiPlatform\Elasticsearch\Util\FieldDatatypeTrait::class, ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Paginator::class => ApiPlatform\Elasticsearch\Paginator::class,