diff --git a/src/Doctrine/Odm/Extension/OrderExtension.php b/src/Doctrine/Odm/Extension/OrderExtension.php index 0afb8d70fc..7e64a6dc42 100644 --- a/src/Doctrine/Odm/Extension/OrderExtension.php +++ b/src/Doctrine/Odm/Extension/OrderExtension.php @@ -17,6 +17,7 @@ use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait; use ApiPlatform\Metadata\Operation; use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Doctrine\ODM\MongoDB\Aggregation\Stage\Search; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\Persistence\ManagerRegistry; @@ -66,15 +67,20 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC if ($this->isPropertyNested($field, $resourceClass)) { [$field] = $this->addLookupsForNestedProperty($field, $aggregationBuilder, $resourceClass, true); } - $aggregationBuilder->sort( - $context['mongodb_odm_sort_fields'] = ($context['mongodb_odm_sort_fields'] ?? []) + [$field => $order] - ); + + $context['mongodb_odm_sort_fields'] = ($context['mongodb_odm_sort_fields'] ?? []) + [$field => $order]; + if ($this->isSearchPipeline($aggregationBuilder)) { + $aggregationBuilder->getStage(0) + ->sort($context['mongodb_odm_sort_fields']); + } else { + $aggregationBuilder->sort($context['mongodb_odm_sort_fields']); + } } return; } - if (null !== $this->order) { + if (null !== $this->order && !$this->isSearchPipeline($aggregationBuilder)) { foreach ($identifiers as $identifier) { $aggregationBuilder->sort( $context['mongodb_odm_sort_fields'] = ($context['mongodb_odm_sort_fields'] ?? []) + [$identifier => $this->order] @@ -90,24 +96,26 @@ protected function getManagerRegistry(): ManagerRegistry private function hasSortStage(Builder $aggregationBuilder): bool { - $shouldStop = false; - $index = 0; - - do { - try { + try { + for ($index = 0; true; ++$index) { if ($aggregationBuilder->getStage($index) instanceof Sort) { // If at least one stage is sort, then it has sorting return true; } - } catch (\OutOfRangeException $outOfRangeException) { - // There is no more stages on the aggregation builder - $shouldStop = true; } + } catch (\OutOfRangeException) { + // There is no more stages on the aggregation builder + return false; + } + } - ++$index; - } while (!$shouldStop); - - // No stage was sort, and we iterated through all stages - return false; + private function isSearchPipeline(Builder $aggregationBuilder): bool + { + try { + return $aggregationBuilder->getStage(0) instanceof Search; + } catch (\OutOfRangeException) { + // Empty pipeline + return false; + } } } diff --git a/src/Doctrine/Odm/Extension/ParameterExtension.php b/src/Doctrine/Odm/Extension/ParameterExtension.php index 585ea9ec05..9dff1dd01d 100644 --- a/src/Doctrine/Odm/Extension/ParameterExtension.php +++ b/src/Doctrine/Odm/Extension/ParameterExtension.php @@ -89,15 +89,10 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass $filter->setProperties($properties ?? []); } - $filterContext = ['filters' => $values, 'parameter' => $parameter, 'match' => $context['match'] ?? null]; + $specificContext = ['filters' => $values, 'parameter' => $parameter]; + $filterContext = array_replace($context, $specificContext); $filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext); - // update by reference - if (isset($filterContext['mongodb_odm_sort_fields'])) { - $context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields']; - } - if (isset($filterContext['match'])) { - $context['match'] = $filterContext['match']; - } + $context = array_replace($context, array_diff_key($filterContext, $specificContext)); } if (isset($context['match'])) { diff --git a/src/Doctrine/Odm/Filter/AtlasSearchFilter.php b/src/Doctrine/Odm/Filter/AtlasSearchFilter.php new file mode 100644 index 0000000000..f71527c300 --- /dev/null +++ b/src/Doctrine/Odm/Filter/AtlasSearchFilter.php @@ -0,0 +1,99 @@ + + * + * 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\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Doctrine\ODM\MongoDB\LockException; +use Doctrine\ODM\MongoDB\Mapping\MappingException; + +/** + * @author Jérôme Tamarelle + */ +final class AtlasSearchFilter implements FilterInterface, OpenApiParameterFilterInterface, ManagerRegistryAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use ManagerRegistryAwareTrait; + use OpenApiFilterTrait; + + /** + * @see https://www.mongodb.com/docs/atlas/atlas-search/compound/ + * + * @param string $term The term to use in the Atlas Search query (must, mustNot, should, filter). Default to "must". + */ + public function __construct( + private readonly string $index = 'default', + private readonly string $operator = 'text', + private readonly string $term = 'must', + ) { + } + + /** + * @throws MappingException + * @throws LockException + */ + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + $parameter = $context['parameter']; + $property = $parameter->getProperty(); + $value = $parameter->getValue(); + + if (!isset($context['search'])) { + $searchStage = $context['search']['stage'] = $aggregationBuilder->search(); + $searchStage->index($this->index); + $context['search']['compound'] = $searchStage->compound(); + } + + $compound = $context['search']['compound']; + $compound->{$this->term}(); + + switch ($this->operator) { + case 'queryString': + $operator = $compound->queryString() + ->defaultPath($property) + ->query($value); + break; + case 'wildcard': + $operator = $compound->wildcard() + ->path($property) + ->query($value); + break; + case 'text': + $operator = $compound->text() + ->path($property) + ->query($value) + ->fuzzy(maxEdits: 1); + break; + case 'phrase': + $operator = $compound->phrase() + ->path($property) + ->query($value) + ->slop(2); + break; + case 'autocomplete': + $operator = $compound->autocomplete() + ->path($property) + ->query($value) + ->fuzzy(maxEdits: 1); + break; + default: + throw new \InvalidArgumentException(\sprintf('Unsupported operator "%s" for AtlasSearchFilter', $this->operator)); + } + } +}