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

PHPORM-155 Fluent aggregation builder #2738

Merged
merged 24 commits into from
Mar 26, 2024
Merged

PHPORM-155 Fluent aggregation builder #2738

merged 24 commits into from
Mar 26, 2024

Conversation

GromNaN
Copy link
Member

@GromNaN GromNaN commented Feb 26, 2024

Fix PHPORM-155

With this integration, it's possible to derivate any Query builder into an aggregation pipeline. The conditions built with the query builder are used as first stages (match, project, limit ...)

Requires mongodb/mongo-php-builder#67

Checklist

  • Add tests and ensure they pass
  • Add an entry to the CHANGELOG.md file
  • Update documentation for new features

@GromNaN GromNaN self-assigned this Feb 26, 2024
@GromNaN GromNaN force-pushed the PHPORM-155 branch 2 times, most recently from c1450e2 to a2859a6 Compare February 26, 2024 17:14
{
$result = $this->toBase()->aggregate($function, $columns);

return $result ?? $this;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method must be overrided, because by default this is a fluent interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does @return need to be @psalm-return?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use phpstan on this project, and it get promoted for Laravel projects.

I have updated the annotation to @return ($function is null ? AggregationBuilder : self), which is the documented way.

@GromNaN GromNaN changed the title PHPORM-155 PHPORM-155 Fluent aggregation builder Feb 27, 2024
@GromNaN
Copy link
Member Author

GromNaN commented Mar 8, 2024

  1. $near, and $nearSphere query operators are not supported in $match stage. They must be replaced by a $geoNear. This is something we can't do automatically, especially when nested in a $and or $or query.
  2. $slice projection operator has a different signature than the $slice expression (array field as 1st argument). This needs to be translated in the context of a $project stage.
  3. $where is not available within aggregation pipeline. They can be replaced by the $function expression.

List of limitations: https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#restrictions

Since this operators are currently supported by the query builder, we need to keep building a find command with the query builder, and only create an aggregation pipeline when necessary.

@GromNaN GromNaN force-pushed the PHPORM-155 branch 4 times, most recently from 438ccd7 to 0e82e24 Compare March 11, 2024 14:33
@GromNaN GromNaN marked this pull request as ready for review March 11, 2024 16:43
@GromNaN GromNaN requested a review from a team as a code owner March 11, 2024 16:43
@GromNaN GromNaN requested a review from jmikola March 11, 2024 16:43
composer.json Outdated Show resolved Hide resolved
@@ -49,6 +49,7 @@
use function uniqid;
use function var_export;

/** @mixin Builder */
Copy link
Member Author

@GromNaN GromNaN Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows completions such as Model::where()->aggregate()->addField(

@GromNaN GromNaN force-pushed the PHPORM-155 branch 2 times, most recently from c46344f to 3328e93 Compare March 14, 2024 10:00
@GromNaN GromNaN changed the base branch from 4.2 to 4.3 March 14, 2024 10:38
{
$result = $this->toBase()->aggregate($function, $columns);

return $result ?? $this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does @return need to be @psalm-return?

src/Query/Builder.php Show resolved Hide resolved
src/Query/Builder.php Show resolved Hide resolved
src/Query/Builder.php Outdated Show resolved Hide resolved
src/Query/Builder.php Outdated Show resolved Hide resolved
src/Query/Builder.php Outdated Show resolved Hide resolved
src/Query/Builder.php Outdated Show resolved Hide resolved
src/Query/Builder.php Outdated Show resolved Hide resolved
@GromNaN
Copy link
Member Author

GromNaN commented Mar 19, 2024

After review with @jmikola we decided to remove the feature of initializing the aggregation pipeline from the query builder with $match, $limit, $sort and $project stages. Library users are invited to use the Aggregation Builder from the start.

The AggregationBuilder class is enhancer with execution methods similar to Query\Builder and Eloquent\Builder:

  • AggregationBuilder::get($options) returns the results in a Laravel\Support\Collection
  • AggregationBuilder::cursor($options) returns the results in a Laravel\Support\LazyCollection which is a lazy-loading feature.
  • AggregationBuilder::first($options) returns the first result, after appending the stage $limit: 1 to the pipeline.

/**
* Execute the aggregation pipeline and return the first result.
*/
public function first(array $options = []): mixed
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was concerned of potential conflict with a stage name, but $first is an expression operator 😅

Copy link
Member

@alcaeus alcaeus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM in general, just a couple of minor suggestions.

composer.json Show resolved Hide resolved
src/Query/Builder.php Show resolved Hide resolved
throw new BadMethodCallException('Aggregation builder requires package mongodb/builder 0.2+');
}

if ($columns !== [] && $columns !== ['*']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to allow [] for $columns? IIRC, the previous signature changed the default value to an empty array, but you're preserving ['*'] now. Do you expect users to explicitly call aggregate(null, [])? If not, I'd stick to just ['*'] here.

use FluentFactoryTrait;

public function __construct(
private MongoDBCollection|LaravelMongoDBCollection $collection,
Copy link
Member

@jmikola jmikola Mar 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to allow MongoDB\Collection here? It's only constructed as such in the test suite, but I presume you could just mock MongoDB\Laravel\Collection instead there.

I do realize MongoDB\Laravel\Collection just forwards method calls to the PHPLIB class, so there's no technical reason that a MongoDB\Collection wouldn't work as well.

OK to leave as-is. I just wanted to bring this up since it makes the API more permissive and I don't expect users would be constructing this on their own anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect MongoDB\Laravel\Collection to be deprecated soon, as it's used only for logging commands. We need to switch to driver monitoring features: PHPORM-56. This union type is forward-compatible.

src/Query/AggregationBuilder.php Show resolved Hide resolved
{
return (clone $this)
->limit(1)
->cursor($options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're going to apply limit(1) I reckon it'd be more performant to just use get() here.

/**
* Execute the aggregation pipeline and return MongoDB cursor.
*/
private function execute(array $options): CursorInterface&Iterator
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this is internal, I suppose you can't use MongoDB\Driver\Cursor here since a CodecCursor might be returned?

Unrelated to this PR, but this made me wonder if we need to keep CursorInterface&Iterator in place in PHPLIB 2.0, or if there were plans to integrate the Iterator API into CursorInterface. I asked @alcaeus about this in PHPLIB-1114.

Nothing for you here. I merely wanted to cross-reference.

tests/Query/AggregationBuilderTest.php Show resolved Hide resolved
Copy link
Member

@jmikola jmikola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow up on both open questions, but feel free to merge after doing so. Code changes LGTM.

tests/Query/AggregationBuilderTest.php Show resolved Hide resolved
@GromNaN GromNaN merged commit fdfb5e5 into mongodb:4.3 Mar 26, 2024
46 checks passed
@GromNaN GromNaN deleted the PHPORM-155 branch March 26, 2024 16:58
@GromNaN GromNaN added this to the 4.3 milestone Apr 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants