Skip to content

Commit 3cec5e3

Browse files
authored
Merge pull request #2554 from alcaeus/add-search-sort
Add sort operator to $search stage
2 parents ec7520e + 9ab1b97 commit 3cec5e3

File tree

7 files changed

+136
-85
lines changed

7 files changed

+136
-85
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,8 @@ public function indexStats(): Stage\IndexStats
236236
* Limits the number of documents passed to the next stage in the pipeline.
237237
*
238238
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/limit/
239-
*
240-
* @return Stage\Limit
241239
*/
242-
public function limit(int $limit)
240+
public function limit(int $limit): self
243241
{
244242
return $this->builder->limit($limit);
245243
}
@@ -435,7 +433,7 @@ public function sortByCount(string $expression): Stage\SortByCount
435433
* @param array<string, int|string>|string $fieldName Field name or array of field/order pairs
436434
* @param int|string $order Field order (if one field is specified)
437435
*/
438-
public function sort($fieldName, $order = null): Stage\Sort
436+
public function sort($fieldName, $order = null): self
439437
{
440438
return $this->builder->sort($fieldName, $order);
441439
}

lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,17 @@
1010
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperators;
1111
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperatorsTrait;
1212

13+
use function in_array;
14+
use function is_array;
15+
use function is_string;
16+
use function strtolower;
17+
1318
/**
19+
* @psalm-import-type SortDirectionKeywords from Sort
1420
* @psalm-type CountType = 'lowerBound'|'total'
21+
* @psalm-type SortMetaKeywords = 'searchScore'
22+
* @psalm-type SortMeta = array{'$meta': SortMetaKeywords}
23+
* @psalm-type SortShape = array<string, int|SortMeta|SortDirectionKeywords>
1524
* @psalm-type SearchStageExpression = array{
1625
* '$search': object{
1726
* index?: string,
@@ -25,6 +34,7 @@
2534
* maxNumPassages?: int,
2635
* },
2736
* returnStoredSource?: bool,
37+
* sort?: object,
2838
* autocomplete?: object,
2939
* compound?: object,
3040
* embeddedDocument?: object,
@@ -53,6 +63,9 @@ class Search extends Stage implements SupportsAllSearchOperators
5363
private ?bool $returnStoredSource = null;
5464
private ?SearchOperator $operator = null;
5565

66+
/** @var array<string, -1|1|SortMeta> */
67+
private array $sort = [];
68+
5669
public function __construct(Builder $builder)
5770
{
5871
parent::__construct($builder);
@@ -79,6 +92,10 @@ public function getExpression(): array
7992
$params->returnStoredSource = $this->returnStoredSource;
8093
}
8194

95+
if ($this->sort) {
96+
$params->sort = (object) $this->sort;
97+
}
98+
8299
if ($this->operator !== null) {
83100
$operatorName = $this->operator->getOperatorName();
84101
$params->$operatorName = $this->operator->getOperatorParams();
@@ -128,6 +145,33 @@ public function returnStoredSource(bool $returnStoredSource = true): static
128145
return $this;
129146
}
130147

148+
/**
149+
* @param array<string, int|string>|string $fieldName Field name or array of field/order pairs
150+
* @param int|string $order Field order (if one field is specified)
151+
* @psalm-param SortShape|string $fieldName
152+
* @psalm-param int|SortMeta|SortDirectionKeywords|null $order
153+
*/
154+
public function sort($fieldName, $order = null): static
155+
{
156+
$allowedMetaSort = ['searchScore'];
157+
158+
$fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];
159+
160+
foreach ($fields as $fieldName => $order) {
161+
if (is_string($order)) {
162+
if (in_array($order, $allowedMetaSort, true)) {
163+
$order = ['$meta' => $order];
164+
} else {
165+
$order = strtolower($order) === 'asc' ? 1 : -1;
166+
}
167+
}
168+
169+
$this->sort[$fieldName] = $order;
170+
}
171+
172+
return $this;
173+
}
174+
131175
/**
132176
* @param T $operator
133177
*

lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66

77
use Doctrine\ODM\MongoDB\Aggregation\Stage;
88
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search;
9+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort;
910

10-
/** @internal */
11+
/**
12+
* @internal
13+
*
14+
* @psalm-import-type SortDirectionKeywords from Sort
15+
* @psalm-import-type SortMetaKeywords from Search
16+
* @psalm-import-type SortMeta from Search
17+
* @psalm-import-type SortShape from Search
18+
*/
1119
abstract class AbstractSearchOperator extends Stage implements SearchOperator
1220
{
1321
public function __construct(private Search $search)
@@ -35,6 +43,17 @@ public function returnStoredSource(bool $returnStoredSource): Search
3543
return $this->search->returnStoredSource($returnStoredSource);
3644
}
3745

