Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce aggregation API to replicate query API #2211

Merged
merged 4 commits into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions UPGRADE-2.1.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# UPGRADE FROM 2.0 to 2.1

The `Doctrine\ODM\MongoDB\Id\AbstractIdGenerator` class has been deprecated. Custom ID generators must implement
the `Doctrine\ODM\MongoDB\Id\IdGenerator` interface.
## ID generators

The `Doctrine\ODM\MongoDB\Mapping\ClassMetadata` class has been marked final. The class will no longer be extendable
in ODM 3.0.
The `Doctrine\ODM\MongoDB\Id\AbstractIdGenerator` class has been deprecated.
Custom ID generators must implement the `Doctrine\ODM\MongoDB\Id\IdGenerator`
interface.

The `boolean`, `integer`, and `int_id` mapping types have been deprecated. Use their shorthand counterparts: `bool` and
`int` respectively. The `int_id` and `int` types are working exactly the same.
## Metadata

The `Doctrine\ODM\MongoDB\Mapping\ClassMetadata` class has been marked final and
will no longer be extendable in 3.0.

The `boolean`, `integer`, and `int_id` mapping types have been deprecated. Use
the `bool`, `int`, and `int` types, respectively. These types behave exactly the
same.
10 changes: 10 additions & 0 deletions UPGRADE-2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# UPGRADE FROM 2.1 to 2.2

## Aggregation

The new `Doctrine\ODM\MongoDB\Aggregation\Builder::getAggregation()` method
returns an `Doctrine\ODM\MongoDB\Aggregation\Aggregation` instance, comparable
to the `Query` class.

The `Doctrine\ODM\MongoDB\Aggregation\Builder::execute()` method was deprecated
and will be removed in ODM 3.0.
23 changes: 23 additions & 0 deletions UPGRADE-3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# UPGRADE FROM 2.x to 3.0

## Aggregation

The new `Doctrine\ODM\MongoDB\Aggregation\Builder::getAggregation()` method
returns an `Doctrine\ODM\MongoDB\Aggregation\Aggregation` instance, comparable
to the `Query` class.

The `Doctrine\ODM\MongoDB\Aggregation\Builder::execute()` method was removed.

## ID generators

The `Doctrine\ODM\MongoDB\Id\AbstractIdGenerator` class has been removed. Custom
ID generators must implement the `Doctrine\ODM\MongoDB\Id\IdGenerator`
interface.

## Metadata
The `Doctrine\ODM\MongoDB\Mapping\ClassMetadata` class has been marked final and
will no longer be extendable.

The `boolean`, `integer`, and `int_id` mapping types have been removed. Use the
`bool`, `int`, and `int` types, respectively. These types behave exactly the
same.
10 changes: 6 additions & 4 deletions docs/en/reference/aggregation-builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,16 @@ those values into an embedded object for the ``id`` field. For example:
Executing an aggregation pipeline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can execute a pipeline using the ``execute()`` method. This will run the
aggregation pipeline and return a cursor for you to iterate over the results:
When you are done building your pipeline, you can build an ``Aggregation``
object using the ``getAggregation()`` method. The returning instance can yield a
single result or return an iterator containing all results.

.. code-block:: php

<?php

$builder = $dm->createAggregationBuilder(\Documents\User::class);
$result = $builder->execute();
$result = $builder->getAggregation();

If you instead want to look at the built aggregation pipeline, call the
``Builder::getPipeline()`` method.
Expand Down Expand Up @@ -209,7 +210,8 @@ can tell the query builder to not return a caching iterator:
$builder->setRewindable(false);

When setting this option to ``false``, attempting a second iteration will result
in an exception.
in an exception. Note that calling ``getAggregation()`` will always yield a
fresh aggregation instance that can be re-executed.

Aggregation pipeline stages
---------------------------
Expand Down
68 changes: 68 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Aggregation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Aggregation;

use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
use Doctrine\ODM\MongoDB\Iterator\Iterator;
use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use IteratorAggregate;
use MongoDB\Collection;
use MongoDB\Driver\Cursor;
use function array_merge;
use function assert;

final class Aggregation implements IteratorAggregate
{
/** @var DocumentManager */
private $dm;

/** @var ClassMetadata|null */
private $classMetadata;

/** @var Collection */
private $collection;

/** @var array */
private $pipeline;

/** @var array */
private $options;

/** @var bool */
private $rewindable;

public function __construct(DocumentManager $dm, ?ClassMetadata $classMetadata, Collection $collection, array $pipeline, array $options = [], bool $rewindable = true)
{
$this->dm = $dm;
$this->classMetadata = $classMetadata;
$this->collection = $collection;
$this->pipeline = $pipeline;
$this->options = $options;
$this->rewindable = $rewindable;
}

public function getIterator() : Iterator
{
// Force cursor to be used
$options = array_merge($this->options, ['cursor' => true]);

$cursor = $this->collection->aggregate($this->pipeline, $options);
assert($cursor instanceof Cursor);

return $this->prepareIterator($cursor);
}

private function prepareIterator(Cursor $cursor) : Iterator
{
if ($this->classMetadata) {
$cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->classMetadata);
}

return $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);
}
}
47 changes: 19 additions & 28 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,24 @@
namespace Doctrine\ODM\MongoDB\Aggregation;

