Skip to content

Commit b0252b6

Browse files
authored
Add support for using $text operator in a aggregation with a filter (#2739)
And fix `$geoNear` stage with empty query
1 parent 123ab7d commit b0252b6

File tree

5 files changed

+72
-11
lines changed

5 files changed

+72
-11
lines changed

lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use GeoJson\Geometry\Point;
1515
use MongoDB\Collection;
1616
use OutOfRangeException;
17+
use stdClass;
1718
use TypeError;
1819

1920
use function array_map;
@@ -264,6 +265,11 @@ public function getAggregation(array $options = []): IterableResult
264265
* @param bool $applyFilters Whether to apply filters on the aggregation
265266
* pipeline stage
266267
*
268+
* For pipelines where the first stage is a $match stage, it will merge
269+
* the document filters with the existing stage in a logical $and. This is
270+
* required as $text operator can be used anywhere in the first $match stage
271+
* or in the document filters.
272+
*
267273
* For pipelines where the first stage is a $geoNear stage, it will apply
268274
* the document filters and discriminator queries to the query portion of
269275
* the geoNear operation. For all other pipelines, it prepends a $match stage
@@ -306,14 +312,20 @@ public function getPipeline(/* bool $applyFilters = true */): array
306312
return $pipeline;
307313
}
308314

309-
if ($this->getStage(0) instanceof Stage\GeoNear) {
315+
if (isset($pipeline[0]['$geoNear'])) {
310316
$pipeline[0]['$geoNear']['query'] = $this->applyFilters($pipeline[0]['$geoNear']['query']);
311317

312318
return $pipeline;
313319
}
314320

321+
if (isset($pipeline[0]['$match'])) {
322+
$pipeline[0]['$match'] = $this->applyFilters($pipeline[0]['$match']);
323+
324+
return $pipeline;
325+
}
326+
315327
$matchExpression = $this->applyFilters([]);
316-
if ($matchExpression !== []) {
328+
if ((array) $matchExpression !== []) {
317329
array_unshift($pipeline, ['$match' => $matchExpression]);
318330
}
319331

@@ -696,18 +708,22 @@ public function addStage(Stage $stage): Stage
696708
/**
697709
* Applies filters and discriminator queries to the pipeline
698710
*
699-
* @param array<string, mixed> $query
711+
* @param array<string, mixed>|stdClass $query
700712
*
701-
* @return array<string, mixed>
713+
* @return array<string, mixed>|stdClass
702714
*/
703-
private function applyFilters(array $query): array
715+
private function applyFilters(array|stdClass $query): array|stdClass
704716
{
717+
if (! is_array($query)) {
718+
$query = (array) $query;
719+
}
720+
705721
$documentPersister = $this->getDocumentPersister();
706722

707723
$query = $documentPersister->addDiscriminatorToPreparedQuery($query);
708724
$query = $documentPersister->addFilterToPreparedQuery($query);
709725

710-
return $query;
726+
return $query === [] ? (object) $query : $query;
711727
}
712728

713729
private function getDocumentPersister(): DocumentPersister

lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,9 +1029,6 @@ public function addFilterToPreparedQuery(array $preparedQuery): array
10291029
{
10301030
/* If filter criteria exists for this class, prepare it and merge
10311031
* over the existing query.
1032-
*
1033-
* @todo Consider recursive merging in case the filter criteria and
1034-
* prepared query both contain top-level $and/$or operators.
10351032
*/
10361033
$filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
10371034
if ($filterCriteria) {

tests/Doctrine/ODM/MongoDB/Tests/Aggregation/BuilderTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,36 @@ public function testBuilderAppliesFilterAndDiscriminatorWithMatchStage(): void
368368
self::assertEquals($expectedPipeline, $builder->getPipeline());
369369
}
370370

371+
public function testBuilderMergeFilterAndDiscriminatorWithMatchStage(): void
372+
{
373+
$this->dm->getFilterCollection()->enable('testFilter');
374+
$filter = $this->dm->getFilterCollection()->getFilter('testFilter');
375+
$filter->setParameter('class', GuestServer::class);
376+
$filter->setParameter('field', 'filtered');
377+
$filter->setParameter('value', true);
378+
379+
$builder = $this->dm->createAggregationBuilder(GuestServer::class);
380+
$builder
381+
->match()
382+
->text('Paul');
383+
384+
$expectedPipeline = [
385+
[
386+
'$match' => [
387+
'$and' => [
388+
[
389+
'stype' => 'server_guest',
390+
'$text' => ['$search' => 'Paul'],
391+
],
392+
['filtered' => true],
393+
],
394+
],
395+
],
396+
];
397+
398+
self::assertEquals($expectedPipeline, $builder->getPipeline());
399+
}
400+
371401
public function testBuilderAppliesFilterAndDiscriminatorWithGeoNearStage(): void
372402
{
373403
$this->dm->getFilterCollection()->enable('testFilter');

tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/GeoNearTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function testLimitDoesNotCreateExtraStage(): void
7272
->geoNear(0, 0)
7373
->limit(1);
7474

75-
$stage = ['near' => [0, 0], 'spherical' => false, 'distanceField' => null, 'query' => [], 'num' => 1];
76-
self::assertSame([['$geoNear' => $stage]], $builder->getPipeline());
75+
$stage = ['near' => [0, 0], 'spherical' => false, 'distanceField' => null, 'query' => (object) [], 'num' => 1];
76+
self::assertEquals([['$geoNear' => $stage]], $builder->getPipeline());
7777
}
7878
}

tests/Doctrine/ODM/MongoDB/Tests/Functional/FilterTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,22 @@ public function testMultipleFiltersOnSameField(): void
334334
*/
335335
self::assertEmpty($this->getUsernamesWithFindAll());
336336
}
337+
338+
public function testFilterOnMatchStageUsingTextOperator(): void
339+
{
340+
$this->dm->getDocumentCollection(User::class)->createIndex(['username' => 'text']);
341+
342+
$this->fc->enable('testFilter');
343+
$testFilter = $this->fc->getFilter('testFilter');
344+
$testFilter->setParameter('class', User::class);
345+
$testFilter->setParameter('field', 'hits');
346+
$testFilter->setParameter('value', 10);
347+
348+
$builder = $this->dm->createAggregationBuilder(User::class)
349+
->match()
350+
->field('username')
351+
->text('John');
352+
353+
self::assertCount(1, $builder->getAggregation()->execute());
354+
}
337355
}

0 commit comments

Comments
 (0)