diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 00c11f728..f51206d0d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -417,8 +417,9 @@ - - + + + @@ -448,6 +449,7 @@ + @@ -458,6 +460,10 @@ + + + + @@ -466,9 +472,14 @@ + + + + + diff --git a/src/Collection.php b/src/Collection.php index 28133ff55..7769b9a5b 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -262,6 +262,7 @@ public function aggregate(array $pipeline, array $options = []) */ public function bulkWrite(array $operations, array $options = []) { + $options = $this->inheritBuilderEncoder($options); $options = $this->inheritWriteOptions($options); $options = $this->inheritCodec($options); @@ -286,6 +287,7 @@ public function bulkWrite(array $operations, array $options = []) */ public function count(array|object $filter = [], array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritReadOptions($options); $operation = new Count($this->databaseName, $this->collectionName, $filter, $options); @@ -307,6 +309,7 @@ public function count(array|object $filter = [], array $options = []) */ public function countDocuments(array|object $filter = [], array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritReadOptions($options); $operation = new CountDocuments($this->databaseName, $this->collectionName, $filter, $options); @@ -444,6 +447,7 @@ public function createSearchIndexes(array $indexes, array $options = []): array */ public function deleteMany(array|object $filter, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritWriteOptions($options); $operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options); @@ -465,6 +469,7 @@ public function deleteMany(array|object $filter, array $options = []) */ public function deleteOne(array|object $filter, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritWriteOptions($options); $operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options); @@ -487,6 +492,7 @@ public function deleteOne(array|object $filter, array $options = []) */ public function distinct(string $fieldName, array|object $filter = [], array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritReadOptions($options); $options = $this->inheritTypeMap($options); @@ -645,6 +651,7 @@ public function explain(Explainable $explainable, array $options = []) */ public function find(array|object $filter = [], array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritReadOptions($options); $options = $this->inheritCodecOrTypeMap($options); @@ -667,6 +674,7 @@ public function find(array|object $filter = [], array $options = []) */ public function findOne(array|object $filter = [], array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritReadOptions($options); $options = $this->inheritCodecOrTypeMap($options); @@ -692,6 +700,7 @@ public function findOne(array|object $filter = [], array $options = []) */ public function findOneAndDelete(array|object $filter, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritWriteOptions($options); $options = $this->inheritCodecOrTypeMap($options); @@ -722,6 +731,7 @@ public function findOneAndDelete(array|object $filter, array $options = []) */ public function findOneAndReplace(array|object $filter, array|object $replacement, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritWriteOptions($options); $options = $this->inheritCodecOrTypeMap($options); @@ -752,6 +762,7 @@ public function findOneAndReplace(array|object $filter, array|object $replacemen */ public function findOneAndUpdate(array|object $filter, array|object $update, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritWriteOptions($options); $options = $this->inheritCodecOrTypeMap($options); @@ -1000,6 +1011,7 @@ public function rename(string $toCollectionName, ?string $toDatabaseName = null, */ public function replaceOne(array|object $filter, array|object $replacement, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); $options = $this->inheritWriteOptions($options); $options = $this->inheritCodec($options); @@ -1023,6 +1035,8 @@ public function replaceOne(array|object $filter, array|object $replacement, arra */ public function updateMany(array|object $filter, array|object $update, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); + $update = $this->builderEncoder->encodeIfSupported($update); $options = $this->inheritWriteOptions($options); $operation = new UpdateMany($this->databaseName, $this->collectionName, $filter, $update, $options); @@ -1045,6 +1059,8 @@ public function updateMany(array|object $filter, array|object $update, array $op */ public function updateOne(array|object $filter, array|object $update, array $options = []) { + $filter = $this->builderEncoder->encodeIfSupported($filter); + $update = $this->builderEncoder->encodeIfSupported($update); $options = $this->inheritWriteOptions($options); $operation = new UpdateOne($this->databaseName, $this->collectionName, $filter, $update, $options); @@ -1112,6 +1128,11 @@ public function withOptions(array $options = []) return new Collection($this->manager, $this->databaseName, $this->collectionName, $options); } + private function inheritBuilderEncoder(array $options): array + { + return ['builderEncoder' => $this->builderEncoder] + $options; + } + private function inheritCodec(array $options): array { // If the options contain a type map, don't inherit anything diff --git a/src/Operation/BulkWrite.php b/src/Operation/BulkWrite.php index 700ce3943..6da8a68b3 100644 --- a/src/Operation/BulkWrite.php +++ b/src/Operation/BulkWrite.php @@ -17,8 +17,10 @@ namespace MongoDB\Operation; +use MongoDB\Builder\BuilderEncoder; use MongoDB\BulkWriteResult; use MongoDB\Codec\DocumentCodec; +use MongoDB\Codec\Encoder; use MongoDB\Driver\BulkWrite as Bulk; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Server; @@ -94,6 +96,9 @@ class BulkWrite implements Executable * * Supported options for the bulk write operation: * + * * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and + * aggregation builders. If not given, the default encoder will be used. + * * * bypassDocumentValidation (boolean): If true, allows the write to * circumvent document level validation. The default is false. * @@ -137,6 +142,10 @@ public function __construct(private string $databaseName, private string $collec $options += ['ordered' => true]; + if (isset($options['builderEncoder']) && ! $options['builderEncoder'] instanceof Encoder) { + throw InvalidArgumentException::invalidType('"builderEncoder" option', $options['builderEncoder'], Encoder::class); + } + if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean'); } @@ -169,7 +178,7 @@ public function __construct(private string $databaseName, private string $collec unset($options['writeConcern']); } - $this->operations = $this->validateOperations($operations, $options['codec'] ?? null); + $this->operations = $this->validateOperations($operations, $options['codec'] ?? null, $options['builderEncoder'] ?? new BuilderEncoder()); $this->options = $options; } @@ -264,7 +273,7 @@ private function createExecuteOptions(): array * @param array[] $operations * @return array[] */ - private function validateOperations(array $operations, ?DocumentCodec $codec): array + private function validateOperations(array $operations, ?DocumentCodec $codec, Encoder $builderEncoder): array { foreach ($operations as $i => $operation) { if (! is_array($operation)) { @@ -298,6 +307,8 @@ private function validateOperations(array $operations, ?DocumentCodec $codec): a case self::DELETE_MANY: case self::DELETE_ONE: + $operations[$i][$type][0] = $builderEncoder->encodeIfSupported($args[0]); + if (! isset($args[1])) { $args[1] = []; } @@ -317,6 +328,8 @@ private function validateOperations(array $operations, ?DocumentCodec $codec): a break; case self::REPLACE_ONE: + $operations[$i][$type][0] = $builderEncoder->encodeIfSupported($args[0]); + if (! isset($args[1]) && ! array_key_exists(1, $args)) { throw new InvalidArgumentException(sprintf('Missing second argument for $operations[%d]["%s"]', $i, $type)); } @@ -367,10 +380,14 @@ private function validateOperations(array $operations, ?DocumentCodec $codec): a case self::UPDATE_MANY: case self::UPDATE_ONE: + $operations[$i][$type][0] = $builderEncoder->encodeIfSupported($args[0]); + if (! isset($args[1]) && ! array_key_exists(1, $args)) { throw new InvalidArgumentException(sprintf('Missing second argument for $operations[%d]["%s"]', $i, $type)); } + $operations[$i][$type][1] = $args[1] = $builderEncoder->encodeIfSupported($args[1]); + if ((! is_document($args[1]) || ! is_first_key_operator($args[1])) && ! is_pipeline($args[1])) { throw new InvalidArgumentException(sprintf('Expected update operator(s) or non-empty pipeline for $operations[%d]["%s"][1]', $i, $type)); } diff --git a/tests/Collection/BuilderCollectionFunctionalTest.php b/tests/Collection/BuilderCollectionFunctionalTest.php new file mode 100644 index 000000000..15a89cda5 --- /dev/null +++ b/tests/Collection/BuilderCollectionFunctionalTest.php @@ -0,0 +1,250 @@ +collection->insertMany([['x' => 1], ['x' => 2], ['x' => 2]]); + } + + public function testAggregate(): void + { + $this->markTestSkipped('Not supported yet'); + } + + public function testBulkWriteDeleteMany(): void + { + $result = $this->collection->bulkWrite([ + [ + 'deleteMany' => [ + Query::query(x: Query::gt(1)), + ], + ], + ]); + $this->assertEquals(2, $result->getDeletedCount()); + } + + public function testBulkWriteDeleteOne(): void + { + $result = $this->collection->bulkWrite([ + [ + 'deleteOne' => [ + Query::query(x: Query::eq(1)), + ], + ], + ]); + $this->assertEquals(1, $result->getDeletedCount()); + } + + public function testBulkWriteReplaceOne(): void + { + $result = $this->collection->bulkWrite([ + [ + 'replaceOne' => [ + Query::query(x: Query::eq(1)), + ['x' => 3], + ], + ], + ]); + $this->assertEquals(1, $result->getModifiedCount()); + + $result = $this->collection->findOne(Query::query(x: Query::eq(3))); + $this->assertEquals(3, $result->x); + } + + public function testBulkWriteUpdateMany(): void + { + $result = $this->collection->bulkWrite([ + [ + 'updateMany' => [ + Query::query(x: Query::gt(1)), + // @todo Use Builder when update operators are supported by PHPLIB-1507 + ['$set' => ['x' => 3]], + ], + ], + ]); + $this->assertEquals(2, $result->getModifiedCount()); + + $result = $this->collection->find(Query::query(x: Query::eq(3)))->toArray(); + $this->assertCount(2, $result); + $this->assertEquals(3, $result[0]->x); + } + + public function testBulkWriteUpdateOne(): void + { + $result = $this->collection->bulkWrite([ + [ + 'updateOne' => [ + Query::query(x: Query::eq(1)), + // @todo Use Builder when update operators are supported by PHPLIB-1507 + ['$set' => ['x' => 3]], + ], + ], + ]); + + $this->assertEquals(1, $result->getModifiedCount()); + + $result = $this->collection->findOne(Query::query(x: Query::eq(3))); + $this->assertEquals(3, $result->x); + } + + public function testCountDocuments(): void + { + $result = $this->collection->countDocuments(Query::query(x: Query::gt(1))); + $this->assertEquals(2, $result); + } + + public function testDeleteMany(): void + { + $result = $this->collection->deleteMany(Query::query(x: Query::gt(1))); + $this->assertEquals(2, $result->getDeletedCount()); + } + + public function testDeleteOne(): void + { + $result = $this->collection->deleteOne(Query::query(x: Query::gt(1))); + $this->assertEquals(1, $result->getDeletedCount()); + } + + public function testDistinct(): void + { + $result = $this->collection->distinct('x', Query::query(x: Query::gt(1))); + $this->assertEquals([2], $result); + } + + public function testFind(): void + { + $results = $this->collection->find(Query::query(x: Query::gt(1)))->toArray(); + $this->assertCount(2, $results); + $this->assertEquals(2, $results[0]->x); + } + + public function testFindOne(): void + { + $result = $this->collection->findOne(Query::query(x: Query::eq(1))); + $this->assertEquals(1, $result->x); + } + + public function testFindOneAndDelete(): void + { + $result = $this->collection->findOneAndDelete(Query::query(x: Query::eq(1))); + $this->assertEquals(1, $result->x); + + $result = $this->collection->find()->toArray(); + $this->assertCount(2, $result); + } + + public function testFindOneAndReplace(): void + { + $this->collection->insertOne(['x' => 1]); + + $result = $this->collection->findOneAndReplace( + Query::query(x: Query::lt(2)), + ['x' => 3], + ); + $this->assertEquals(1, $result->x); + + $result = $this->collection->findOne(Query::query(x: Query::eq(3))); + $this->assertEquals(3, $result->x); + } + + public function testFindOneAndUpdate(): void + { + $result = $this->collection->findOneAndUpdate( + Query::query(x: Query::lt(2)), + // @todo Use Builder when update operators are supported by PHPLIB-1507 + ['$set' => ['x' => 3]], + ); + $this->assertEquals(1, $result->x); + + $result = $this->collection->findOne(Query::query(x: Query::eq(3))); + $this->assertEquals(3, $result->x); + } + + public function testReplaceOne(): void + { + $this->collection->insertOne(['x' => 1]); + + $result = $this->collection->replaceOne( + Query::query(x: Query::lt(2)), + ['x' => 3], + ); + $this->assertEquals(1, $result->getModifiedCount()); + + $result = $this->collection->findOne(Query::query(x: Query::eq(3))); + $this->assertEquals(3, $result->x); + } + + public function testUpdateOne(): void + { + $this->collection->insertOne(['x' => 1]); + + $result = $this->collection->updateOne( + Query::query(x: Query::lt(2)), + // @todo Use Builder when update operators are supported by PHPLIB-1507 + ['$set' => ['x' => 3]], + ); + $this->assertEquals(1, $result->getModifiedCount()); + + $result = $this->collection->findOne(Query::query(x: Query::eq(3))); + $this->assertEquals(3, $result->x); + } + + public function testUpdateWithPipeline(): void + { + $this->skipIfServerVersion('<', '4.2.0', 'Pipeline-style updates are not supported'); + + $result = $this->collection->updateOne( + Query::query(x: Query::lt(2)), + new Pipeline( + Stage::set(x: 3), + ), + ); + + $this->assertEquals(1, $result->getModifiedCount()); + } + + public function testUpdateMany(): void + { + $result = $this->collection->updateMany( + Query::query(x: Query::gt(1)), + // @todo Use Builder when update operators are supported by PHPLIB-1507 + ['$set' => ['x' => 3]], + ); + $this->assertEquals(2, $result->getModifiedCount()); + + $result = $this->collection->find(Query::query(x: Query::eq(3)))->toArray(); + $this->assertCount(2, $result); + $this->assertEquals(3, $result[0]->x); + } + + public function testUpdateManyWithPipeline(): void + { + $this->skipIfServerVersion('<', '4.2.0', 'Pipeline-style updates are not supported'); + + $result = $this->collection->updateMany( + Query::query(x: Query::gt(1)), + new Pipeline( + Stage::set(x: 3), + ), + ); + $this->assertEquals(2, $result->getModifiedCount()); + + $result = $this->collection->find(Query::query(x: Query::eq(3)))->toArray(); + $this->assertCount(2, $result); + $this->assertEquals(3, $result[0]->x); + } + + public function testWatch(): void + { + $this->markTestSkipped('Not supported yet'); + } +}