use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
use Doctrine\ODM\MongoDB\Iterator\Iterator;
use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Query\Expr as QueryExpr;
use GeoJson\Geometry\Point;
use MongoDB\Collection;
use MongoDB\Driver\Cursor;
use OutOfRangeException;
use TypeError;
use const E_USER_DEPRECATED;
use function array_map;
use function array_merge;
use function array_unshift;
use function assert;
use function func_get_arg;
use function func_num_args;
use function gettype;
use function is_array;
use function is_bool;
use function sprintf;
use function trigger_error;

/**
* Fluent interface for building aggregation pipelines.
Expand Down Expand Up @@ -168,16 +164,17 @@ public function count(string $fieldName) : Stage\Count

/**
* Executes the aggregation pipeline
*
* @deprecated This method was deprecated in doctrine/mongodb-odm 2.2. Please use getAggregation() instead.
*/
public function execute(array $options = []) : Iterator
{
// Force cursor to be used
$options = array_merge($options, ['cursor' => true]);

$cursor = $this->collection->aggregate($this->getPipeline(), $options);
assert($cursor instanceof Cursor);
@trigger_error(
sprintf('The "%s" method was deprecated in doctrine/mongodb-odm 2.2. Please use getAggregation() instead.', __METHOD__),
E_USER_DEPRECATED
);

return $this->prepareIterator($cursor);
return $this->getAggregation($options)->getIterator();
}

public function expr() : Expr
Expand Down Expand Up @@ -223,6 +220,16 @@ public function geoNear($x, $y = null) : Stage\GeoNear
return $stage;
}

/**
* Returns an aggregation object for the current pipeline
*/
public function getAggregation(array $options = []) : Aggregation
{
$class = $this->hydrationClass ? $this->dm->getClassMetadata($this->hydrationClass) : null;

return new Aggregation($this->dm, $class, $this->collection, $this->getPipeline(), $options, $this->rewindable);
}

// phpcs:disable Squiz.Commenting.FunctionComment.ExtraParamComment
/**
* Returns the assembled aggregation pipeline
Expand Down Expand Up @@ -584,20 +591,4 @@ private function getDocumentPersister() : DocumentPersister
{
return $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
}

private function prepareIterator(Cursor $cursor) : Iterator
{
$class = null;
if ($this->hydrationClass) {
$class = $this->dm->getClassMetadata($this->hydrationClass);
}

if ($class) {
$cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $class);
}

$cursor = $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);

return $cursor;
}
}
18 changes: 18 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use Doctrine\ODM\MongoDB\Iterator\Iterator;
use GeoJson\Geometry\Point;
use const E_USER_DEPRECATED;
use function sprintf;
use function trigger_error;

/**
* Fluent interface for building aggregation pipelines.
Expand All @@ -29,12 +32,27 @@ abstract public function getExpression() : array;

/**
* Executes the aggregation pipeline
*
* @deprecated This method was deprecated in doctrine/mongodb-odm 2.2. Please use getAggregation() instead.
*/
public function execute(array $options = []) : Iterator
{
@trigger_error(
sprintf('The "%s" method was deprecated in doctrine/mongodb-odm 2.2. Please use getAggregation() instead.', __METHOD__),
E_USER_DEPRECATED
);

return $this->builder->execute($options);
}

/**
* Returns an aggregation object for the current pipeline
*/
public function getAggregation(array $options = []) : Aggregation
{
return $this->builder->getAggregation($options);
}

/**
* Adds new fields to documents. $addFields outputs documents that contain
* all existing fields from the input documents and newly added fields.
Expand Down
32 changes: 32 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/Aggregation/BuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\ODM\MongoDB\Tests\Aggregation;

use DateTimeImmutable;
use Doctrine\ODM\MongoDB\Aggregation\Aggregation;
use Doctrine\ODM\MongoDB\Aggregation\Stage;
use Doctrine\ODM\MongoDB\Iterator\Iterator;
use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
Expand Down Expand Up @@ -197,6 +198,37 @@ public function testAggregationBuilder()
$this->assertSame(3, $results[0]->numPosts);
}

public function testGetAggregation()
{
$this->insertTestData();

$builder = $this->dm->createAggregationBuilder(BlogPost::class);

$aggregation = $builder
->hydrate(BlogTagAggregation::class)
->unwind('$tags')
->group()
->field('id')
->expression('$tags')
->field('numPosts')
->sum(1)
->sort('numPosts', 'desc')
->getAggregation();

$this->assertInstanceOf(Aggregation::class, $aggregation);

$resultCursor = $aggregation->getIterator();

$this->assertInstanceOf(Iterator::class, $resultCursor);

$results = $resultCursor->toArray();
$this->assertCount(2, $results);
$this->assertInstanceOf(BlogTagAggregation::class, $results[0]);

$this->assertSame('baseball', $results[0]->tag->name);
$this->assertSame(3, $results[0]->numPosts);
}

public function testPipelineConvertsTypes()
{
$builder = $this->dm->createAggregationBuilder(Article::class);
Expand Down