Skip to content

Commit d49c801

Browse files
authoredFeb 25, 2025··
Merge 5.1 into 5.x (#3290)
2 parents 1974aec + f10f346 commit d49c801

File tree

5 files changed

+420
-54
lines changed

5 files changed

+420
-54
lines changed
 

‎docs/fundamentals/aggregation-builder.txt

+156-40
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ aggregation builder to create the stages of an aggregation pipeline:
3939

4040
- :ref:`laravel-add-aggregation-dependency`
4141
- :ref:`laravel-build-aggregation`
42+
- :ref:`laravel-aggregation-examples`
4243
- :ref:`laravel-create-custom-operator-factory`
4344

4445
.. tip::
@@ -71,12 +72,13 @@ includes the following line in the ``require`` object:
7172

7273
.. _laravel-build-aggregation:
7374

74-
Create an Aggregation Pipeline
75-
------------------------------
75+
Create Aggregation Stages
76+
-------------------------
7677

7778
To start an aggregation pipeline, call the ``Model::aggregate()`` method.
78-
Then, chain the aggregation stage methods in the sequence you want them to
79-
run.
79+
Then, chain aggregation stage methods and specify the necessary
80+
parameters for the stage. For example, you can call the ``sort()``
81+
operator method to build a ``$sort`` stage.
8082

8183
The aggregation builder includes the following namespaces that you can import
8284
to build aggregation stages:
@@ -88,17 +90,17 @@ to build aggregation stages:
8890

8991
.. tip::
9092

91-
To learn more about builder classes, see the `mongodb/mongodb-php-builder <https://github.com/mongodb/mongo-php-builder/>`__
93+
To learn more about builder classes, see the
94+
:github:`mongodb/mongodb-php-builder <mongodb/mongo-php-builder>`
9295
GitHub repository.
9396

94-
This section features the following examples, which show how to use common
95-
aggregation stages and combine stages to build an aggregation pipeline:
97+
This section features the following examples that show how to use common
98+
aggregation stages:
9699

97100
- :ref:`laravel-aggregation-match-stage-example`
98101
- :ref:`laravel-aggregation-group-stage-example`
99102
- :ref:`laravel-aggregation-sort-stage-example`
100103
- :ref:`laravel-aggregation-project-stage-example`
101-
- :ref:`laravel-aggregation-pipeline-example`
102104

103105
To learn more about MongoDB aggregation operators, see
104106
:manual:`Aggregation Stages </reference/operator/aggregation-pipeline/>` in
@@ -112,10 +114,10 @@ by the ``User`` model. You can add the sample data by running the following
112114
``insert()`` method:
113115

114116
.. literalinclude:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
115-
:language: php
116-
:dedent:
117-
:start-after: begin aggregation builder sample data
118-
:end-before: end aggregation builder sample data
117+
:language: php
118+
:dedent:
119+
:start-after: begin aggregation builder sample data
120+
:end-before: end aggregation builder sample data
119121

120122
.. _laravel-aggregation-match-stage-example:
121123

@@ -151,6 +153,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the documents
151153
returned by running the code:
152154

153155
.. io-code-block::
156+
:copyable: true
154157

155158
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
156159
:language: php
@@ -226,6 +229,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the documents
226229
returned by running the code:
227230

228231
.. io-code-block::
232+
:copyable: true
229233

230234
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
231235
:language: php
@@ -270,6 +274,7 @@ alphabetical order. Click the :guilabel:`{+code-output-label+}` button to see
270274
the documents returned by running the code:
271275

272276
.. io-code-block::
277+
:copyable: true
273278

274279
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
275280
:language: php
@@ -370,6 +375,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the data returned by
370375
running the code:
371376

372377
.. io-code-block::
378+
:copyable: true
373379

374380
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
375381
:language: php
@@ -390,56 +396,166 @@ running the code:
390396
{ "name": "Ellis Lee" }
391397
]
392398

399+
.. _laravel-aggregation-examples:
393400

394-
.. _laravel-aggregation-pipeline-example:
401+
Build Aggregation Pipelines
402+
---------------------------
395403

396-
Aggregation Pipeline Example
397-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
404+
To build an aggregation pipeline, call the ``Model::aggregate()`` method,
405+
then chain the aggregation stages in the sequence you want them to
406+
run. The examples in this section are adapted from the {+server-docs-name+}.
407+
Each example provides a link to the sample data that you can insert into
408+
your database to test the aggregation operation.
409+
410+
This section features the following examples, which show how to use common
411+
aggregation stages:
398412

