Skip to content
This repository was archived by the owner on Feb 28, 2025. It is now read-only.

Allow to merge an array of stages into a pipeline #57

Merged
merged 3 commits into from
Mar 15, 2024
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
78 changes: 42 additions & 36 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.21.1@8c473e2437be8b6a8fd8f630f0f11a16b114c494">
<files psalm-version="5.22.2@d768d914152dbbf3486c36398802f74e80cfde48">
<file src="src/Builder/Encoder/AbstractExpressionEncoder.php">
<MixedAssignment>
<code>$val</code>
<code>$val</code>
<code>$value[$key]</code>
<code><![CDATA[$val]]></code>
<code><![CDATA[$val]]></code>
<code><![CDATA[$value[$key]]]></code>
</MixedAssignment>
</file>
<file src="src/Builder/Encoder/CombinedFieldQueryEncoder.php">
<MixedAssignment>
<code>$filter</code>
<code>$filterValue</code>
<code><![CDATA[$filter]]></code>
<code><![CDATA[$filterValue]]></code>
</MixedAssignment>
</file>
<file src="src/Builder/Encoder/FieldPathEncoder.php">
Expand All @@ -20,105 +20,111 @@
</file>
<file src="src/Builder/Encoder/OperatorEncoder.php">
<MixedAssignment>
<code>$result</code>
<code>$result[]</code>
<code>$val</code>
<code>$val</code>
<code>$val</code>
<code>$val</code>
<code>$val</code>
<code><![CDATA[$result]]></code>
<code><![CDATA[$result[]]]></code>
<code><![CDATA[$val]]></code>
<code><![CDATA[$val]]></code>
<code><![CDATA[$val]]></code>
<code><![CDATA[$val]]></code>
<code><![CDATA[$val]]></code>
</MixedAssignment>
</file>
<file src="src/Builder/Encoder/OutputWindowEncoder.php">
<MixedArgument>
<code>$result</code>
<code><![CDATA[$result]]></code>
</MixedArgument>
</file>
<file src="src/Builder/Encoder/PipelineEncoder.php">
<MixedAssignment>
<code><![CDATA[$encoded[]]]></code>
<code><![CDATA[$stage]]></code>
</MixedAssignment>
</file>
<file src="src/Builder/Encoder/QueryEncoder.php">
<MixedArgument>
<code><![CDATA[$this->recursiveEncode($value)]]></code>
</MixedArgument>
<MixedAssignment>
<code>$subValue</code>
<code>$value</code>
<code><![CDATA[$subValue]]></code>
<code><![CDATA[$value]]></code>
</MixedAssignment>
</file>
<file src="src/Builder/Projection/ElemMatchOperator.php">
<MixedArgumentTypeCoercion>
<code>$query</code>
<code><![CDATA[$query]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/Builder/Query.php">
<ArgumentTypeCoercion>
<code>$query</code>
<code><![CDATA[$query]]></code>
</ArgumentTypeCoercion>
</file>
<file src="src/Builder/Query/ElemMatchOperator.php">
<MixedArgumentTypeCoercion>
<code>$query</code>
<code><![CDATA[$query]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/Builder/Stage/AddFieldsStage.php">
<PropertyTypeCoercion>
<code>$expression</code>
<code><![CDATA[$expression]]></code>
</PropertyTypeCoercion>
<TooManyTemplateParams>
<code>stdClass</code>
<code><![CDATA[stdClass]]></code>
</TooManyTemplateParams>
</file>
<file src="src/Builder/Stage/FacetStage.php">
<PropertyTypeCoercion>
<code>$facet</code>
<code><![CDATA[$facet]]></code>
</PropertyTypeCoercion>
<TooManyTemplateParams>
<code>stdClass</code>
<code><![CDATA[stdClass]]></code>
</TooManyTemplateParams>
</file>
<file src="src/Builder/Stage/GeoNearStage.php">
<MixedArgumentTypeCoercion>
<code>$query</code>
<code><![CDATA[$query]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/Builder/Stage/GraphLookupStage.php">
<MixedArgumentTypeCoercion>
<code>$restrictSearchWithMatch</code>
<code><![CDATA[$restrictSearchWithMatch]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/Builder/Stage/GroupStage.php">
<PropertyTypeCoercion>
<code>$field</code>
<code><![CDATA[$field]]></code>
</PropertyTypeCoercion>
<TooManyTemplateParams>
<code>stdClass</code>
<code><![CDATA[stdClass]]></code>
</TooManyTemplateParams>
</file>
<file src="src/Builder/Stage/MatchStage.php">
<MixedArgumentTypeCoercion>
<code>$query</code>
<code><![CDATA[$query]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/Builder/Stage/ProjectStage.php">
<PropertyTypeCoercion>
<code>$specification</code>
<code><![CDATA[$specification]]></code>
</PropertyTypeCoercion>
<TooManyTemplateParams>
<code>stdClass</code>
<code><![CDATA[stdClass]]></code>
</TooManyTemplateParams>
</file>
<file src="src/Builder/Stage/SetStage.php">
<PropertyTypeCoercion>
<code>$field</code>
<code><![CDATA[$field]]></code>
</PropertyTypeCoercion>
<TooManyTemplateParams>
<code>stdClass</code>
<code><![CDATA[stdClass]]></code>
</TooManyTemplateParams>
</file>
<file src="src/Builder/Stage/SortStage.php">
<PropertyTypeCoercion>
<code>$sort</code>
<code><![CDATA[$sort]]></code>
</PropertyTypeCoercion>
<TooManyTemplateParams>
<code>stdClass</code>
<code><![CDATA[stdClass]]></code>
</TooManyTemplateParams>
</file>
<file src="src/Builder/Type/OutputWindow.php">
Expand All @@ -129,8 +135,8 @@
</file>
<file src="src/Builder/Type/QueryObject.php">
<MixedAssignment>
<code>$queries[$fieldPath]</code>
<code>$query</code>
<code><![CDATA[$queries[$fieldPath]]]></code>
<code><![CDATA[$query]]></code>
</MixedAssignment>
<RedundantConditionGivenDocblockType>
<code><![CDATA[count($queriesOrArrayOfQueries) === 1 &&
Expand Down
24 changes: 15 additions & 9 deletions src/Builder/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,31 @@
use IteratorAggregate;
use MongoDB\Builder\Type\StageInterface;
use MongoDB\Exception\InvalidArgumentException;
use Traversable;
use stdClass;

use function array_is_list;
use function array_merge;
use function is_array;

/**
* An aggregation pipeline consists of one or more stages that process documents.
*
* @see https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
*
* @psalm-immutable
* @implements IteratorAggregate<StageInterface>
* @psalm-type stage = StageInterface|array<string,mixed>|stdClass
* @implements IteratorAggregate<stage>
*/
class Pipeline implements IteratorAggregate
final class Pipeline implements IteratorAggregate
{
/** @var StageInterface[] */
private readonly array $stages;

/** @no-named-arguments */
public function __construct(StageInterface|Pipeline ...$stagesOrPipelines)
/**
* @psalm-param stage|list<stage> ...$stagesOrPipelines
*
* @no-named-arguments
*/
public function __construct(StageInterface|Pipeline|array|stdClass ...$stagesOrPipelines)
{
if (! array_is_list($stagesOrPipelines)) {
throw new InvalidArgumentException('Named arguments are not supported for pipelines');
Expand All @@ -36,7 +41,9 @@ public function __construct(StageInterface|Pipeline ...$stagesOrPipelines)
$stages = [];

foreach ($stagesOrPipelines as $stageOrPipeline) {
if ($stageOrPipeline instanceof Pipeline) {
if (is_array($stageOrPipeline) && array_is_list($stageOrPipeline)) {
Copy link
Member

Choose a reason for hiding this comment

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

I noticed you don't check whether each item in the list is a StageInterface instance. In theory, this should be fine as PipelineEncoder uses encodeIfSupported to encode each stage. Anything the encoder can't handle will be sent to the server as is, potentially causing an execution error later. This would allow users to inject raw pipeline stages (e.g. if they want to use something not supported by their builder version) as long as they know what they are doing.

$stages = array_merge($stages, $stageOrPipeline);
} elseif ($stageOrPipeline instanceof Pipeline) {
$stages = array_merge($stages, $stageOrPipeline->stages);
} else {
$stages[] = $stageOrPipeline;
Expand All @@ -46,8 +53,7 @@ public function __construct(StageInterface|Pipeline ...$stagesOrPipelines)
$this->stages = $stages;
}

/** @return Traversable<StageInterface> */
public function getIterator(): Traversable
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->stages);
}
Expand Down
30 changes: 26 additions & 4 deletions tests/Builder/PipelineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,46 @@ class PipelineTest extends TestCase
{
public function testEmptyPipeline(): void
{
$pipeline = new Pipeline();
$pipeline = new Pipeline([]);

$this->assertSame([], iterator_to_array($pipeline));
}

public function testFromArray(): void
{
$pipeline = new Pipeline(
['$match' => ['tag' => 'foo']],
[
['$sort' => ['_id' => 1]],
['$skip' => 10],
],
['$limit' => 5],
);

$expected = [
['$match' => ['tag' => 'foo']],
['$sort' => ['_id' => 1]],
['$skip' => 10],
['$limit' => 5],
];

$this->assertSame($expected, iterator_to_array($pipeline));
}

public function testMergingPipeline(): void
{
$stages = array_map(
fn (int $i) => $this->createMock(StageInterface::class),
range(0, 5),
range(0, 7),
);

$pipeline = new Pipeline(
$stages[0],
$stages[1],
new Pipeline($stages[2], $stages[3]),
$stages[4],
new Pipeline($stages[5]),
[$stages[4], $stages[5]],
new Pipeline($stages[6]),
[$stages[7]],
);

$this->assertSame($stages, iterator_to_array($pipeline));
Expand Down