Skip to content

Commit 4cfdcd9

Browse files
committed
PHPLIB-418: Add the ability to specify a pipeline to an update command
1 parent a84ab8a commit 4cfdcd9

12 files changed

+58
-26
lines changed

docs/includes/apiargs-MongoDBCollection-common-param.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ type: array|object
2424
description: |
2525
Specifies the field and value combinations to update and any relevant update
2626
operators. ``$update`` uses MongoDB's :method:`update operators
27-
</reference/operator/update>`.
27+
</reference/operator/update>`. Starting with MongoDB 4.2, an aggregation
28+
pipeline can be passed as update specification.
2829
interface: phpmethod
2930
operation: ~
3031
optional: false

src/Operation/FindAndModify.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,18 @@ private function createCommandDocument(Server $server)
248248
$cmd['upsert'] = $this->options['upsert'];
249249
}
250250

251-
foreach (['collation', 'fields', 'query', 'sort', 'update'] as $option) {
251+
foreach (['collation', 'fields', 'query', 'sort'] as $option) {
252252
if (isset($this->options[$option])) {
253253
$cmd[$option] = (object) $this->options[$option];
254254
}
255255
}
256256

257+
if (isset($this->options['update'])) {
258+
$cmd['update'] = \MongoDB\is_pipeline($this->options['update'])
259+
? $this->options['update']
260+
: (object) $this->options['update'];
261+
}
262+
257263
if (isset($this->options['arrayFilters'])) {
258264
$cmd['arrayFilters'] = $this->options['arrayFilters'];
259265
}

src/Operation/FindOneAndUpdate.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ public function __construct($databaseName, $collectionName, $filter, $update, ar
101101
throw InvalidArgumentException::invalidType('$update', $update, 'array or object');
102102
}
103103

104-
if ( ! \MongoDB\is_first_key_operator($update)) {
105-
throw new InvalidArgumentException('First key in $update argument is not an update operator');
104+
if ( ! \MongoDB\is_first_key_operator($update) && ! \MongoDB\is_pipeline($update)) {
105+
throw new InvalidArgumentException('Expected an update document with operator as first key or a pipeline');
106106
}
107107

108108
$options += [

src/Operation/ReplaceOne.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public function __construct($databaseName, $collectionName, $filter, $replacemen
7676
throw new InvalidArgumentException('First key in $replacement argument is an update operator');
7777
}
7878

79+
if (\MongoDB\is_pipeline($replacement)) {
80+
throw new InvalidArgumentException('$replacement argument is a pipeline');
81+
}
82+
7983
$this->update = new Update(
8084
$databaseName,
8185
$collectionName,

src/Operation/Update.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public function __construct($databaseName, $collectionName, $filter, $update, ar
121121
throw InvalidArgumentException::invalidType('"multi" option', $options['multi'], 'boolean');
122122
}
123123

124-
if ($options['multi'] && ! \MongoDB\is_first_key_operator($update)) {
124+
if ($options['multi'] && ! \MongoDB\is_first_key_operator($update) && ! \MongoDB\is_pipeline($update)) {
125125
throw new InvalidArgumentException('"multi" option cannot be true if $update is a replacement document');
126126
}
127127

src/Operation/UpdateMany.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ public function __construct($databaseName, $collectionName, $filter, $update, ar
7878
throw InvalidArgumentException::invalidType('$update', $update, 'array or object');
7979
}
8080

81-
if ( ! \MongoDB\is_first_key_operator($update)) {
82-
throw new InvalidArgumentException('First key in $update argument is not an update operator');
81+
if ( ! \MongoDB\is_first_key_operator($update) && ! \MongoDB\is_pipeline($update)) {
82+
throw new InvalidArgumentException('Expected an update document with operator as first key or a pipeline');
8383
}
8484

8585
$this->update = new Update(

src/Operation/UpdateOne.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ public function __construct($databaseName, $collectionName, $filter, $update, ar
7878
throw InvalidArgumentException::invalidType('$update', $update, 'array or object');
7979
}
8080

81-
if ( ! \MongoDB\is_first_key_operator($update)) {
82-
throw new InvalidArgumentException('First key in $update argument is not an update operator');
81+
if ( ! \MongoDB\is_first_key_operator($update) && ! \MongoDB\is_pipeline($update)) {
82+
throw new InvalidArgumentException('Expected an update document with operator as first key or a pipeline');
8383
}
8484

8585
$this->update = new Update(

src/functions.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,40 @@ function is_first_key_operator($document)
110110
return (isset($firstKey[0]) && $firstKey[0] === '$');
111111
}
112112

113+
/**
114+
* Returns whether an update specification is a valid aggregation pipeline.
115+
*
116+
* @internal
117+
* @param mixed $pipeline
118+
* @return boolean
119+
*/
120+
function is_pipeline($pipeline)
121+
{
122+
if (! is_array($pipeline)) {
123+
return false;
124+
}
125+
126+
if ($pipeline === []) {
127+
return false;
128+
}
129+
130+
foreach ($pipeline as $stage) {
131+
if (! is_array($stage) && ! is_object($stage)) {
132+
return false;
133+
}
134+
135+
$stage = (array) $stage;
136+
reset($stage);
137+
$key = key($stage);
138+
139+
if (! isset($key[0]) || $key[0] !== '$') {
140+
return false;
141+
}
142+
}
143+
144+
return true;
145+
}
146+
113147
/**
114148
* Returns whether we are currently in a transaction.
115149
*

tests/Operation/FindOneAndUpdateTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ public function testConstructorUpdateArgumentTypeCheck($update)
2525
new FindOneAndUpdate($this->getDatabaseName(), $this->getCollectionName(), [], $update);
2626
}
2727

28-
public function testConstructorUpdateArgumentRequiresOperators()
28+
public function testConstructorUpdateArgumentRequiresOperatorsOrPipeline()
2929
{
3030
$this->expectException(InvalidArgumentException::class);
31-
$this->expectExceptionMessage('First key in $update argument is not an update operator');
31+
$this->expectExceptionMessage('Expected an update document with operator as first key or a pipeline');
3232
new FindOneAndUpdate($this->getDatabaseName(), $this->getCollectionName(), [], []);
3333
}
3434

tests/Operation/UpdateManyTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function testConstructorUpdateArgument($update)
4141
public function testConstructorUpdateArgumentRequiresOperators($replacement)
4242
{
4343
$this->expectException(InvalidArgumentException::class);
44-
$this->expectExceptionMessage('First key in $update argument is not an update operator');
44+
$this->expectExceptionMessage('Expected an update document with operator as first key or a pipeline');
4545
new UpdateMany($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], $replacement);
4646
}
4747

tests/Operation/UpdateOneTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function testConstructorUpdateArgument($update)
4141
public function testConstructorUpdateArgumentRequiresOperators($replacement)
4242
{
4343
$this->expectException(InvalidArgumentException::class);
44-
$this->expectExceptionMessage('First key in $update argument is not an update operator');
44+
$this->expectExceptionMessage('Expected an update document with operator as first key or a pipeline');
4545
new UpdateOne($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1], $replacement);
4646
}
4747

tests/SpecTests/CrudSpecTest.php

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@
1111
*/
1212
class CrudSpecTest extends FunctionalTestCase
1313
{
14-
/* These should all pass before the driver can be considered compatible with
15-
* MongoDB 4.2. */
16-
private static $incompleteTests = [
17-
'bulkWrite-arrayFilters: BulkWrite with arrayFilters' => 'Fails due to command assertions',
18-
'updateWithPipelines: UpdateOne using pipelines' => 'PHPLIB-418',
19-
'updateWithPipelines: UpdateMany using pipelines' => 'PHPLIB-418',
20-
'updateWithPipelines: FindOneAndUpdate using pipelines' => 'PHPLIB-418',
21-
];
22-
2314
/**
2415
* Assert that the expected and actual command documents match.
2516
*
@@ -45,10 +36,6 @@ public static function assertCommandMatches(stdClass $expected, stdClass $actual
4536
*/
4637
public function testCrud(stdClass $test, array $runOn = null, array $data, $databaseName = null, $collectionName = null)
4738
{
48-
if (isset(self::$incompleteTests[$this->dataDescription()])) {
49-
$this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]);
50-
}
51-
5239
if (isset($runOn)) {
5340
$this->checkServerRequirements($runOn);
5441
}

0 commit comments

Comments
 (0)