diff --git a/.php_cs.dist b/.php_cs.dist
index 4416b378429..c14bee5ca39 100644
--- a/.php_cs.dist
+++ b/.php_cs.dist
@@ -13,7 +13,7 @@ HEADER;
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
- ->exclude('tests/Fixtures/app/cache')
+ ->exclude('tests/Fixtures/app/var/cache')
;
return PhpCsFixer\Config::create()
diff --git a/src/Bridge/Elasticsearch/DataProvider/CollectionDataProvider.php b/src/Bridge/Elasticsearch/DataProvider/CollectionDataProvider.php
index c01b12f52ea..7686fe04575 100644
--- a/src/Bridge/Elasticsearch/DataProvider/CollectionDataProvider.php
+++ b/src/Bridge/Elasticsearch/DataProvider/CollectionDataProvider.php
@@ -32,7 +32,7 @@
final class CollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
private $client;
- private $indexMetadataFactory;
+ private $documentMetadataFactory;
private $denormalizer;
private $pagination;
private $collectionExtensions;
@@ -40,10 +40,10 @@ final class CollectionDataProvider implements ContextAwareCollectionDataProvider
/**
* @param FullBodySearchCollectionExtensionInterface[] $collectionExtensions
*/
- public function __construct(Client $client, DocumentMetadataFactoryInterface $indexMetadataFactory, DenormalizerInterface $denormalizer, Pagination $pagination, iterable $collectionExtensions = [])
+ public function __construct(Client $client, DocumentMetadataFactoryInterface $documentMetadataFactory, DenormalizerInterface $denormalizer, Pagination $pagination, iterable $collectionExtensions = [])
{
$this->client = $client;
- $this->indexMetadataFactory = $indexMetadataFactory;
+ $this->documentMetadataFactory = $documentMetadataFactory;
$this->denormalizer = $denormalizer;
$this->pagination = $pagination;
$this->collectionExtensions = $collectionExtensions;
@@ -55,7 +55,7 @@ public function __construct(Client $client, DocumentMetadataFactoryInterface $in
public function supports(string $resourceClass, ?string $operationName = null, array $context = []): bool
{
try {
- $this->indexMetadataFactory->create($resourceClass);
+ $this->documentMetadataFactory->create($resourceClass);
} catch (IndexNotFoundException $e) {
return false;
}
@@ -68,7 +68,7 @@ public function supports(string $resourceClass, ?string $operationName = null, a
*/
public function getCollection(string $resourceClass, ?string $operationName = null, array $context = [])
{
- $indexMetadata = $this->indexMetadataFactory->create($resourceClass);
+ $documentMetadata = $this->documentMetadataFactory->create($resourceClass);
$body = [];
foreach ($this->collectionExtensions as $collectionExtension) {
@@ -95,8 +95,8 @@ public function getCollection(string $resourceClass, ?string $operationName = nu
}
$documents = $this->client->search([
- 'index' => $indexMetadata->getIndex(),
- 'type' => $indexMetadata->getType(),
+ 'index' => $documentMetadata->getIndex(),
+ 'type' => $documentMetadata->getType(),
'body' => $body,
]);
diff --git a/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php b/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php
index 76186d7873c..149dbb270f7 100644
--- a/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php
+++ b/src/Bridge/Elasticsearch/DataProvider/Extension/SortExtension.php
@@ -17,6 +17,7 @@
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;
/**
* Applies selected sorting while querying resource collection.
@@ -35,12 +36,14 @@ final class SortExtension implements FullBodySearchCollectionExtensionInterface
private $defaultDirection;
private $identifierExtractor;
private $resourceMetadataFactory;
+ private $nameConverter;
- public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IdentifierExtractorInterface $identifierExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, ?string $defaultDirection = null)
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IdentifierExtractorInterface $identifierExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, ?NameConverterInterface $nameConverter = null, ?string $defaultDirection = null)
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->identifierExtractor = $identifierExtractor;
$this->propertyMetadataFactory = $propertyMetadataFactory;
+ $this->nameConverter = $nameConverter;
$this->defaultDirection = $defaultDirection;
}
@@ -85,9 +88,12 @@ private function getOrder(string $resourceClass, string $property, string $direc
$order = ['order' => strtolower($direction)];
if (null !== $nestedPath = $this->getNestedFieldPath($resourceClass, $property)) {
+ $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($property);
$order['nested'] = ['path' => $nestedPath];
}
+ $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property);
+
return [$property => $order];
}
}
diff --git a/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractFilter.php b/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractFilter.php
index 28d64e59c31..144a4fa46ec 100644
--- a/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractFilter.php
+++ b/src/Bridge/Elasticsearch/DataProvider/Filter/AbstractFilter.php
@@ -20,6 +20,7 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* Abstract class with helpers for easing the implementation of a filter.
@@ -34,12 +35,14 @@ abstract class AbstractFilter implements FilterInterface
protected $properties;
protected $propertyNameCollectionFactory;
+ protected $nameConverter;
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?array $properties = null)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?NameConverterInterface $nameConverter = null, ?array $properties = null)
{
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->resourceClassResolver = $resourceClassResolver;
+ $this->nameConverter = $nameConverter;
$this->properties = $properties;
}
@@ -49,7 +52,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
protected function getProperties(string $resourceClass): \Traversable
{
if (null !== $this->properties) {
- return yield from $this->properties;
+ return yield from array_keys($this->properties);
}
try {
diff --git a/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php b/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php
index d2aee5f0bfb..dd80d4a94c1 100644
--- a/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php
+++ b/src/Bridge/Elasticsearch/DataProvider/Filter/OrderFilter.php
@@ -16,6 +16,7 @@
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* Order the collection by given properties.
@@ -31,9 +32,9 @@ final class OrderFilter extends AbstractFilter implements SortFilterInterface
{
private $orderParameterName;
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, string $orderParameterName = 'order', ?array $properties = null)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?NameConverterInterface $nameConverter = null, string $orderParameterName = 'order', ?array $properties = null)
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $properties);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $nameConverter, $properties);
$this->orderParameterName = $orderParameterName;
}
@@ -56,7 +57,7 @@ public function apply(array $clauseBody, string $resourceClass, ?string $operati
continue;
}
- if (empty($direction) && null !== $defaultDirection = $this->properties[$property]['default_direction'] ?? null) {
+ if (empty($direction) && null !== $defaultDirection = $this->properties[$property] ?? null) {
$direction = $defaultDirection;
}
@@ -67,9 +68,11 @@ public function apply(array $clauseBody, string $resourceClass, ?string $operati
$order = ['order' => $direction];
if (null !== $nestedPath = $this->getNestedFieldPath($resourceClass, $property)) {
+ $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath);
$order['nested'] = ['path' => $nestedPath];
}
+ $property = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property);
$orders[] = [$property => $order];
}
diff --git a/src/Bridge/Elasticsearch/DataProvider/Filter/TermFilter.php b/src/Bridge/Elasticsearch/DataProvider/Filter/TermFilter.php
index 4e5382421fa..5a3b8378184 100644
--- a/src/Bridge/Elasticsearch/DataProvider/Filter/TermFilter.php
+++ b/src/Bridge/Elasticsearch/DataProvider/Filter/TermFilter.php
@@ -21,6 +21,7 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* Filter the collection by given properties.
@@ -37,9 +38,9 @@ final class TermFilter extends AbstractFilter implements ConstantScoreFilterInte
private $iriConverter;
private $propertyAccessor;
- public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, IdentifierExtractorInterface $identifierExtractor, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor, ?array $properties = null)
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, IdentifierExtractorInterface $identifierExtractor, IriConverterInterface $iriConverter, PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter = null, ?array $properties = null)
{
- parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $properties);
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $resourceClassResolver, $nameConverter, $properties);
$this->identifierExtractor = $identifierExtractor;
$this->iriConverter = $iriConverter;
@@ -68,13 +69,16 @@ public function apply(array $clauseBody, string $resourceClass, ?string $operati
continue;
}
+ $convertedProperty = null === $this->nameConverter ? $property : $this->nameConverter->normalize($property);
+
if (1 === \count($values)) {
- $term = ['term' => [$property => $values[0]]];
+ $term = ['term' => [$convertedProperty => $values[0]]];
} else {
- $term = ['terms' => [$property => $values]];
+ $term = ['terms' => [$convertedProperty => $values]];
}
if (null !== $nestedPath = $this->getNestedFieldPath($resourceClass, $property)) {
+ $nestedPath = null === $this->nameConverter ? $nestedPath : $this->nameConverter->normalize($nestedPath);
$term = ['nested' => ['path' => $nestedPath, 'query' => $term]];
}
diff --git a/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php b/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php
index a67738af553..c420c14baf7 100644
--- a/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php
+++ b/src/Bridge/Elasticsearch/DataProvider/ItemDataProvider.php
@@ -34,13 +34,13 @@
final class ItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface
{
private $client;
- private $indexMetadataFactory;
+ private $documentMetadataFactory;
private $denormalizer;
- public function __construct(Client $client, DocumentMetadataFactoryInterface $indexMetadataFactory, DenormalizerInterface $denormalizer)
+ public function __construct(Client $client, DocumentMetadataFactoryInterface $documentMetadataFactory, DenormalizerInterface $denormalizer)
{
$this->client = $client;
- $this->indexMetadataFactory = $indexMetadataFactory;
+ $this->documentMetadataFactory = $documentMetadataFactory;
$this->denormalizer = $denormalizer;
}
@@ -50,7 +50,7 @@ public function __construct(Client $client, DocumentMetadataFactoryInterface $in
public function supports(string $resourceClass, ?string $operationName = null, array $context = []): bool
{
try {
- $this->indexMetadataFactory->create($resourceClass);
+ $this->documentMetadataFactory->create($resourceClass);
} catch (IndexNotFoundException $e) {
return false;
}
@@ -68,15 +68,15 @@ public function getItem(string $resourceClass, $id, ?string $operationName = nul
throw new InvalidArgumentException('Composite identifiers not supported.');
}
- $id = array_values($id)[0];
+ $id = reset($id);
}
- $indexMetadata = $this->indexMetadataFactory->create($resourceClass);
+ $documentMetadata = $this->documentMetadataFactory->create($resourceClass);
try {
$document = $this->client->get([
- 'index' => $indexMetadata->getIndex(),
- 'type' => $indexMetadata->getType(),
+ 'index' => $documentMetadata->getIndex(),
+ 'type' => $documentMetadata->getType(),
'id' => (string) $id,
]);
} catch (Missing404Exception $e) {
diff --git a/src/Bridge/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php b/src/Bridge/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php
index d4aea392a15..4897f47e1c3 100644
--- a/src/Bridge/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php
+++ b/src/Bridge/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php
@@ -40,35 +40,35 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
*/
public function create(string $resourceClass): DocumentMetadata
{
- $indexMetadata = null;
+ $documentMetadata = null;
if ($this->decorated) {
try {
- $indexMetadata = $this->decorated->create($resourceClass);
+ $documentMetadata = $this->decorated->create($resourceClass);
} catch (IndexNotFoundException $e) {
}
}
$resourceMetadata = null;
- if (!$indexMetadata || null === $indexMetadata->getIndex()) {
+ if (!$documentMetadata || null === $documentMetadata->getIndex()) {
$resourceMetadata = $resourceMetadata ?? $this->resourceMetadataFactory->create($resourceClass);
if (null !== $index = $resourceMetadata->getAttribute('elasticsearch_index')) {
- $indexMetadata = $indexMetadata ? $indexMetadata->withIndex($index) : new DocumentMetadata($index);
+ $documentMetadata = $documentMetadata ? $documentMetadata->withIndex($index) : new DocumentMetadata($index);
}
}
- if (!$indexMetadata || DocumentMetadata::DEFAULT_TYPE === $indexMetadata->getType()) {
+ if (!$documentMetadata || DocumentMetadata::DEFAULT_TYPE === $documentMetadata->getType()) {
$resourceMetadata = $resourceMetadata ?? $this->resourceMetadataFactory->create($resourceClass);
if (null !== $type = $resourceMetadata->getAttribute('elasticsearch_type')) {
- $indexMetadata = $indexMetadata ? $indexMetadata->withType($type) : new DocumentMetadata(null, $type);
+ $documentMetadata = $documentMetadata ? $documentMetadata->withType($type) : new DocumentMetadata(null, $type);
}
}
- if ($indexMetadata) {
- return $indexMetadata;
+ if ($documentMetadata) {
+ return $documentMetadata;
}
throw new IndexNotFoundException(sprintf('No index associated with the "%s" resource class.', $resourceClass));
diff --git a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php
index 007085df9bc..42dc584e235 100644
--- a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php
+++ b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php
@@ -58,23 +58,23 @@ public function create(string $resourceClass): DocumentMetadata
return $this->handleNotFound($this->localCache[$resourceClass] = $cacheItem->get(), $resourceClass);
}
- $indexMetadata = $this->decorated->create($resourceClass);
+ $documentMetadata = $this->decorated->create($resourceClass);
- $cacheItem->set($indexMetadata);
+ $cacheItem->set($documentMetadata);
$this->cacheItemPool->save($cacheItem);
- return $this->handleNotFound($this->localCache[$resourceClass] = $indexMetadata, $resourceClass);
+ return $this->handleNotFound($this->localCache[$resourceClass] = $documentMetadata, $resourceClass);
}
/**
* @throws IndexNotFoundException
*/
- private function handleNotFound(DocumentMetadata $indexMetadata, string $resourceClass): DocumentMetadata
+ private function handleNotFound(DocumentMetadata $documentMetadata, string $resourceClass): DocumentMetadata
{
- if (null === $indexMetadata->getIndex()) {
+ if (null === $documentMetadata->getIndex()) {
throw new IndexNotFoundException(sprintf('No index associated with the "%s" resource class.', $resourceClass));
}
- return $indexMetadata;
+ return $documentMetadata;
}
}
diff --git a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php
index 1f379701a02..7044ce8aee4 100644
--- a/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php
+++ b/src/Bridge/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php
@@ -47,17 +47,17 @@ public function __construct(Client $client, ResourceMetadataFactoryInterface $re
*/
public function create(string $resourceClass): DocumentMetadata
{
- $indexMetadata = null;
+ $documentMetadata = null;
if ($this->decorated) {
try {
- $indexMetadata = $this->decorated->create($resourceClass);
+ $documentMetadata = $this->decorated->create($resourceClass);
} catch (IndexNotFoundException $e) {
}
}
- if ($indexMetadata && null !== $indexMetadata->getIndex()) {
- return $indexMetadata;
+ if ($documentMetadata && null !== $documentMetadata->getIndex()) {
+ return $documentMetadata;
}
$index = Inflector::tableize($this->resourceMetadataFactory->create($resourceClass)->getShortName());
@@ -65,15 +65,15 @@ public function create(string $resourceClass): DocumentMetadata
try {
$this->client->cat()->indices(['index' => $index]);
} catch (Missing404Exception $e) {
- if ($indexMetadata) {
- return $indexMetadata;
+ if ($documentMetadata) {
+ return $documentMetadata;
}
throw new IndexNotFoundException(sprintf('No index associated with the "%s" resource class.', $resourceClass));
}
- if ($indexMetadata) {
- return $indexMetadata->withIndex($index);
+ if ($documentMetadata) {
+ return $documentMetadata->withIndex($index);
}
return new DocumentMetadata($index);
diff --git a/src/Bridge/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php b/src/Bridge/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php
index 6b23ea634b4..ca734f5c8bb 100644
--- a/src/Bridge/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php
+++ b/src/Bridge/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php
@@ -39,33 +39,33 @@ public function __construct(array $mapping, ?DocumentMetadataFactoryInterface $d
*/
public function create(string $resourceClass): DocumentMetadata
{
- $indexMetadata = null;
+ $documentMetadata = null;
if ($this->decorated) {
try {
- $indexMetadata = $this->decorated->create($resourceClass);
+ $documentMetadata = $this->decorated->create($resourceClass);
} catch (IndexNotFoundException $e) {
}
}
if (null === $index = $this->mapping[$resourceClass] ?? null) {
- if ($indexMetadata) {
- return $indexMetadata;
+ if ($documentMetadata) {
+ return $documentMetadata;
}
throw new IndexNotFoundException(sprintf('No index associated with the "%s" resource class.', $resourceClass));
}
- $indexMetadata = $indexMetadata ?? new DocumentMetadata();
+ $documentMetadata = $documentMetadata ?? new DocumentMetadata();
if (isset($index['index'])) {
- $indexMetadata = $indexMetadata->withIndex($index['index']);
+ $documentMetadata = $documentMetadata->withIndex($index['index']);
}
if (isset($index['type'])) {
- $indexMetadata = $indexMetadata->withType($index['type']);
+ $documentMetadata = $documentMetadata->withType($index['type']);
}
- return $indexMetadata;
+ return $documentMetadata;
}
}
diff --git a/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php b/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php
index 2304c564f70..9ba0039c9ef 100644
--- a/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php
+++ b/src/Bridge/Elasticsearch/Serializer/ItemNormalizer.php
@@ -83,10 +83,7 @@ public function normalize($object, $format = null, array $context = [])
private function populateIdentifier(array $data, string $class): array
{
$identifier = $this->identifierExtractor->getIdentifierFromResourceClass($class);
-
- if (null !== $this->nameConverter) {
- $identifier = $this->nameConverter->normalize($identifier);
- }
+ $identifier = null === $this->nameConverter ? $identifier : $this->nameConverter->normalize($identifier);
if (!isset($data['_source'][$identifier])) {
$data['_source'][$identifier] = $data['_id'];
diff --git a/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php b/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php
new file mode 100644
index 00000000000..91c72047bcd
--- /dev/null
+++ b/src/Bridge/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php
@@ -0,0 +1,58 @@
+
+ *
+ * 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\NameConverter;
+
+use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
+
+final class InnerFieldsNameConverter implements NameConverterInterface
+{
+ private $decorated;
+
+ public function __construct(?NameConverterInterface $decorated = null)
+ {
+ $this->decorated = $decorated ?? new CamelCaseToSnakeCaseNameConverter();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize($propertyName)
+ {
+ return $this->convertInnerFields($propertyName, true);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function denormalize($propertyName)
+ {
+ return $this->convertInnerFields($propertyName, false);
+ }
+
+ private function convertInnerFields(string $propertyName, bool $normalization)
+ {
+ if (null === $this->decorated) {
+ return $propertyName;
+ }
+
+ $convertedProperties = [];
+
+ foreach (explode('.', $propertyName) as $decomposedProperty) {
+ $convertedProperties[] = $this->decorated->{$normalization ? 'normalize' : 'denormalize'}($decomposedProperty);
+ }
+
+ return implode('.', $convertedProperties);
+ }
+}
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
index d75d3ffcb4c..53035652e0f 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
@@ -36,6 +36,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
+use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -554,16 +555,24 @@ private function registerMercureConfiguration(ContainerBuilder $container, array
private function registerElasticsearchConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader)
{
- if (!$config['elasticsearch']['enabled'] || !class_exists(Client::class)) {
+ $enabled = $config['elasticsearch']['enabled'] && class_exists(Client::class);
+
+ $container->setParameter('api_platform.elasticsearch.enabled', $enabled);
+
+ if (!$enabled) {
return;
}
$loader->load('elasticsearch.xml');
+ if ($container->hasAlias('api_platform.name_converter')) {
+ $container->getDefinition('api_platform.elasticsearch.name_converter.inner_fields')
+ ->setArgument(0, new Reference('api_platform.name_converter'));
+ }
+
$container->registerForAutoconfiguration(ElasticSearchQueryCollectionExtensionInterface::class)
->addTag('api_platform.elasticsearch.query_extension.collection');
- $container->setParameter('api_platform.elasticsearch.enabled', $config['elasticsearch']['enabled']);
$container->setParameter('api_platform.elasticsearch.host', $config['elasticsearch']['host']);
$container->setParameter('api_platform.elasticsearch.mapping', $config['elasticsearch']['mapping']);
}
diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
index a21e68af7d0..9594398c1c0 100644
--- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
+++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
@@ -229,7 +229,11 @@ public function getConfigTreeBuilder()
->arrayNode('mercure')
->{class_exists(Update::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
- ->scalarNode('hub_url')->defaultNull()->info('The URL send in the Link HTTP header. If not set, will default to the URL for the Symfony\'s bundle default hub.')
+ ->scalarNode('hub_url')
+ ->defaultNull()
+ ->info('The URL send in the Link HTTP header. If not set, will default to the URL for the Symfony\'s bundle default hub.')
+ ->end()
+ ->end()
->end()
->arrayNode('elasticsearch')
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml b/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
index 9cbf35d1923..27e25b05602 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/elasticsearch.xml
@@ -50,10 +50,12 @@
+
+
-
+
@@ -95,6 +97,7 @@
+
%api_platform.collection.order%
@@ -107,12 +110,14 @@
+
+
%api_platform.collection.order_parameter_name%
diff --git a/tests/Bridge/Elasticsearch/Api/IdentifierExtractorTest.php b/tests/Bridge/Elasticsearch/Api/IdentifierExtractorTest.php
new file mode 100644
index 00000000000..35f39f5e921
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/Api/IdentifierExtractorTest.php
@@ -0,0 +1,67 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\Api;
+
+use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractor;
+use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\Exception\NonUniqueIdentifierException;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use PHPUnit\Framework\TestCase;
+
+class IdentifierExtractorTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ IdentifierExtractorInterface::class,
+ new IdentifierExtractor($this->prophesize(IdentifiersExtractorInterface::class)->reveal())
+ );
+ }
+
+ public function testGetIdentifierFromResourceClass()
+ {
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Dummy::class)->willReturn(['id'])->shouldBeCalledOnce();
+
+ $identifierExtractor = new IdentifierExtractor($identifiersExtractorProphecy->reveal());
+
+ self::assertSame('id', $identifierExtractor->getIdentifierFromResourceClass(Dummy::class));
+ }
+
+ public function testGetIdentifierFromResourceClassWithCompositeIdentifiers()
+ {
+ self::expectException(NonUniqueIdentifierException::class);
+ self::expectExceptionMessage('Composite identifiers not supported.');
+
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(CompositeRelation::class)->willReturn(['compositeItem', 'compositeLabel'])->shouldBeCalledOnce();
+
+ $identifierExtractor = new IdentifierExtractor($identifiersExtractorProphecy->reveal());
+ $identifierExtractor->getIdentifierFromResourceClass(CompositeRelation::class);
+ }
+
+ public function testGetIdentifierFromResourceClassWithNoIdentifier()
+ {
+ self::expectException(NonUniqueIdentifierException::class);
+ self::expectExceptionMessage('Resource "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" has no identifiers.');
+
+ $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
+ $identifiersExtractorProphecy->getIdentifiersFromResourceClass(Dummy::class)->willReturn([])->shouldBeCalledOnce();
+
+ $identifierExtractor = new IdentifierExtractor($identifiersExtractorProphecy->reveal());
+ $identifierExtractor->getIdentifierFromResourceClass(Dummy::class);
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/CollectionDataProviderTest.php b/tests/Bridge/Elasticsearch/DataProvider/CollectionDataProviderTest.php
new file mode 100644
index 00000000000..b244e6af8e6
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/CollectionDataProviderTest.php
@@ -0,0 +1,145 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider;
+
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\CollectionDataProvider;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Paginator;
+use ApiPlatform\Core\Bridge\Elasticsearch\Exception\IndexNotFoundException;
+use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
+use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
+use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
+use ApiPlatform\Core\DataProvider\Pagination;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use Elasticsearch\Client;
+use PHPUnit\Framework\TestCase;
+use Prophecy\Argument;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+class CollectionDataProviderTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ CollectionDataProviderInterface::class,
+ new CollectionDataProvider(
+ $this->prophesize(Client::class)->reveal(),
+ $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(DenormalizerInterface::class)->reveal(),
+ new Pagination(new RequestStack(), $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal())
+ )
+ );
+ }
+
+ public function testSupports()
+ {
+ $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
+ $documentMetadataFactoryProphecy->create(Foo::class)->willReturn(new DocumentMetadata('foo'))->shouldBeCalled();
+ $documentMetadataFactoryProphecy->create(Dummy::class)->willThrow(new IndexNotFoundException())->shouldBeCalled();
+
+ $collectionDataProvider = new CollectionDataProvider(
+ $this->prophesize(Client::class)->reveal(),
+ $documentMetadataFactoryProphecy->reveal(),
+ $this->prophesize(DenormalizerInterface::class)->reveal(),
+ new Pagination(new RequestStack(), $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal())
+ );
+
+ self::assertTrue($collectionDataProvider->supports(Foo::class));
+ self::assertFalse($collectionDataProvider->supports(Dummy::class));
+ }
+
+ public function testGetCollection()
+ {
+ $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
+ $documentMetadataFactoryProphecy->create(Foo::class)->willReturn(new DocumentMetadata('foo'))->shouldBeCalled();
+
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata());
+
+ $documents = [
+ 'took' => 15,
+ 'time_out' => false,
+ '_shards' => [
+ 'total' => 5,
+ 'successful' => 5,
+ 'skipped' => 0,
+ 'failed' => 0,
+ ],
+ 'hits' => [
+ 'total' => 4,
+ 'max_score' => 1,
+ 'hits' => [
+ [
+ '_index' => 'foo',
+ '_type' => '_doc',
+ '_id' => '1',
+ '_score' => 1,
+ '_source' => [
+ 'id' => 1,
+ 'name' => 'Kilian',
+ 'bar' => 'Jornet',
+ ],
+ ],
+ [
+ '_index' => 'foo',
+ '_type' => '_doc',
+ '_id' => '2',
+ '_score' => 1,
+ '_source' => [
+ 'id' => 2,
+ 'name' => 'François',
+ 'bar' => 'D\'Haene',
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $clientProphecy = $this->prophesize(Client::class);
+ $clientProphecy
+ ->search(
+ Argument::allOf(
+ Argument::withEntry('index', 'foo'),
+ Argument::withEntry('type', DocumentMetadata::DEFAULT_TYPE),
+ Argument::withEntry('body', Argument::allOf(
+ Argument::withEntry('size', 2),
+ Argument::withEntry('from', 0),
+ Argument::withEntry('query', Argument::allOf(
+ Argument::withEntry('match_all', Argument::type(\stdClass::class)),
+ Argument::size(1)
+ )),
+ Argument::size(3)
+ )),
+ Argument::size(3)
+ )
+ )
+ ->willReturn($documents)
+ ->shouldBeCalled();
+
+ $collectionDataProvider = new CollectionDataProvider(
+ $clientProphecy->reveal(),
+ $documentMetadataFactoryProphecy->reveal(),
+ $denormalizer = $this->prophesize(DenormalizerInterface::class)->reveal(),
+ new Pagination(new RequestStack(), $resourceMetadataFactoryProphecy->reveal(), ['items_per_page' => 2])
+ );
+
+ self::assertEquals(
+ new Paginator($denormalizer, $documents, Foo::class, 2, 0),
+ $collectionDataProvider->getCollection(Foo::class)
+ );
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtensionTest.php b/tests/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtensionTest.php
new file mode 100644
index 00000000000..eb571fe6ea4
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/Extension/ConstantScoreFilterExtensionTest.php
@@ -0,0 +1,79 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider\Extension;
+
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\ConstantScoreFilterExtension;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\FullBodySearchCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\ConstantScoreFilterInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\SortFilterInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+
+class ConstantScoreFilterExtensionTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ FullBodySearchCollectionExtensionInterface::class,
+ new ConstantScoreFilterExtension(
+ $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(ContainerInterface::class)->reveal()
+ )
+ );
+ }
+
+ public function testApplyToCollection()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata(null, null, null, null, ['get' => ['filters' => ['filter.term']]]))->shouldBeCalled();
+
+ $constantScoreFilterProphecy = $this->prophesize(ConstantScoreFilterInterface::class);
+ $constantScoreFilterProphecy->apply([], Foo::class, 'get', ['filters' => ['name' => ['Kilian', 'Xavier', 'François']]])->willReturn(['bool' => ['must' => [['terms' => ['name' => ['Kilian', 'Xavier', 'François']]]]]])->shouldBeCalled();
+
+ $containerProphecy = $this->prophesize(ContainerInterface::class);
+ $containerProphecy->has('filter.term')->willReturn(true)->shouldBeCalled();
+ $containerProphecy->get('filter.term')->willReturn($constantScoreFilterProphecy->reveal())->shouldBeCalled();
+
+ $constantScoreFilterExtension = new ConstantScoreFilterExtension($resourceMetadataFactoryProphecy->reveal(), $containerProphecy->reveal());
+
+ self::assertSame(['query' => ['constant_score' => ['filter' => ['bool' => ['must' => [['terms' => ['name' => ['Kilian', 'Xavier', 'François']]]]]]]]], $constantScoreFilterExtension->applyToCollection([], Foo::class, 'get', ['filters' => ['name' => ['Kilian', 'Xavier', 'François']]]));
+ }
+
+ public function testApplyToCollectionWithNoFilters()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata());
+
+ $constantScoreFilterExtension = new ConstantScoreFilterExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(ContainerInterface::class)->reveal());
+
+ self::assertEmpty($constantScoreFilterExtension->applyToCollection([], Foo::class));
+ }
+
+ public function testApplyToCollectionWithNoConstantScoreFilters()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata(null, null, null, null, ['get' => ['filters' => ['filter.order']]]))->shouldBeCalled();
+
+ $containerProphecy = $this->prophesize(ContainerInterface::class);
+ $containerProphecy->has('filter.order')->willReturn(true)->shouldBeCalled();
+ $containerProphecy->get('filter.order')->willReturn($this->prophesize(SortFilterInterface::class)->reveal())->shouldBeCalled();
+
+ $constantScoreFilterExtension = new ConstantScoreFilterExtension($resourceMetadataFactoryProphecy->reveal(), $containerProphecy->reveal());
+
+ self::assertEmpty($constantScoreFilterExtension->applyToCollection([], Foo::class, 'get', ['filters' => ['name' => ['Kilian', 'Xavier', 'François']]]));
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php b/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php
new file mode 100644
index 00000000000..5694f68e2ab
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/Extension/SortExtensionTest.php
@@ -0,0 +1,90 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider\Extension;
+
+use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\FullBodySearchCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\SortExtension;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
+
+class SortExtensionTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ FullBodySearchCollectionExtensionInterface::class,
+ new SortExtension(
+ $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(IdentifierExtractorInterface::class)->reveal(),
+ $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(NameConverterInterface::class)->reveal(),
+ 'asc'
+ )
+ );
+ }
+
+ public function testApplyToCollection()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['order' => ['name', 'bar' => 'desc']]))->shouldBeCalled();
+
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)))->shouldBeCalled();
+ $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)))->shouldBeCalled();
+
+ $nameConverterProphecy = $this->prophesize(NameConverterInterface::class);
+ $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled();
+ $nameConverterProphecy->normalize('bar')->willReturn('bar')->shouldBeCalled();
+
+ $sortExtension = new SortExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(IdentifierExtractorInterface::class)->reveal(), $propertyMetadataFactoryProphecy->reveal(), $nameConverterProphecy->reveal(), 'asc');
+
+ self::assertSame(['sort' => [['name' => ['order' => 'asc']], ['bar' => ['order' => 'desc']]]], $sortExtension->applyToCollection([], Foo::class));
+ }
+
+ public function testApplyToCollectionWithDefaultDirection()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata())->shouldBeCalled();
+
+ $identifierExtractorProphecy = $this->prophesize(IdentifierExtractorInterface::class);
+ $identifierExtractorProphecy->getIdentifierFromResourceClass(Foo::class)->willReturn('id')->shouldBeCalled();
+
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT)))->shouldBeCalled();
+
+ $nameConverterProphecy = $this->prophesize(NameConverterInterface::class);
+ $nameConverterProphecy->normalize('id')->willReturn('id')->shouldBeCalled();
+
+ $sortExtension = new SortExtension($resourceMetadataFactoryProphecy->reveal(), $identifierExtractorProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $nameConverterProphecy->reveal(), 'asc');
+
+ self::assertSame(['sort' => [['id' => ['order' => 'asc']]]], $sortExtension->applyToCollection([], Foo::class));
+ }
+
+ public function testApplyToCollectionWithNoOrdering()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata())->shouldBeCalled();
+
+ $sortExtension = new SortExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(IdentifierExtractorInterface::class)->reveal(), $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(), $this->prophesize(NameConverterInterface::class)->reveal());
+
+ self::assertEmpty($sortExtension->applyToCollection([], Foo::class));
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtensionTest.php b/tests/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtensionTest.php
new file mode 100644
index 00000000000..66fea21d9d5
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/Extension/SortFilterExtensionTest.php
@@ -0,0 +1,79 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider\Extension;
+
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\FullBodySearchCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Extension\SortFilterExtension;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\ConstantScoreFilterInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\SortFilterInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+
+class SortFilterExtensionTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ FullBodySearchCollectionExtensionInterface::class,
+ new SortFilterExtension(
+ $this->prophesize(ResourceMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(ContainerInterface::class)->reveal()
+ )
+ );
+ }
+
+ public function testApplyToCollection()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata(null, null, null, null, ['get' => ['filters' => ['filter.order']]]))->shouldBeCalled();
+
+ $sortFilterProphecy = $this->prophesize(SortFilterInterface::class);
+ $sortFilterProphecy->apply([], Foo::class, 'get', ['filters' => ['order' => ['id' => 'desc']]])->willReturn([['id' => ['order' => 'desc']]])->shouldBeCalled();
+
+ $containerProphecy = $this->prophesize(ContainerInterface::class);
+ $containerProphecy->has('filter.order')->willReturn(true)->shouldBeCalled();
+ $containerProphecy->get('filter.order')->willReturn($sortFilterProphecy->reveal())->shouldBeCalled();
+
+ $sortFilterExtension = new SortFilterExtension($resourceMetadataFactoryProphecy->reveal(), $containerProphecy->reveal());
+
+ self::assertSame(['sort' => [['id' => ['order' => 'desc']]]], $sortFilterExtension->applyToCollection([], Foo::class, 'get', ['filters' => ['order' => ['id' => 'desc']]]));
+ }
+
+ public function testApplyToCollectionWithNoFilters()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata());
+
+ $sortFilterExtension = new SortFilterExtension($resourceMetadataFactoryProphecy->reveal(), $this->prophesize(ContainerInterface::class)->reveal());
+
+ self::assertEmpty($sortFilterExtension->applyToCollection([], Foo::class));
+ }
+
+ public function testApplyToCollectionWithNoSortFilters()
+ {
+ $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadata(null, null, null, null, ['get' => ['filters' => ['filter.term']]]))->shouldBeCalled();
+
+ $containerProphecy = $this->prophesize(ContainerInterface::class);
+ $containerProphecy->has('filter.term')->willReturn(true)->shouldBeCalled();
+ $containerProphecy->get('filter.term')->willReturn($this->prophesize(ConstantScoreFilterInterface::class)->reveal())->shouldBeCalled();
+
+ $sortFilterExtension = new SortFilterExtension($resourceMetadataFactoryProphecy->reveal(), $containerProphecy->reveal());
+
+ self::assertEmpty($sortFilterExtension->applyToCollection([], Foo::class, 'get', ['filters' => ['order' => ['id' => 'desc']]]));
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php b/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php
new file mode 100644
index 00000000000..d4f6c4dee54
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/Filter/OrderFilterTest.php
@@ -0,0 +1,81 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider\Filter;
+
+use ApiPlatform\Core\Api\ResourceClassResolverInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\OrderFilter;
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Filter\SortFilterInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
+
+class OrderFilterTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ SortFilterInterface::class,
+ new OrderFilter(
+ $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(),
+ $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(ResourceClassResolverInterface::class)->reveal(),
+ $this->prophesize(NameConverterInterface::class)->reveal()
+ )
+ );
+ }
+
+ public function testApply()
+ {
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)))->shouldBeCalled();
+
+ $nameConverterProphecy = $this->prophesize(NameConverterInterface::class);
+ $nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled();
+
+ $orderFilter = new OrderFilter(
+ $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(),
+ $propertyMetadataFactoryProphecy->reveal(),
+ $this->prophesize(ResourceClassResolverInterface::class)->reveal(),
+ $nameConverterProphecy->reveal(),
+ 'order',
+ ['name' => 'asc']
+ );
+
+ self::assertSame(
+ [['name' => ['order' => 'asc']]],
+ $orderFilter->apply([], Foo::class, null, ['filters' => ['order' => ['name' => null]]])
+ );
+ }
+
+ public function testDescription()
+ {
+ $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING)))->shouldBeCalled();
+
+ $orderFilter = new OrderFilter(
+ $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(),
+ $propertyMetadataFactoryProphecy->reveal(),
+ $this->prophesize(ResourceClassResolverInterface::class)->reveal(),
+ $this->prophesize(NameConverterInterface::class)->reveal(),
+ 'order',
+ ['name' => 'asc']
+ );
+
+ self::assertSame(['order[name]' => ['property' => 'name', 'type' => 'string', 'required' => false]], $orderFilter->getDescription(Foo::class));
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/ItemDataProviderTest.php b/tests/Bridge/Elasticsearch/DataProvider/ItemDataProviderTest.php
new file mode 100644
index 00000000000..175284c2571
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/ItemDataProviderTest.php
@@ -0,0 +1,116 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider;
+
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\ItemDataProvider;
+use ApiPlatform\Core\Bridge\Elasticsearch\Exception\IndexNotFoundException;
+use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
+use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface;
+use ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer;
+use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+use ApiPlatform\Core\Exception\InvalidArgumentException;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use Elasticsearch\Client;
+use Elasticsearch\Common\Exceptions\Missing404Exception;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+class ItemDataProviderTest extends TestCase
+{
+ public function testConstruct()
+ {
+ self::assertInstanceOf(
+ ItemDataProviderInterface::class,
+ new ItemDataProvider(
+ $this->prophesize(Client::class)->reveal(),
+ $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(),
+ $this->prophesize(DenormalizerInterface::class)->reveal()
+ )
+ );
+ }
+
+ public function testSupports()
+ {
+ $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
+ $documentMetadataFactoryProphecy->create(Foo::class)->willReturn(new DocumentMetadata('foo'))->shouldBeCalled();
+ $documentMetadataFactoryProphecy->create(Dummy::class)->willThrow(new IndexNotFoundException())->shouldBeCalled();
+
+ $itemDataProvider = new ItemDataProvider(
+ $this->prophesize(Client::class)->reveal(),
+ $documentMetadataFactoryProphecy->reveal(),
+ $this->prophesize(DenormalizerInterface::class)->reveal()
+ );
+
+ self::assertTrue($itemDataProvider->supports(Foo::class));
+ self::assertFalse($itemDataProvider->supports(Dummy::class));
+ }
+
+ public function testGetItem()
+ {
+ $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
+ $documentMetadataFactoryProphecy->create(Foo::class)->willReturn(new DocumentMetadata('foo'))->shouldBeCalled();
+
+ $document = [
+ '_index' => 'test',
+ '_type' => '_doc',
+ '_id' => '1',
+ '_version' => 1,
+ 'found' => true,
+ '_source' => [
+ 'id' => 1,
+ 'name' => 'Rossinière',
+ 'bar' => 'erèinissor',
+ ],
+ ];
+
+ $foo = new Foo();
+ $foo->setName('Rossinière');
+ $foo->setBar('erèinissor');
+
+ $clientProphecy = $this->prophesize(Client::class);
+ $clientProphecy->get(['index' => 'foo', 'type' => DocumentMetadata::DEFAULT_TYPE, 'id' => '1'])->willReturn($document)->shouldBeCalled();
+
+ $denormalizerProphecy = $this->prophesize(DenormalizerInterface::class);
+ $denormalizerProphecy->denormalize($document, Foo::class, ItemNormalizer::FORMAT, [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true])->willReturn($foo)->shouldBeCalled();
+
+ $itemDataProvider = new ItemDataProvider($clientProphecy->reveal(), $documentMetadataFactoryProphecy->reveal(), $denormalizerProphecy->reveal());
+
+ self::assertSame($foo, $itemDataProvider->getItem(Foo::class, ['id' => 1]));
+ }
+
+ public function testGetItemWithMissing404Exception()
+ {
+ $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class);
+ $documentMetadataFactoryProphecy->create(Foo::class)->willReturn(new DocumentMetadata('foo'))->shouldBeCalled();
+
+ $clientProphecy = $this->prophesize(Client::class);
+ $clientProphecy->get(['index' => 'foo', 'type' => DocumentMetadata::DEFAULT_TYPE, 'id' => '404'])->willThrow(new Missing404Exception())->shouldBeCalled();
+
+ $itemDataProvider = new ItemDataProvider($clientProphecy->reveal(), $documentMetadataFactoryProphecy->reveal(), $this->prophesize(DenormalizerInterface::class)->reveal());
+
+ self::assertNull($itemDataProvider->getItem(Foo::class, ['id' => 404]));
+ }
+
+ public function testGetItemWithCompositeIdentifiers()
+ {
+ self::expectException(InvalidArgumentException::class);
+ self::expectExceptionMessage('Composite identifiers not supported.');
+
+ $itemDataProvider = new ItemDataProvider($this->prophesize(Client::class)->reveal(), $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(), $this->prophesize(DenormalizerInterface::class)->reveal());
+ $itemDataProvider->getItem(CompositeRelation::class, ['compositeItem' => 1, 'compositeLabel' => 2]);
+ }
+}
diff --git a/tests/Bridge/Elasticsearch/DataProvider/PaginatorTest.php b/tests/Bridge/Elasticsearch/DataProvider/PaginatorTest.php
new file mode 100644
index 00000000000..369dce6cca5
--- /dev/null
+++ b/tests/Bridge/Elasticsearch/DataProvider/PaginatorTest.php
@@ -0,0 +1,173 @@
+
+ *
+ * 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\Tests\Bridge\Elasticsearch\DataProvider;
+
+use ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Paginator;
+use ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer;
+use ApiPlatform\Core\DataProvider\PaginatorInterface;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+class PaginatorTest extends TestCase
+{
+ protected const DOCUMENTS = [
+ 'hits' => [
+ 'total' => 8,
+ 'max_score' => 1,
+ 'hits' => [
+ [
+ '_index' => 'foo',
+ '_type' => '_doc',
+ '_id' => '5',
+ '_score' => 1,
+ '_source' => [
+ 'id' => 5,
+ 'name' => 'Fribourg',
+ 'bar' => 'gruobirf',
+ ],
+ ],
+ [
+ '_index' => 'foo',
+ '_type' => '_doc',
+ '_id' => '6',
+ '_score' => 1,
+ '_source' => [
+ 'id' => 6,
+ 'name' => 'Lausanne',
+ 'bar' => 'ennasual',
+ ],
+ ],
+ [
+ '_index' => 'foo',
+ '_type' => '_doc',
+ '_id' => '7',
+ '_score' => 1,
+ '_source' => [
+ 'id' => 7,
+ 'name' => 'Vallorbe',
+ 'bar' => 'ebrollav',
+ ],
+ ],
+ [
+ '_index' => 'foo',
+ '_type' => '_doc',
+ '_id' => '8',
+ '_score' => 1,
+ '_source' => [
+ 'id' => 8,
+ 'name' => 'Lugano',
+ 'bar' => 'onagul',
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ /**
+ * @var PaginatorInterface
+ */
+ protected $paginator;
+
+ public function testConstruct()
+ {
+ self::assertInstanceOf(PaginatorInterface::class, $this->paginator);
+ }
+
+ public function testCount()
+ {
+ self::assertCount(4, $this->paginator);
+ }
+
+ public function testGetLastPage()
+ {
+ self::assertSame(2., $this->paginator->getLastPage());
+ }
+
+ public function testGetLastPageWithZeroAsLimit()
+ {
+ self::assertSame(1., $this->getPaginator(0, 0)->getLastPage());
+ }
+
+ public function testGetLastPageWithNegativeLimit()
+ {
+ self::assertSame(1., $this->getPaginator(-1, 0)->getLastPage());
+ }
+
+ public function testGetTotalItems()
+ {
+ self::assertSame(8., $this->paginator->getTotalItems());
+ }
+
+ public function testGetCurrentPage()
+ {
+ self::assertSame(2., $this->paginator->getCurrentPage());
+ }
+
+ public function testGetCurrentPageWithZeroAsLimit()
+ {
+ self::assertSame(1., $this->getPaginator(0, 0)->getCurrentPage());
+ }
+
+ public function testGetCurrentPageWithNegativeLimit()
+ {
+ self::assertSame(1., $this->getPaginator(-1, 0)->getCurrentPage());
+ }
+
+ public function testGetItemsPerPage()
+ {
+ self::assertSame(4., $this->paginator->getItemsPerPage());
+ }
+
+ public function testGetIterator()
+ {
+ self::assertEquals(
+ array_map(
+ function (array $document): Foo {
+ return $this->denormalizeFoo($document['_source']);
+ },
+ static::DOCUMENTS['hits']['hits']
+ ),
+ iterator_to_array($this->paginator)
+ );
+ }
+
+ protected function setUp()
+ {
+ $this->paginator = $this->getPaginator(4, 4);
+ }
+
+ protected function getPaginator(int $limit, int $offset)
+ {
+ $denormalizerProphecy = $this->prophesize(DenormalizerInterface::class);
+
+ foreach (static::DOCUMENTS['hits']['hits'] as $document) {
+ $denormalizerProphecy
+ ->denormalize($document, Foo::class, ItemNormalizer::FORMAT, [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true])
+ ->willReturn($this->denormalizeFoo($document['_source']));
+ }
+
+ return new Paginator($denormalizerProphecy->reveal(), self::DOCUMENTS, Foo::class, $limit, $offset);
+ }
+
+ protected function denormalizeFoo(array $fooDocument): Foo
+ {
+ $foo = new Foo();
+ $foo->setName($fooDocument['name']);
+ $foo->setBar($fooDocument['bar']);
+
+ return $foo;
+ }
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index ef78e545fae..61f0fa3e5d9 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -680,6 +680,7 @@ private function getBaseContainerBuilderProphecy()
'api_platform.graphql.graphiql.enabled' => true,
'api_platform.resource_class_directories' => Argument::type('array'),
'api_platform.validator.serialize_payload_fields' => [],
+ 'api_platform.elasticsearch.enabled' => false,
];
foreach ($parameters as $key => $value) {