399-
This aggregation pipeline example chains multiple stages. Each stage runs
400-
on the output retrieved from each preceding stage. In this example, the
401-
stages perform the following operations sequentially:
413+
- :ref:`laravel-aggregation-filter-group-example`
414+
- :ref:`laravel-aggregation-unwind-example`
415+
- :ref:`laravel-aggregation-lookup-example`
402416

403-
- Add the ``birth_year`` field to the documents and set the value to the year
404-
extracted from the ``birthday`` field.
405-
- Group the documents by the value of the ``occupation`` field and compute
406-
the average value of ``birth_year`` for each group by using the
407-
``Accumulator::avg()`` function. Assign the result of the computation to
408-
the ``birth_year_avg`` field.
409-
- Sort the documents by the group key field in ascending order.
410-
- Create the ``profession`` field from the value of the group key field,
411-
include the ``birth_year_avg`` field, and omit the ``_id`` field.
417+
.. _laravel-aggregation-filter-group-example:
418+
419+
Filter and Group Example
420+
~~~~~~~~~~~~~~~~~~~~~~~~
421+
422+
This example uses the sample data given in the :manual:`Calculate Count,
423+
Sum, and Average </reference/operator/aggregation/group/#calculate-count--sum--and-average>`
424+
section of the ``$group`` stage reference in the {+server-docs-name+}.
425+
426+
The following code example calculates the total sales amount, average
427+
sales quantity, and sale count for each day in the year 2014. To do so,
428+
it uses an aggregation pipeline that contains the following stages:
429+
430+
1. :manual:`$match </reference/operator/aggregation/match/>` stage to
431+
filter for documents that contain a ``date`` field in which the year is
432+
2014
433+
434+
#. :manual:`$group </reference/operator/aggregation/group/>` stage to
435+
group the documents by date and calculate the total sales amount,
436+
average sales quantity, and sale count for each group
437+
438+
#. :manual:`$sort </reference/operator/aggregation/sort/>` stage to
439+
sort the results by the total sale amount for each group in descending
440+
order
412441

413442
Click the :guilabel:`{+code-output-label+}` button to see the data returned by
414443
running the code:
415444

416445
.. io-code-block::
446+
:copyable: true
417447

418448
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
419449
:language: php
420450
:dedent:
421-
:start-after: begin pipeline example
422-
:end-before: end pipeline example
451+
:start-after: start-builder-match-group
452+
:end-before: end-builder-match-group
423453

424454
.. output::
425455
:language: json
426456
:visible: false
427457

428458
[
429-
{
430-
"birth_year_avg": 1991.5,
431-
"profession": "designer"
432-
},
433-
{
434-
"birth_year_avg": 1995.5,
435-
"profession": "engineer"
436-
}
459+
{ "_id": "2014-04-04", "totalSaleAmount": { "$numberDecimal": "200" }, "averageQuantity": 15, "count": 2 },
460+
{ "_id": "2014-03-15", "totalSaleAmount": { "$numberDecimal": "50" }, "averageQuantity": 10, "count": 1 },
461+
{ "_id": "2014-03-01", "totalSaleAmount": { "$numberDecimal": "40" }, "averageQuantity": 1.5, "count": 2 }
462+
]
463+
464+
.. _laravel-aggregation-unwind-example:
465+
466+
Unwind Embedded Arrays Example
467+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
468+
469+
This example uses the sample data given in the :manual:`Unwind Embedded Arrays
470+
</reference/operator/aggregation/unwind/#unwind-embedded-arrays>`
471+
section of the ``$unwind`` stage reference in the {+server-docs-name+}.
472+
473+
The following code example groups sold items by their tags and
474+
calculates the total sales amount for each tag. To do so,
475+
it uses an aggregation pipeline that contains the following stages:
476+
477+
1. :manual:`$unwind </reference/operator/aggregation/unwind/>` stage to
478+
output a separate document for each element in the ``items`` array
479+
480+
#. :manual:`$unwind </reference/operator/aggregation/unwind/>` stage to
481+
output a separate document for each element in the ``items.tags`` arrays
482+
483+
#. :manual:`$group </reference/operator/aggregation/group/>` stage to
484+
group the documents by the tag value and calculate the total sales
485+
amount of items that have each tag
486+
487+
Click the :guilabel:`{+code-output-label+}` button to see the data returned by
488+
running the code:
489+
490+
.. io-code-block::
491+
:copyable: true
492+
493+
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
494+
:start-after: start-builder-unwind
495+
:end-before: end-builder-unwind
496+
:language: php
497+
:dedent:
498+
499+
.. output::
500+
:language: json
501+
:visible: false
502+
503+
[
504+
{ "_id": "school", "totalSalesAmount": { "$numberDecimal": "104.85" } },
505+
{ "_id": "electronics", "totalSalesAmount": { "$numberDecimal": "800.00" } },
506+
{ "_id": "writing", "totalSalesAmount": { "$numberDecimal": "60.00" } },
507+
{ "_id": "office", "totalSalesAmount": { "$numberDecimal": "1019.60" } },
508+
{ "_id": "stationary", "totalSalesAmount": { "$numberDecimal": "264.45" } }
437509
]
438510

