From f10f346dc4d0a0a3fcccda6591dc4f85310b8d78 Mon Sep 17 00:00:00 2001
From: Rea Rustagi <85902999+rustagir@users.noreply.github.com>
Date: Tue, 25 Feb 2025 11:26:01 -0500
Subject: [PATCH] DOCSP-44554: add more aggregation examples (#3272)
* DOCSP-44554: add more agg exs
* import model fps
* fix formatting
* CI errors
* language formatting
* MW PR fixes 1
* JT small fix
---
docs/fundamentals/aggregation-builder.txt | 196 +++++++++++---
.../aggregation/AggregationsBuilderTest.php | 244 +++++++++++++++++-
.../fundamentals/aggregation/Inventory.php | 12 +
.../fundamentals/aggregation/Order.php | 11 +
.../fundamentals/aggregation/Sale.php | 11 +
5 files changed, 420 insertions(+), 54 deletions(-)
create mode 100644 docs/includes/fundamentals/aggregation/Inventory.php
create mode 100644 docs/includes/fundamentals/aggregation/Order.php
create mode 100644 docs/includes/fundamentals/aggregation/Sale.php
diff --git a/docs/fundamentals/aggregation-builder.txt b/docs/fundamentals/aggregation-builder.txt
index 0dbcd3823..3169acfeb 100644
--- a/docs/fundamentals/aggregation-builder.txt
+++ b/docs/fundamentals/aggregation-builder.txt
@@ -39,6 +39,7 @@ aggregation builder to create the stages of an aggregation pipeline:
- :ref:`laravel-add-aggregation-dependency`
- :ref:`laravel-build-aggregation`
+- :ref:`laravel-aggregation-examples`
- :ref:`laravel-create-custom-operator-factory`
.. tip::
@@ -71,12 +72,13 @@ includes the following line in the ``require`` object:
.. _laravel-build-aggregation:
-Create an Aggregation Pipeline
-------------------------------
+Create Aggregation Stages
+-------------------------
To start an aggregation pipeline, call the ``Model::aggregate()`` method.
-Then, chain the aggregation stage methods in the sequence you want them to
-run.
+Then, chain aggregation stage methods and specify the necessary
+parameters for the stage. For example, you can call the ``sort()``
+operator method to build a ``$sort`` stage.
The aggregation builder includes the following namespaces that you can import
to build aggregation stages:
@@ -88,17 +90,17 @@ to build aggregation stages:
.. tip::
- To learn more about builder classes, see the `mongodb/mongodb-php-builder `__
+ To learn more about builder classes, see the
+ :github:`mongodb/mongodb-php-builder `
GitHub repository.
-This section features the following examples, which show how to use common
-aggregation stages and combine stages to build an aggregation pipeline:
+This section features the following examples that show how to use common
+aggregation stages:
- :ref:`laravel-aggregation-match-stage-example`
- :ref:`laravel-aggregation-group-stage-example`
- :ref:`laravel-aggregation-sort-stage-example`
- :ref:`laravel-aggregation-project-stage-example`
-- :ref:`laravel-aggregation-pipeline-example`
To learn more about MongoDB aggregation operators, see
:manual:`Aggregation Stages ` in
@@ -112,10 +114,10 @@ by the ``User`` model. You can add the sample data by running the following
``insert()`` method:
.. literalinclude:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
- :language: php
- :dedent:
- :start-after: begin aggregation builder sample data
- :end-before: end aggregation builder sample data
+ :language: php
+ :dedent:
+ :start-after: begin aggregation builder sample data
+ :end-before: end aggregation builder sample data
.. _laravel-aggregation-match-stage-example:
@@ -151,6 +153,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the documents
returned by running the code:
.. io-code-block::
+ :copyable: true
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
:language: php
@@ -226,6 +229,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the documents
returned by running the code:
.. io-code-block::
+ :copyable: true
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
:language: php
@@ -270,6 +274,7 @@ alphabetical order. Click the :guilabel:`{+code-output-label+}` button to see
the documents returned by running the code:
.. io-code-block::
+ :copyable: true
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
:language: php
@@ -370,6 +375,7 @@ Click the :guilabel:`{+code-output-label+}` button to see the data returned by
running the code:
.. io-code-block::
+ :copyable: true
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
:language: php
@@ -390,56 +396,166 @@ running the code:
{ "name": "Ellis Lee" }
]
+.. _laravel-aggregation-examples:
-.. _laravel-aggregation-pipeline-example:
+Build Aggregation Pipelines
+---------------------------
-Aggregation Pipeline Example
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+To build an aggregation pipeline, call the ``Model::aggregate()`` method,
+then chain the aggregation stages in the sequence you want them to
+run. The examples in this section are adapted from the {+server-docs-name+}.
+Each example provides a link to the sample data that you can insert into
+your database to test the aggregation operation.
+
+This section features the following examples, which show how to use common
+aggregation stages:
-This aggregation pipeline example chains multiple stages. Each stage runs
-on the output retrieved from each preceding stage. In this example, the
-stages perform the following operations sequentially:
+- :ref:`laravel-aggregation-filter-group-example`
+- :ref:`laravel-aggregation-unwind-example`
+- :ref:`laravel-aggregation-lookup-example`
-- Add the ``birth_year`` field to the documents and set the value to the year
- extracted from the ``birthday`` field.
-- Group the documents by the value of the ``occupation`` field and compute
- the average value of ``birth_year`` for each group by using the
- ``Accumulator::avg()`` function. Assign the result of the computation to
- the ``birth_year_avg`` field.
-- Sort the documents by the group key field in ascending order.
-- Create the ``profession`` field from the value of the group key field,
- include the ``birth_year_avg`` field, and omit the ``_id`` field.
+.. _laravel-aggregation-filter-group-example:
+
+Filter and Group Example
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example uses the sample data given in the :manual:`Calculate Count,
+Sum, and Average `
+section of the ``$group`` stage reference in the {+server-docs-name+}.
+
+The following code example calculates the total sales amount, average
+sales quantity, and sale count for each day in the year 2014. To do so,
+it uses an aggregation pipeline that contains the following stages:
+
+1. :manual:`$match ` stage to
+ filter for documents that contain a ``date`` field in which the year is
+ 2014
+
+#. :manual:`$group ` stage to
+ group the documents by date and calculate the total sales amount,
+ average sales quantity, and sale count for each group
+
+#. :manual:`$sort ` stage to
+ sort the results by the total sale amount for each group in descending
+ order
Click the :guilabel:`{+code-output-label+}` button to see the data returned by
running the code:
.. io-code-block::
+ :copyable: true
.. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
:language: php
:dedent:
- :start-after: begin pipeline example
- :end-before: end pipeline example
+ :start-after: start-builder-match-group
+ :end-before: end-builder-match-group
.. output::
:language: json
:visible: false
[
- {
- "birth_year_avg": 1991.5,
- "profession": "designer"
- },
- {
- "birth_year_avg": 1995.5,
- "profession": "engineer"
- }
+ { "_id": "2014-04-04", "totalSaleAmount": { "$numberDecimal": "200" }, "averageQuantity": 15, "count": 2 },
+ { "_id": "2014-03-15", "totalSaleAmount": { "$numberDecimal": "50" }, "averageQuantity": 10, "count": 1 },
+ { "_id": "2014-03-01", "totalSaleAmount": { "$numberDecimal": "40" }, "averageQuantity": 1.5, "count": 2 }
+ ]
+
+.. _laravel-aggregation-unwind-example:
+
+Unwind Embedded Arrays Example
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example uses the sample data given in the :manual:`Unwind Embedded Arrays
+`
+section of the ``$unwind`` stage reference in the {+server-docs-name+}.
+
+The following code example groups sold items by their tags and
+calculates the total sales amount for each tag. To do so,
+it uses an aggregation pipeline that contains the following stages:
+
+1. :manual:`$unwind ` stage to
+ output a separate document for each element in the ``items`` array
+
+#. :manual:`$unwind ` stage to
+ output a separate document for each element in the ``items.tags`` arrays
+
+#. :manual:`$group ` stage to
+ group the documents by the tag value and calculate the total sales
+ amount of items that have each tag
+
+Click the :guilabel:`{+code-output-label+}` button to see the data returned by
+running the code:
+
+.. io-code-block::
+ :copyable: true
+
+ .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
+ :start-after: start-builder-unwind
+ :end-before: end-builder-unwind
+ :language: php
+ :dedent:
+
+ .. output::
+ :language: json
+ :visible: false
+
+ [
+ { "_id": "school", "totalSalesAmount": { "$numberDecimal": "104.85" } },
+ { "_id": "electronics", "totalSalesAmount": { "$numberDecimal": "800.00" } },
+ { "_id": "writing", "totalSalesAmount": { "$numberDecimal": "60.00" } },
+ { "_id": "office", "totalSalesAmount": { "$numberDecimal": "1019.60" } },
+ { "_id": "stationary", "totalSalesAmount": { "$numberDecimal": "264.45" } }
]
-.. note::
+.. _laravel-aggregation-lookup-example:
+
+Single Equality Join Example
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example uses the sample data given in the :manual:`Perform a Single
+Equality Join with $lookup
+`
+section of the ``$lookup`` stage reference in the {+server-docs-name+}.
+
+The following code example joins the documents from the ``orders``
+collection with the documents from the ``inventory`` collection by using
+the ``item`` field from the ``orders`` collection and the ``sku`` field
+from the ``inventory`` collection.
- Since this pipeline omits the ``match()`` stage, the input for the initial
- stage consists of all the documents in the collection.
+To do so, the example uses an aggregation pipeline that contains a
+:manual:`$lookup ` stage that
+specifies the collection to retrieve data from and the local and
+foreign field names.
+
+Click the :guilabel:`{+code-output-label+}` button to see the data returned by
+running the code:
+
+.. io-code-block::
+ :copyable: true
+
+ .. input:: /includes/fundamentals/aggregation/AggregationsBuilderTest.php
+ :start-after: start-builder-lookup
+ :end-before: end-builder-lookup
+ :language: php
+ :dedent:
+
+ .. output::
+ :language: json
+ :visible: false
+
+ [
+ { "_id": 1, "item": "almonds", "price": 12, "quantity": 2, "inventory_docs": [
+ { "_id": 1, "sku": "almonds", "description": "product 1", "instock": 120 }
+ ] },
+ { "_id": 2, "item": "pecans", "price": 20, "quantity": 1, "inventory_docs": [
+ { "_id": 4, "sku": "pecans", "description": "product 4", "instock": 70 }
+ ] },
+ { "_id": 3, "inventory_docs": [
+ { "_id": 5, "sku": null, "description": "Incomplete" },
+ { "_id": 6 }
+ ] }
+ ]
.. _laravel-create-custom-operator-factory:
diff --git a/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php b/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php
index 4880ee75f..49f7d5c8f 100644
--- a/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php
+++ b/docs/includes/fundamentals/aggregation/AggregationsBuilderTest.php
@@ -4,7 +4,11 @@
namespace App\Http\Controllers;
+use App\Models\Inventory;
+use App\Models\Order;
+use App\Models\Sale;
use DateTimeImmutable;
+use MongoDB\BSON\Decimal128;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Builder\Accumulator;
use MongoDB\Builder\Expression;
@@ -18,6 +22,10 @@ class AggregationsBuilderTest extends TestCase
{
protected function setUp(): void
{
+ require_once __DIR__ . '/Sale.php';
+ require_once __DIR__ . '/Order.php';
+ require_once __DIR__ . '/Inventory.php';
+
parent::setUp();
User::truncate();
@@ -84,27 +92,235 @@ public function testAggregationBuilderProjectStage(): void
$this->assertArrayNotHasKey('_id', $result->first());
}
- public function testAggregationBuilderPipeline(): void
+ public function testAggregationBuilderMatchGroup(): void
{
- // begin pipeline example
- $pipeline = User::aggregate()
- ->addFields(
- birth_year: Expression::year(
- Expression::dateFieldPath('birthday'),
- ),
+ Sale::truncate();
+
+ Sale::insert([
+ [
+ '_id' => 1,
+ 'item' => 'abc',
+ 'price' => new Decimal128('10'),
+ 'quantity' => 2,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2014-03-01T08:00:00Z')),
+ ],
+ [
+ '_id' => 2,
+ 'item' => 'jkl',
+ 'price' => new Decimal128('20'),
+ 'quantity' => 1,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2014-03-01T09:00:00Z')),
+ ],
+ [
+ '_id' => 3,
+ 'item' => 'xyz',
+ 'price' => new Decimal128('5'),
+ 'quantity' => 10,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2014-03-15T09:00:00Z')),
+ ],
+ [
+ '_id' => 4,
+ 'item' => 'xyz',
+ 'price' => new Decimal128('5'),
+ 'quantity' => 20,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2014-04-04T11:21:39.736Z')),
+ ],
+ [
+ '_id' => 5,
+ 'item' => 'abc',
+ 'price' => new Decimal128('10'),
+ 'quantity' => 10,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2014-04-04T21:23:13.331Z')),
+ ],
+ [
+ '_id' => 6,
+ 'item' => 'def',
+ 'price' => new Decimal128('7.5'),
+ 'quantity' => 5,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2015-06-04T05:08:13Z')),
+ ],
+ [
+ '_id' => 7,
+ 'item' => 'def',
+ 'price' => new Decimal128('7.5'),
+ 'quantity' => 10,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2015-09-10T08:43:00Z')),
+ ],
+ [
+ '_id' => 8,
+ 'item' => 'abc',
+ 'price' => new Decimal128('10'),
+ 'quantity' => 5,
+ 'date' => new UTCDateTime(new DateTimeImmutable('2016-02-06T20:20:13Z')),
+ ],
+ ]);
+
+ // start-builder-match-group
+ $pipeline = Sale::aggregate()
+ ->match(
+ date: [
+ Query::gte(new UTCDateTime(new DateTimeImmutable('2014-01-01'))),
+ Query::lt(new UTCDateTime(new DateTimeImmutable('2015-01-01'))),
+ ],
)
->group(
- _id: Expression::fieldPath('occupation'),
- birth_year_avg: Accumulator::avg(Expression::numberFieldPath('birth_year')),
+ _id: Expression::dateToString(Expression::dateFieldPath('date'), '%Y-%m-%d'),
+ totalSaleAmount: Accumulator::sum(
+ Expression::multiply(
+ Expression::numberFieldPath('price'),
+ Expression::numberFieldPath('quantity'),
+ ),
+ ),
+ averageQuantity: Accumulator::avg(
+ Expression::numberFieldPath('quantity'),
+ ),
+ count: Accumulator::sum(1),
)
- ->sort(_id: Sort::Asc)
- ->project(profession: Expression::fieldPath('_id'), birth_year_avg: 1, _id: 0);
- // end pipeline example
+ ->sort(
+ totalSaleAmount: Sort::Desc,
+ );
+ // end-builder-match-group
$result = $pipeline->get();
- $this->assertEquals(2, $result->count());
- $this->assertNotNull($result->first()['birth_year_avg']);
+ $this->assertEquals(3, $result->count());
+ $this->assertNotNull($result->first()['totalSaleAmount']);
+ }
+
+ public function testAggregationBuilderUnwind(): void
+ {
+ Sale::truncate();
+
+ Sale::insert([
+ [
+ '_id' => '1',
+ 'items' => [
+ [
+ 'name' => 'pens',
+ 'tags' => ['writing', 'office', 'school', 'stationary'],
+ 'price' => new Decimal128('12.00'),
+ 'quantity' => 5,
+ ],
+ [
+ 'name' => 'envelopes',
+ 'tags' => ['stationary', 'office'],
+ 'price' => new Decimal128('19.95'),
+ 'quantity' => 8,
+ ],
+ ],
+ ],
+ [
+ '_id' => '2',
+ 'items' => [
+ [
+ 'name' => 'laptop',
+ 'tags' => ['office', 'electronics'],
+ 'price' => new Decimal128('800.00'),
+ 'quantity' => 1,
+ ],
+ [
+ 'name' => 'notepad',
+ 'tags' => ['stationary', 'school'],
+ 'price' => new Decimal128('14.95'),
+ 'quantity' => 3,
+ ],
+ ],
+ ],
+ ]);
+
+ // start-builder-unwind
+ $pipeline = Sale::aggregate()
+ ->unwind(Expression::arrayFieldPath('items'))
+ ->unwind(Expression::arrayFieldPath('items.tags'))
+ ->group(
+ _id: Expression::fieldPath('items.tags'),
+ totalSalesAmount: Accumulator::sum(
+ Expression::multiply(
+ Expression::numberFieldPath('items.price'),
+ Expression::numberFieldPath('items.quantity'),
+ ),
+ ),
+ );
+ // end-builder-unwind
+
+ $result = $pipeline->get();
+
+ $this->assertEquals(5, $result->count());
+ $this->assertNotNull($result->first()['totalSalesAmount']);
+ }
+
+ public function testAggregationBuilderLookup(): void
+ {
+ Order::truncate();
+ Inventory::truncate();
+
+ Order::insert([
+ [
+ '_id' => 1,
+ 'item' => 'almonds',
+ 'price' => 12,
+ 'quantity' => 2,
+ ],
+ [
+ '_id' => 2,
+ 'item' => 'pecans',
+ 'price' => 20,
+ 'quantity' => 1,
+ ],
+ [
+ '_id' => 3,
+ ],
+ ]);
+
+ Inventory::insert([
+ [
+ '_id' => 1,
+ 'sku' => 'almonds',
+ 'description' => 'product 1',
+ 'instock' => 120,
+ ],
+ [
+ '_id' => 2,
+ 'sku' => 'bread',
+ 'description' => 'product 2',
+ 'instock' => 80,
+ ],
+ [
+ '_id' => 3,
+ 'sku' => 'cashews',
+ 'description' => 'product 3',
+ 'instock' => 60,
+ ],
+ [
+ '_id' => 4,
+ 'sku' => 'pecans',
+ 'description' => 'product 4',
+ 'instock' => 70,
+ ],
+ [
+ '_id' => 5,
+ 'sku' => null,
+ 'description' => 'Incomplete',
+ ],
+ [
+ '_id' => 6,
+ ],
+ ]);
+
+ // start-builder-lookup
+ $pipeline = Order::aggregate()
+ ->lookup(
+ from: 'inventory',
+ localField: 'item',
+ foreignField: 'sku',
+ as: 'inventory_docs',
+ );
+ // end-builder-lookup
+
+ $result = $pipeline->get();
+
+ $this->assertEquals(3, $result->count());
+ $this->assertNotNull($result->first()['item']);
}
// phpcs:disable Squiz.Commenting.FunctionComment.WrongStyle
diff --git a/docs/includes/fundamentals/aggregation/Inventory.php b/docs/includes/fundamentals/aggregation/Inventory.php
new file mode 100644
index 000000000..e1cdc7be1
--- /dev/null
+++ b/docs/includes/fundamentals/aggregation/Inventory.php
@@ -0,0 +1,12 @@
+