Skip to content

Commit d3caa38

Browse files
committed
PHPLIB-1419 Encode Agg builder objects in Collection methods
Ignore static analysis issues PHPLIB-1419 Fix BC break
1 parent 5da5c15 commit d3caa38

5 files changed

+103
-2
lines changed

psalm-baseline.xml

+5
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@
223223
<MixedPropertyTypeCoercion>
224224
<code><![CDATA[$options['builderEncoder'] ?? new BuilderEncoder()]]></code>
225225
</MixedPropertyTypeCoercion>
226+
<PossiblyInvalidArgument>
227+
<code><![CDATA[$pipeline]]></code>
228+
<code><![CDATA[$pipeline]]></code>
229+
<code><![CDATA[$pipeline]]></code>
230+
</PossiblyInvalidArgument>
226231
</file>
227232
<file src="src/Command/ListCollections.php">
228233
<MixedAssignment>

src/Collection.php

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use MongoDB\BSON\JavascriptInterface;
2424
use MongoDB\BSON\PackedArray;
2525
use MongoDB\Builder\BuilderEncoder;
26+
use MongoDB\Builder\Pipeline;
2627
use MongoDB\Codec\DocumentCodec;
2728
use MongoDB\Codec\Encoder;
2829
use MongoDB\Driver\CursorInterface;
@@ -223,6 +224,12 @@ public function __toString()
223224
*/
224225
public function aggregate(array $pipeline, array $options = [])
225226
{
227+
if (is_builder_pipeline($pipeline)) {
228+
$pipeline = new Pipeline(...$pipeline);
229+
}
230+
231+
$pipeline = $this->builderEncoder->encodeIfSupported($pipeline);
232+
226233
$hasWriteStage = is_last_pipeline_operator_write($pipeline);
227234

228235
$options = $this->inheritReadPreference($options);
@@ -1098,6 +1105,11 @@ public function updateSearchIndex(string $name, array|object $definition, array
10981105
*/
10991106
public function watch(array $pipeline = [], array $options = [])
11001107
{
1108+
if (is_builder_pipeline($pipeline)) {
1109+
$pipeline = new Pipeline(...$pipeline);
1110+
}
1111+
1112+
$pipeline = $this->builderEncoder->encodeIfSupported($pipeline);
11011113
$options = $this->inheritReadOptions($options);
11021114
$options = $this->inheritCodecOrTypeMap($options);
11031115

src/functions.php

+33
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use MongoDB\BSON\Document;
2222
use MongoDB\BSON\PackedArray;
2323
use MongoDB\BSON\Serializable;
24+
use MongoDB\Builder\Type\StageInterface;
2425
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2526
use MongoDB\Driver\Manager;
2627
use MongoDB\Driver\ReadPreference;
@@ -327,6 +328,38 @@ function is_pipeline(array|object $pipeline, bool $allowEmpty = false): bool
327328
return true;
328329
}
329330

331+
/**
332+
* Returns whether the argument is a list that contains at least one
333+
* {@see StageInterface} object.
334+
*
335+
* @internal
336+
*/
337+
function is_builder_pipeline(array $pipeline): bool
338+
{
339+
if (! $pipeline) {
340+
return false;
341+
}
342+
343+
if (! array_is_list($pipeline)) {
344+
return false;
345+
}
346+
347+
$result = false;
348+
foreach ($pipeline as $stage) {
349+
if (! is_array($stage) && ! is_object($stage)) {
350+
return false;
351+
}
352+
353+
if ($stage instanceof StageInterface) {
354+
$result = true;
355+
} elseif (! is_first_key_operator($stage)) {
356+
return false;
357+
}
358+
}
359+
360+
return $result;
361+
}
362+
330363
/**
331364
* Returns whether we are currently in a transaction.
332365
*

tests/Collection/BuilderCollectionFunctionalTest.php

+34-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace MongoDB\Tests\Collection;
44

5+
use MongoDB\Builder\Expression;
56
use MongoDB\Builder\Pipeline;
67
use MongoDB\Builder\Query;
78
use MongoDB\Builder\Stage;
89

10+
use function iterator_to_array;
11+
912
class BuilderCollectionFunctionalTest extends FunctionalTestCase
1013
{
1114
public function setUp(): void
@@ -17,7 +20,18 @@ public function setUp(): void
1720

1821
public function testAggregate(): void
1922
{
20-
$this->markTestSkipped('Not supported yet');
23+
$this->collection->insertMany([['x' => 10], ['x' => 10], ['x' => 10]]);
24+
$pipeline = new Pipeline(
25+
Stage::bucketAuto(
26+
groupBy: Expression::intFieldPath('x'),
27+
buckets: 2,
28+
),
29+
);
30+
// Extract the list of stages for arg type restriction
31+
$pipeline = iterator_to_array($pipeline);
32+
33+
$results = $this->collection->aggregate($pipeline)->toArray();
34+
$this->assertCount(2, $results);
2135
}
2236

2337
public function testBulkWriteDeleteMany(): void
@@ -245,6 +259,24 @@ public function testUpdateManyWithPipeline(): void
245259

246260
public function testWatch(): void
247261
{
248-
$this->markTestSkipped('Not supported yet');
262+
$this->skipIfChangeStreamIsNotSupported();
263+
264+
if ($this->isShardedCluster()) {
265+
$this->markTestSkipped('Test does not apply on sharded clusters: need more than a single getMore call on the change stream.');
266+
}
267+
268+
$pipeline = new Pipeline(
269+
Stage::match(operationType: Query::eq('insert')),
270+
);
271+
// Extract the list of stages for arg type restriction
272+
$pipeline = iterator_to_array($pipeline);
273+
274+
$changeStream = $this->collection->watch($pipeline);
275+
$changeStream->rewind();
276+
$this->assertNull($changeStream->current());
277+
$this->collection->insertOne(['x' => 3]);
278+
$changeStream->next();
279+
$this->assertTrue($changeStream->valid());
280+
$this->assertEquals('insert', $changeStream->current()->operationType);
249281
}
250282
}

tests/FunctionsTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use MongoDB\BSON\Document;
66
use MongoDB\BSON\PackedArray;
7+
use MongoDB\Builder\Stage\LimitStage;
8+
use MongoDB\Builder\Stage\MatchStage;
79
use MongoDB\Driver\WriteConcern;
810
use MongoDB\Model\BSONArray;
911
use MongoDB\Model\BSONDocument;
@@ -12,6 +14,7 @@
1214
use function MongoDB\apply_type_map_to_document;
1315
use function MongoDB\create_field_path_type_map;
1416
use function MongoDB\document_to_array;
17+
use function MongoDB\is_builder_pipeline;
1518
use function MongoDB\is_first_key_operator;
1619
use function MongoDB\is_last_pipeline_operator_write;
1720
use function MongoDB\is_mapreduce_output_inline;
@@ -311,6 +314,22 @@ public function providePipelines(): array
311314
];
312315
}
313316

317+
/** @dataProvider provideStagePipelines */
318+
public function testIsBuilderPipeline($expected, $pipeline): void
319+
{
320+
$this->assertSame($expected, is_builder_pipeline($pipeline));
321+
}
322+
323+
public function provideStagePipelines(): iterable
324+
{
325+
yield 'empty array' => [false, []];
326+
yield 'array of arrays' => [false, [['$match' => ['x' => 1]]]];
327+
yield 'map of stages' => [false, [1 => new MatchStage([])]];
328+
yield 'stages' => [true, [new MatchStage([]), new LimitStage(1)]];
329+
yield 'stages and operators' => [true, [new MatchStage([]), ['$limit' => 1]]];
330+
yield 'stages and invalid' => [false, [new MatchStage([]), ['foo' => 'bar']]];
331+
}
332+
314333
/** @dataProvider provideWriteConcerns */
315334
public function testIsWriteConcernAcknowledged($expected, WriteConcern $writeConcern): void
316335
{

0 commit comments

Comments
 (0)