439-
.. note::
511+
.. _laravel-aggregation-lookup-example:
512+
513+
Single Equality Join Example
514+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
515+
516+
This example uses the sample data given in the :manual:`Perform a Single
517+
Equality Join with $lookup
518+
</reference/operator/aggregation/lookup/#perform-a-single-equality-join-with--lookup>`
519+
section of the ``$lookup`` stage reference in the {+server-docs-name+}.
520+
521+
The following code example joins the documents from the ``orders``
522+
collection with the documents from the ``inventory`` collection by using
523+
the ``item`` field from the ``orders`` collection and the ``sku`` field
524+
from the ``inventory`` collection.
440525

441-
Since this pipeline omits the ``match()`` stage, the input for the initial
442-
stage consists of all the documents in the collection.
526+
To do so, the example uses an aggregation pipeline that contains a
527+
:manual:`$lookup </reference/operator/aggregation/lookup/>` stage that
528+
specifies the collection to retrieve data from and the local and
529+
foreign field names.
530+
531+
Click the :guilabel:`{+code-output-label+}` button to see the data returned by
532+
running the code:
533+
534+
.. io-code-block::
535+
:copyable: true
536+
537+
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
538+
:start-after: start-builder-lookup
539+
:end-before: end-builder-lookup
540+
:language: php
541+
:dedent:
542+
543+
.. output::
544+
:language: json
545+
:visible: false
546+
547+
[
548+
{ "_id": 1, "item": "almonds", "price": 12, "quantity": 2, "inventory_docs": [
549+
{ "_id": 1, "sku": "almonds", "description": "product 1", "instock": 120 }
550+
] },
551+
{ "_id": 2, "item": "pecans", "price": 20, "quantity": 1, "inventory_docs": [
552+
{ "_id": 4, "sku": "pecans", "description": "product 4", "instock": 70 }
553+
] },
554+
{ "_id": 3, "inventory_docs": [
555+
{ "_id": 5, "sku": null, "description": "Incomplete" },
556+
{ "_id": 6 }
557+
] }
558+
]
443559

444560
.. _laravel-create-custom-operator-factory:
445561

‎docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php

+230-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
namespace App\Http\Controllers;
66

