Skip to content

Commit 7ebc038

Browse files
committed
Merge branch 'khaperets-lookup-pipeline' into 2.5.x
2 parents 47e82e4 + 1352c1c commit 7ebc038

File tree

2 files changed

+155
-4
lines changed

2 files changed

+155
-4
lines changed

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

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ class Lookup extends Stage
3838
/** @var string */
3939
private $as;
4040

41+
/** @var array */
42+
private $let;
43+
44+
/** @var array */
45+
private $pipeline;
46+
47+
/** @var bool */
48+
private $excludeLocalAndForeignField = false;
49+
4150
public function __construct(Builder $builder, string $from, DocumentManager $documentManager, ClassMetadata $class)
4251
{
4352
parent::__construct($builder);
@@ -65,7 +74,7 @@ public function alias(string $alias): self
6574
/**
6675
* Specifies the collection or field name in the same database to perform the join with.
6776
*
68-
* The from collection cannot be sharded.
77+
* Starting in MongoDB 5.1, the from collection can be sharded.
6978
*/
7079
public function from(string $from): self
7180
{
@@ -98,14 +107,27 @@ public function from(string $from): self
98107

99108
public function getExpression(): array
100109
{
101-
return [
110+
$expression = [
102111
'$lookup' => [
103112
'from' => $this->from,
104-
'localField' => $this->localField,
105-
'foreignField' => $this->foreignField,
106113
'as' => $this->as,
107114
],
108115
];
116+
117+
if (! $this->excludeLocalAndForeignField) {
118+
$expression['$lookup']['localField'] = $this->localField;
119+
$expression['$lookup']['foreignField'] = $this->foreignField;
120+
}
121+
122+
if (! empty($this->let)) {
123+
$expression['$lookup']['let'] = $this->let;
124+
}
125+
126+
if (! empty($this->pipeline)) {
127+
$expression['$lookup']['pipeline'] = $this->pipeline;
128+
}
129+
130+
return $expression;
109131
}
110132

111133
/**
@@ -138,6 +160,46 @@ public function foreignField(string $foreignField): self
138160
return $this;
139161
}
140162

163+
/**
164+
* Optional. Specifies variables to use in the pipeline stages.
165+
*
166+
* Use the variable expressions to access the fields from
167+
* the joined collection's documents that are input to the pipeline.
168+
*/
169+
public function let(array $let): self
170+
{
171+
$this->let = $let;
172+
173+
return $this;
174+
}
175+
176+
/**
177+
* Specifies the pipeline to run on the joined collection.
178+
*
179+
* The pipeline determines the resulting documents from the joined collection.
180+
* To return all documents, specify an empty pipeline [].
181+
*
182+
* The pipeline cannot directly access the joined document fields.
183+
* Instead, define variables for the joined document fields using the let option
184+
* and then reference the variables in the pipeline stages.
185+
*/
186+
public function pipeline(array $pipeline): self
187+
{
188+
$this->pipeline = $pipeline;
189+
190+
return $this;
191+
}
192+
193+
/**
194+
* Excludes localField and foreignField from an expression.
195+
*/
196+
public function excludeLocalAndForeignField(): self
197+
{
198+
$this->excludeLocalAndForeignField = true;
199+
200+
return $this;
201+
}
202+
141203
protected function prepareFieldName(string $fieldName, ?ClassMetadata $class = null): string
142204
{
143205
if (! $class) {

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,95 @@ public function testLookupStage(): void
4747
$this->assertSame('alcaeus', $result[0]['user'][0]['username']);
4848
}
4949

50+
public function testLookupStageWithPipeline(): void
51+
{
52+
$builder = $this->dm->createAggregationBuilder(SimpleReferenceUser::class);
53+
$builder
54+
->lookup('user')
55+
->excludeLocalAndForeignField()
56+
->alias('user')
57+
->let(['name' => '$username'])
58+
->pipeline([
59+
[
60+
'$match' => ['username' => 'alcaeus'],
61+
],
62+
[
63+
'$project' => [
64+
'_id' => 0,
65+
'username' => 1,
66+
],
67+
],
68+
]);
69+
70+
$expectedPipeline = [
71+
[
72+
'$lookup' => [
73+
'from' => 'users',
74+
'as' => 'user',
75+
'let' => ['name' => '$username'],
76+
'pipeline' => [
77+
[
78+
'$match' => ['username' => 'alcaeus'],
79+
],
80+
[
81+
'$project' => [
82+
'_id' => 0,
83+
'username' => 1,
84+
],
85+
],
86+
],
87+
],
88+
],
89+
];
90+
91+
$this->assertEquals($expectedPipeline, $builder->getPipeline());
92+
}
93+
94+
public function testLookupStageWithPipelineAndLocalForeignFields(): void
95+
{
96+
$builder = $this->dm->createAggregationBuilder(SimpleReferenceUser::class);
97+
$builder
98+
->lookup('user')
99+
->alias('user')
100+
->let(['name' => '$username'])
101+
->pipeline([
102+
[
103+
'$match' => ['username' => 'alcaeus'],
104+
],
105+
[
106+
'$project' => [
107+
'_id' => 0,
108+
'username' => 1,
109+
],
110+
],
111+
]);
112+
113+
$expectedPipeline = [
114+
[
115+
'$lookup' => [
116+
'from' => 'users',
117+
'as' => 'user',
118+
'localField' => 'userId',
119+
'foreignField' => '_id',
120+
'let' => ['name' => '$username'],
121+
'pipeline' => [
122+
[
123+
'$match' => ['username' => 'alcaeus'],
124+
],
125+
[
126+
'$project' => [
127+
'_id' => 0,
128+
'username' => 1,
129+
],
130+
],
131+
],
132+
],
133+
],
134+
];
135+
136+
$this->assertEquals($expectedPipeline, $builder->getPipeline());
137+
}
138+
50139
public function testLookupStageWithFieldNameTranslation(): void
51140
{
52141
$builder = $this->dm->createAggregationBuilder(SimpleReferenceUser::class);

0 commit comments

Comments
 (0)