46+
/**
47+
* @param array<string, int|string>|string $fieldName Field name or array of field/order pairs
48+
* @param int|string $order Field order (if one field is specified)
49+
* @psalm-param SortShape|string $fieldName
50+
* @psalm-param int|SortMeta|SortDirectionKeywords|null $order
51+
*/
52+
public function sort($fieldName, $order = null): Search
53+
{
54+
return $this->search->sort($fieldName, $order);
55+
}
56+
3857
/**
3958
* @return array<string, object>
4059
* @psalm-return non-empty-array<non-empty-string, object>

phpstan-baseline.neon

Lines changed: 5 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
parameters:
22
ignoreErrors:
3-
-
4-
message: "#^Circular definition detected in type alias PipelineExpression\\.$#"
5-
count: 1
6-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php
7-
83
-
94
message: "#^PHPDoc tag @param references unknown parameter\\: \\$applyFilters$#"
105
count: 1
@@ -50,36 +45,11 @@ parameters:
5045
count: 1
5146
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Bucket/AbstractOutput.php
5247

53-
-
54-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Densify\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
55-
count: 1
56-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php
57-
58-
-
59-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
60-
count: 1
61-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php
62-
63-
-
64-
message: "#^Circular definition detected in type alias FacetStageExpression\\.$#"
65-
count: 1
66-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Facet.php
67-
68-
-
69-
message: "#^Return type \\(static\\(Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\GeoNear\\)\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\GeoNear\\:\\:limit\\(\\) should be compatible with return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Limit\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\:\\:limit\\(\\)$#"
70-
count: 1
71-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GeoNear.php
72-
7348
-
7449
message: "#^Unable to resolve the template type T in call to method Doctrine\\\\ODM\\\\MongoDB\\\\DocumentManager\\:\\:getClassMetadata\\(\\)$#"
7550
count: 1
7651
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GraphLookup.php
7752

78-
-
79-
message: "#^Circular definition detected in type alias PipelineParamType\\.$#"
80-
count: 1
81-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php
82-
8353
-
8454
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Lookup\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
8555
count: 1
@@ -95,31 +65,11 @@ parameters:
9565
count: 1
9666
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php
9767

98-
-
99-
message: "#^Circular definition detected in type alias WhenMatchedParamType\\.$#"
100-
count: 1
101-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php
102-
103-
-
104-
message: "#^Circular definition detected in type alias WhenMatchedType\\.$#"
105-
count: 1
106-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php
107-
108-
-
109-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Merge\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
110-
count: 1
111-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php
112-
11368
-
11469
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Merge\\:\\:whenMatched\\(\\) has parameter \\$whenMatched with no value type specified in iterable type array\\.$#"
11570
count: 1
11671
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php
11772

118-
-
119-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
120-
count: 1
121-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php
122-
12373
-
12474
message: "#^Property Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Merge\\:\\:\\$whenMatched type has no value type specified in iterable type array\\.$#"
12575
count: 1
@@ -140,21 +90,11 @@ parameters:
14090
count: 1
14191
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
14292

143-
-
144-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
145-
count: 1
146-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
147-
14893
-
14994
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\:\\:near\\(\\) has parameter \\$origin with no value type specified in iterable type array\\.$#"
15095
count: 1
15196
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
15297

153-
-
154-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
155-
count: 1
156-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
157-
15898
-
15999
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\\\Compound\\:\\:geoShape\\(\\) has parameter \\$geometry with no value type specified in iterable type array\\.$#"
160100
count: 1
@@ -470,31 +410,11 @@ parameters:
470410
count: 1
471411
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php
472412

473-
-
474-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\SetWindowFields\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
475-
count: 1
476-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php
477-
478-
-
479-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
480-
count: 1
481-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php
482-
483-
-
484-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\UnionWith\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
485-
count: 1
486-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php
487-
488413
-
489414
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\UnionWith\\:\\:pipeline\\(\\) has parameter \\$pipeline with no value type specified in iterable type array\\.$#"
490415
count: 1
491416
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php
492417

493-
-
494-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
495-
count: 1
496-
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php
497-
498418
-
499419
message: "#^Property Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\UnionWith\\:\\:\\$pipeline type has no value type specified in iterable type array\\.$#"
500420
count: 1
@@ -710,6 +630,11 @@ parameters:
710630
count: 1
711631
path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php
712632

633+
-
634+
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Tests\\\\Aggregation\\\\Stage\\\\SearchTest\\:\\:testSearchOperatorsWithSort\\(\\) has parameter \\$expectedOperator with no value type specified in iterable type array\\.$#"
635+
count: 1
636+
path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php
637+
713638
-
714639
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Tests\\\\Aggregation\\\\Stage\\\\SetWindowFieldsTest\\:\\:testOperators\\(\\) has parameter \\$args with no type specified\\.$#"
715640
count: 1

phpstan.neon.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ parameters:
1717
ignoreErrors:
1818
# Ignore typing providers in tests
1919
- '#^Method Doctrine\\ODM\\MongoDB\\Tests\\[^:]+(Test)::(get\w+|data\w+|provide\w+)\(\) return type has no value type specified in iterable type (array|iterable)\.#'
20+
21+
# Ignore circular references in Psalm types
22+
- message: '#^Circular definition detected in type alias#'
23+
path: lib/Doctrine/ODM/MongoDB/Aggregation/
24+
2025
# To be removed when reaching phpstan level 6
2126
checkMissingVarTagTypehint: true
2227
checkMissingTypehints: true
2328
checkMissingIterableValueType: true
29+
2430
# Disabled due to inconsistent errors upon encountering psalm types
2531
reportUnmatchedIgnoredErrors: false
2632

psalm-baseline.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
<code>IteratorAggregate</code>
66
</MissingTemplateParam>
77
</file>
8+
<file src="lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php">
9+
<MoreSpecificImplementedParamType>
10+
<code>$fieldName</code>
11+
<code>$order</code>
12+
</MoreSpecificImplementedParamType>
13+
</file>
14+
<file src="lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php">
15+
<MoreSpecificImplementedParamType>
16+
<code>$fieldName</code>
17+
<code>$order</code>
18+
</MoreSpecificImplementedParamType>
19+
</file>
820
<file src="lib/Doctrine/ODM/MongoDB/Configuration.php">
921
<TypeDoesNotContainType>
1022
<code><![CDATA[$reflectionClass->implementsInterface(ClassMetadataFactoryInterface::class)]]></code>

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,53 @@ public function testSearchOperators(array $expectedOperator, Closure $createOper
11811181
);
11821182
}
11831183

1184+
#[DataProvider('provideAutocompleteBuilders')]
1185+
#[DataProvider('provideCompoundBuilders')]
1186+
#[DataProvider('provideEmbeddedDocumentBuilders')]
1187+
#[DataProvider('provideEqualsBuilders')]
1188+
#[DataProvider('provideExistsBuilders')]
1189+
#[DataProvider('provideGeoShapeBuilders')]
1190+
#[DataProvider('provideGeoWithinBuilders')]
1191+
#[DataProvider('provideMoreLikeThisBuilders')]
1192+
#[DataProvider('provideNearBuilders')]
1193+
#[DataProvider('providePhraseBuilders')]
1194+
#[DataProvider('provideQueryStringBuilders')]
1195+
#[DataProvider('provideRangeBuilders')]
1196+
#[DataProvider('provideRegexBuilders')]
1197+
#[DataProvider('provideTextBuilders')]
1198+
#[DataProvider('provideWildcardBuilders')]
1199+
public function testSearchOperatorsWithSort(array $expectedOperator, Closure $createOperator): void
1200+
{
1201+
$baseExpected = [
1202+
'index' => 'my_search_index',
1203+
'sort' => (object) [
1204+
'unused' => ['$meta' => 'searchScore'],
1205+
'date' => -1,
1206+
'bar' => 1,
1207+
],
1208+
];
1209+
1210+
$searchStage = new Search($this->getTestAggregationBuilder());
1211+
$searchStage
1212+
->index('my_search_index');
1213+
1214+
$result = $createOperator($searchStage);
1215+
1216+
self::logicalOr(
1217+
new IsInstanceOf(AbstractSearchOperator::class),
1218+
new IsInstanceOf(Search::class),
1219+
);
1220+
1221+
$result
1222+
->sort(['unused' => 'searchScore', 'date' => -1])
1223+
->sort(['bar' => 1]);
1224+
1225+
self::assertEquals(
1226+
['$search' => (object) array_merge($baseExpected, $expectedOperator)],
1227+
$searchStage->getExpression(),
1228+
);
1229+
}
1230+
11841231
#[DataProvider('provideAutocompleteBuilders')]
11851232
#[DataProvider('provideEmbeddedDocumentBuilders')]
11861233
#[DataProvider('provideEqualsBuilders')]

0 commit comments

Comments
 (0)