7+
use App\Models\Inventory;
8+
use App\Models\Order;
9+
use App\Models\Sale;
710
use DateTimeImmutable;
11+
use MongoDB\BSON\Decimal128;
812
use MongoDB\BSON\UTCDateTime;
913
use MongoDB\Builder\Accumulator;
1014
use MongoDB\Builder\Expression;
@@ -18,6 +22,10 @@ class AggregationsBuilderTest extends TestCase
1822
{
1923
protected function setUp(): void
2024
{
25+
require_once __DIR__ . '/Sale.php';
26+
require_once __DIR__ . '/Order.php';
27+
require_once __DIR__ . '/Inventory.php';
28+
2129
parent::setUp();
2230

2331
User::truncate();
@@ -84,27 +92,235 @@ public function testAggregationBuilderProjectStage(): void
8492
$this->assertArrayNotHasKey('_id', $result->first());
8593
}
8694

87-
public function testAggregationBuilderPipeline(): void
95+
public function testAggregationBuilderMatchGroup(): void
8896
{
89-
// begin pipeline example
90-
$pipeline = User::aggregate()
91-
->addFields(
92-
birth_year: Expression::year(
93-
Expression::dateFieldPath('birthday'),
94-
),
97+
Sale::truncate();
98+
99+
Sale::insert([
100+
[
101+
'_id' => 1,
102+
'item' => 'abc',
103+
'price' => new Decimal128('10'),
104+
'quantity' => 2,
105+
'date' => new UTCDateTime(new DateTimeImmutable('2014-03-01T08:00:00Z')),
106+
],
107+
[
108+
'_id' => 2,
109+
'item' => 'jkl',
110+
'price' => new Decimal128('20'),
111+
'quantity' => 1,
112+
'date' => new UTCDateTime(new DateTimeImmutable('2014-03-01T09:00:00Z')),
113+
],
114+
[
115+
'_id' => 3,
116+
'item' => 'xyz',
117+
'price' => new Decimal128('5'),
118+
'quantity' => 10,
119+
'date' => new UTCDateTime(new DateTimeImmutable('2014-03-15T09:00:00Z')),
120+
],
121+
[
122+
'_id' => 4,
123+
'item' => 'xyz',
124+
'price' => new Decimal128('5'),
125+
'quantity' => 20,
126+
'date' => new UTCDateTime(new DateTimeImmutable('2014-04-04T11:21:39.736Z')),
127+
],
128+
[
129+
'_id' => 5,
130+
'item' => 'abc',
131+
'price' => new Decimal128('10'),
132+
'quantity' => 10,
133+
'date' => new UTCDateTime(new DateTimeImmutable('2014-04-04T21:23:13.331Z')),
134+
],
135+
[
136+
'_id' => 6,
137+
'item' => 'def',
138+
'price' => new Decimal128('7.5'),
139+
'quantity' => 5,
140+
'date' => new UTCDateTime(new DateTimeImmutable('2015-06-04T05:08:13Z')),
141+
],
142+
[
143+
'_id' => 7,
144+
'item' => 'def',
145+
'price' => new Decimal128('7.5'),
146+
'quantity' => 10,
147+
'date' => new UTCDateTime(new DateTimeImmutable('2015-09-10T08:43:00Z')),
148+
],
149+
[
150+
'_id' => 8,
151+
'item' => 'abc',
152+
'price' => new Decimal128('10'),
153+
'quantity' => 5,
154+
'date' => new UTCDateTime(new DateTimeImmutable('2016-02-06T20:20:13Z')),
155+
],
156+
]);
157+
158+
// start-builder-match-group
159+
$pipeline = Sale::aggregate()
160+
->match(
161+
date: [
162+
Query::gte(new UTCDateTime(new DateTimeImmutable('2014-01-01'))),
163+
Query::lt(new UTCDateTime(new DateTimeImmutable('2015-01-01'))),
164+
],
95165
)
96166
->group(
97-
_id: Expression::fieldPath('occupation'),
98-
birth_year_avg: Accumulator::avg(Expression::numberFieldPath('birth_year')),
167+
_id: Expression::dateToString(Expression::dateFieldPath('date'), '%Y-%m-%d'),
168+
totalSaleAmount: Accumulator::sum(
169+
Expression::multiply(
170+
Expression::numberFieldPath('price'),
171+
Expression::numberFieldPath('quantity'),
172+
),
173+
),
174+
averageQuantity: Accumulator::avg(
175+
Expression::numberFieldPath('quantity'),
176+
),
177+
count: Accumulator::sum(1),
99178
)
100-
->sort(_id: Sort::Asc)
101-
->project(profession: Expression::fieldPath('_id'), birth_year_avg: 1, _id: 0);
102-
// end pipeline example
179+
->sort(
180+
totalSaleAmount: Sort::Desc,
181+
);
182+
// end-builder-match-group
103183

104184
$result = $pipeline->get();
105185

106-
$this->assertEquals(2, $result->count());
107-
$this->assertNotNull($result->first()['birth_year_avg']);
186+
$this->assertEquals(3, $result->count());
187+
$this->assertNotNull($result->first()['totalSaleAmount']);
188+
}
189+
190+
public function testAggregationBuilderUnwind(): void
191+
{
192+
Sale::truncate();
193+
194+
Sale::insert([
195+
[
196+
'_id' => '1',
197+
'items' => [
198+
[
199+
'name' => 'pens',
200+
'tags' => ['writing', 'office', 'school', 'stationary'],
201+
'price' => new Decimal128('12.00'),
202+
'quantity' => 5,
203+
],
204+
[
205+
'name' => 'envelopes',
206+
'tags' => ['stationary', 'office'],
207+
'price' => new Decimal128('19.95'),
208+
'quantity' => 8,
209+
],
210+
],
211+
],
212+
[
213+
'_id' => '2',
214+
'items' => [
215+
[
216+
'name' => 'laptop',
217+
'tags' => ['office', 'electronics'],
218+
'price' => new Decimal128('800.00'),
219+
'quantity' => 1,
220+
],
221+
[
222+
'name' => 'notepad',
223+
'tags' => ['stationary', 'school'],
224+
'price' => new Decimal128('14.95'),
225+
'quantity' => 3,
226+
],
227+
],
228+
],
229+
]);
230+
231+
// start-builder-unwind
232+
$pipeline = Sale::aggregate()
233+
->unwind(Expression::arrayFieldPath('items'))
234+
->unwind(Expression::arrayFieldPath('items.tags'))
235+
->group(
236+
_id: Expression::fieldPath('items.tags'),
237+
totalSalesAmount: Accumulator::sum(
238+
Expression::multiply(
239+
Expression::numberFieldPath('items.price'),
240+
Expression::numberFieldPath('items.quantity'),
241+
),
242+
),
243+
);
244+
// end-builder-unwind
245+
246+
$result = $pipeline->get();
247+
248+
$this->assertEquals(5, $result->count());
249+
$this->assertNotNull($result->first()['totalSalesAmount']);
250+
}
251+
252+
public function testAggregationBuilderLookup(): void
253+
{
254+
Order::truncate();
255+
Inventory::truncate();
256+
257+
Order::insert([
258+
[
259+
'_id' => 1,
260+
'item' => 'almonds',
261+
'price' => 12,
262+
'quantity' => 2,
263+
],
264+
[
265+
'_id' => 2,
266+
'item' => 'pecans',
267+
'price' => 20,
268+
'quantity' => 1,
269+
],
270+
[
271+
'_id' => 3,
272+
],
273+
]);
274+
275+
Inventory::insert([
276+
[
277+
'_id' => 1,
278+
'sku' => 'almonds',
279+
'description' => 'product 1',
280+
'instock' => 120,
281+
],
282+
[
283+
'_id' => 2,
284+
'sku' => 'bread',
285+
'description' => 'product 2',
286+
'instock' => 80,
287+
],
288+
[
289+
'_id' => 3,
290+
'sku' => 'cashews',
291+
'description' => 'product 3',
292+
'instock' => 60,
293+
],
294+
[
295+
'_id' => 4,
296+
'sku' => 'pecans',
297+
'description' => 'product 4',
298+
'instock' => 70,
299+
],
300+
[
301+
'_id' => 5,
302+
'sku' => null,
303+
'description' => 'Incomplete',
304+
],
305+
[
306+
'_id' => 6,
307+
],
308+
]);
309+
310+
// start-builder-lookup
311+
$pipeline = Order::aggregate()
312+
->lookup(
313+
from: 'inventory',
314+
localField: 'item',
315+
foreignField: 'sku',
316+
as: 'inventory_docs',
317+
);
318+
// end-builder-lookup
319+
320+
$result = $pipeline->get();
321+
322+
$this->assertEquals(3, $result->count());
323+
$this->assertNotNull($result->first()['item']);
108324
}
109325

110326
// phpcs:disable Squiz.Commenting.FunctionComment.WrongStyle
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use MongoDB\Laravel\Eloquent\Model;
6+
7+
class Inventory extends Model
8+
{
9+
protected $connection = 'mongodb';
10+
protected $table = 'inventory';
11+
protected $fillable = ['_id', 'sku', 'description', 'instock'];
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use MongoDB\Laravel\Eloquent\Model;
6+
7+
class Order extends Model
8+
{
9+
protected $connection = 'mongodb';
10+
protected $fillable = ['_id', 'item', 'price', 'quantity'];
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use MongoDB\Laravel\Eloquent\Model;
6+
7+
class Sale extends Model
8+
{
9+
protected $connection = 'mongodb';
10+
protected $fillable = ['_id', 'item', 'price', 'quantity', 'date', 'items'];
11+
}

0 commit comments

Comments
 (0)
Please sign in to comment.