Skip to content

Commit a2859a6

Browse files
committed
PHPORM-155 Integrate aggregation builder to the Query builder
1 parent 12e78e5 commit a2859a6

File tree

5 files changed

+102
-22
lines changed

5 files changed

+102
-22
lines changed

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"issues": "https://www.mongodb.com/support",
1515
"security": "https://www.mongodb.com/security"
1616
},
17+
"repositories": [
18+
{ "type": "vcs", "url": "https://github.com/GromNaN/mongo-php-builder.git" }
19+
],
1720
"authors": [
1821
{ "name": "Andreas Braun", "email": "andreas.braun@mongodb.com", "role": "Leader" },
1922
{ "name": "Jérôme Tamarelle", "email": "jerome.tamarelle@mongodb.com", "role": "Maintainer" },
@@ -31,7 +34,7 @@
3134
"mongodb/mongodb": "^1.15"
3235
},
3336
"require-dev": {
34-
"mongodb/builder": "^0.1",
37+
"mongodb/builder": "dev-laravel-fluent",
3538
"phpunit/phpunit": "^10.3",
3639
"orchestra/testbench": "^8.0|^9.0",
3740
"mockery/mockery": "^1.4.4",

src/Eloquent/Builder.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ class Builder extends EloquentBuilder
5151
'tomql',
5252
];
5353

54+
public function aggregate($function = null, $columns = ['*'])
55+
{
56+
$result = $this->toBase()->aggregate($function, $columns);
57+
58+
return $result ?? $this;
59+
}
60+
5461
/** @inheritdoc */
5562
public function update(array $values, array $options = [])
5663
{

src/Query/Builder.php

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
use MongoDB\Builder\Stage\ReplaceRootStage;
3333
use MongoDB\Builder\Stage\SkipStage;
3434
use MongoDB\Builder\Stage\SortStage;
35+
use MongoDB\Builder\Stage\UnwindStage;
36+
use MongoDB\Builder\Type\StageInterface;
3537
use MongoDB\Builder\Variable;
3638
use MongoDB\Driver\Cursor;
3739
use Override;
@@ -291,14 +293,8 @@ public function dump(mixed ...$args)
291293
return $this;
292294
}
293295

294-
/**
295-
* Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
296-
*
297-
* Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
298-
*
299-
* @return array<string, mixed[]>
300-
*/
301-
public function toMql(): array
296+
/** @return StageInterface[] */
297+
protected function getPipeline(): array
302298
{
303299
$columns = $this->columns ?? [];
304300

@@ -373,33 +369,33 @@ public function toMql(): array
373369
// Build the aggregation pipeline.
374370
$pipeline = [];
375371
if ($wheres) {
376-
$pipeline[] = ['$match' => $wheres];
372+
$pipeline[] = new MatchStage(...$wheres);
377373
}
378374

379375
// apply unwinds for subdocument array aggregation
380376
foreach ($unwinds as $unwind) {
381-
$pipeline[] = ['$unwind' => '$' . $unwind];
377+
$pipeline[] = new UnwindStage($unwind);
382378
}
383379

384380
if ($group) {
385-
$pipeline[] = ['$group' => $group];
381+
$pipeline[] = new GroupStage(...$group);
386382
}
387383

388384
// Apply order and limit
389385
if ($this->orders) {
390-
$pipeline[] = ['$sort' => $this->orders];
386+
$pipeline[] = new SortStage($this->orders);
391387
}
392388

393389
if ($this->offset) {
394-
$pipeline[] = ['$skip' => $this->offset];
390+
$pipeline[] = new SkipStage($this->offset);
395391
}
396392

397393
if ($this->limit) {
398-
$pipeline[] = ['$limit' => $this->limit];
394+
$pipeline[] = new LimitStage($this->limit);
399395
}
400396

401397
if ($this->projections) {
402-
$pipeline[] = ['$project' => $this->projections];
398+
$pipeline[] = new ProjectStage(...$this->projections);
403399
}
404400

405401
$options = [
@@ -457,6 +453,22 @@ public function toMql(): array
457453
$pipeline[] = new ProjectStage(...$projection);
458454
}
459455

456+
return $pipeline;
457+
}
458+
459+
/**
460+
* Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
461+
*
462+
* Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
463+
*
464+
* @return array<string, mixed[]>
465+
*/
466+
public function toMql(): array
467+
{
468+
$pipeline = $this->getPipeline();
469+
$encoder = new BuilderEncoder();
470+
$pipeline = $encoder->encode(new Pipeline(...$pipeline));
471+
460472
$options = ['typeMap' => ['root' => 'array', 'document' => 'array']];
461473

462474
if ($this->timeout) {
@@ -468,12 +480,8 @@ public function toMql(): array
468480
}
469481

470482
$options = array_merge($options, $this->options);
471-
472483
$options = $this->inheritConnectionOptions($options);
473484

474-
$encoder = new BuilderEncoder();
475-
$pipeline = $encoder->encode(new Pipeline(...$pipeline));
476-
477485
return ['aggregate' => [$pipeline, $options]];
478486
}
479487

@@ -554,9 +562,16 @@ public function generateCacheKey()
554562
return md5(serialize(array_values($key)));
555563
}
556564

557-
/** @inheritdoc */
558-
public function aggregate($function, $columns = [])
565+
/**
566+
* @return self|PipelineBuilder
567+
* @psalm-return $function === null ? PipelineBuilder : self
568+
*/
569+
public function aggregate($function = null, $columns = [])
559570
{
571+
if ($function === null) {
572+
return new PipelineBuilder($this->getPipeline());
573+
}
574+
560575
$this->aggregate = [
561576
'function' => $function,
562577
'columns' => $columns,

src/Query/PipelineBuilder.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Query;
6+
7+
use MongoDB\Builder\Stage\FluentFactory;
8+
9+
class PipelineBuilder extends FluentFactory
10+
{
11+
public function __construct(array $pipeline = [])
12+
{
13+
$this->pipeline = $pipeline;
14+
}
15+
}

tests/Query/PipelineBuilderTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Tests\Query;
6+
7+
use MongoDB\BSON\Document;
8+
use MongoDB\Builder\BuilderEncoder;
9+
use MongoDB\Laravel\Query\PipelineBuilder;
10+
use MongoDB\Laravel\Tests\Models\User;
11+
use MongoDB\Laravel\Tests\TestCase;
12+
13+
use function assert;
14+
15+
class PipelineBuilderTest extends TestCase
16+
{
17+
public function testCreateFromQueryBuilder(): void
18+
{
19+
$builder = User::where('foo', 'bar')->aggregate();
20+
assert($builder instanceof PipelineBuilder);
21+
$builder->addFields(baz: 'qux');
22+
23+
$codec = new BuilderEncoder();
24+
$pipeline = $codec->encode($builder->getPipeline());
25+
$json = Document::fromPHP(['pipeline' => $pipeline])->toCanonicalExtendedJSON();
26+
27+
$expected = Document::fromPHP([
28+
'pipeline' => [
29+
[
30+
'$match' => ['foo' => 'bar'],
31+
],
32+
[
33+
'$addFields' => ['baz' => 'qux'],
34+
],
35+
],
36+
])->toCanonicalExtendedJSON();
37+
38+
$this->assertJsonStringEqualsJsonString($expected, $json);
39+
}
40+
}

0 commit comments

Comments
 (0)