diff --git a/psalm-baseline.xml b/psalm-baseline.xml index deb61756f..00c11f728 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -195,6 +195,9 @@ + + + @@ -216,6 +219,11 @@ + + + + + @@ -228,6 +236,11 @@ + + + + + diff --git a/src/Client.php b/src/Client.php index 0b9282cf0..5b02aa44c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -19,6 +19,10 @@ use Composer\InstalledVersions; use Iterator; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; +use MongoDB\Builder\BuilderEncoder; +use MongoDB\Codec\Encoder; use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; @@ -38,8 +42,10 @@ use MongoDB\Operation\ListDatabaseNames; use MongoDB\Operation\ListDatabases; use MongoDB\Operation\Watch; +use stdClass; use Throwable; +use function array_diff_key; use function is_array; use function is_string; @@ -67,6 +73,9 @@ class Client private array $typeMap; + /** @psalm-var Encoder */ + private readonly Encoder $builderEncoder; + private WriteConcern $writeConcern; /** @@ -78,6 +87,9 @@ class Client * * Supported driver-specific options: * + * * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and + * aggregation builders. If not given, the default encoder will be used. + * * * typeMap (array): Default type map for cursors and BSON documents. * * Other options are documented in MongoDB\Driver\Manager::__construct(). @@ -108,12 +120,17 @@ public function __construct(?string $uri = null, array $uriOptions = [], array $ } } + if (isset($driverOptions['builderEncoder']) && ! $driverOptions['builderEncoder'] instanceof Encoder) { + throw InvalidArgumentException::invalidType('"builderEncoder" option', $driverOptions['builderEncoder'], Encoder::class); + } + $driverOptions['driver'] = $this->mergeDriverInfo($driverOptions['driver'] ?? []); $this->uri = $uri ?? self::DEFAULT_URI; + $this->builderEncoder = $driverOptions['builderEncoder'] ?? new BuilderEncoder(); $this->typeMap = $driverOptions['typeMap']; - unset($driverOptions['typeMap']); + $driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]); $this->manager = new Manager($uri, $uriOptions, $driverOptions); $this->readConcern = $this->manager->getReadConcern(); @@ -133,6 +150,7 @@ public function __debugInfo() 'manager' => $this->manager, 'uri' => $this->uri, 'typeMap' => $this->typeMap, + 'builderEncoder' => $this->builderEncoder, 'writeConcern' => $this->writeConcern, ]; } @@ -329,7 +347,7 @@ final public function removeSubscriber(Subscriber $subscriber): void */ public function selectCollection(string $databaseName, string $collectionName, array $options = []) { - $options += ['typeMap' => $this->typeMap]; + $options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder]; return new Collection($this->manager, $databaseName, $collectionName, $options); } @@ -345,7 +363,7 @@ public function selectCollection(string $databaseName, string $collectionName, a */ public function selectDatabase(string $databaseName, array $options = []) { - $options += ['typeMap' => $this->typeMap]; + $options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder]; return new Database($this->manager, $databaseName, $options); } diff --git a/src/Collection.php b/src/Collection.php index 0118bc335..28133ff55 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -19,8 +19,12 @@ use Countable; use Iterator; +use MongoDB\BSON\Document; use MongoDB\BSON\JavascriptInterface; +use MongoDB\BSON\PackedArray; +use MongoDB\Builder\BuilderEncoder; use MongoDB\Codec\DocumentCodec; +use MongoDB\Codec\Encoder; use MongoDB\Driver\CursorInterface; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Manager; @@ -66,6 +70,7 @@ use MongoDB\Operation\UpdateOne; use MongoDB\Operation\UpdateSearchIndex; use MongoDB\Operation\Watch; +use stdClass; use function array_diff_key; use function array_intersect_key; @@ -84,6 +89,9 @@ class Collection private const WIRE_VERSION_FOR_READ_CONCERN_WITH_WRITE_STAGE = 8; + /** @psalm-var Encoder */ + private readonly Encoder $builderEncoder; + private ?DocumentCodec $codec = null; private ReadConcern $readConcern; @@ -102,6 +110,9 @@ class Collection * * Supported options: * + * * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and + * aggregation builders. If not given, the default encoder will be used. + * * * codec (MongoDB\Codec\DocumentCodec): Codec used to decode documents * from BSON to PHP objects. * @@ -134,6 +145,10 @@ public function __construct(private Manager $manager, private string $databaseNa throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName); } + if (isset($options['builderEncoder']) && ! $options['builderEncoder'] instanceof Encoder) { + throw InvalidArgumentException::invalidType('"builderEncoder" option', $options['builderEncoder'], Encoder::class); + } + if (isset($options['codec']) && ! $options['codec'] instanceof DocumentCodec) { throw InvalidArgumentException::invalidType('"codec" option', $options['codec'], DocumentCodec::class); } @@ -154,6 +169,7 @@ public function __construct(private Manager $manager, private string $databaseNa throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); } + $this->builderEncoder = $options['builderEncoder'] ?? new BuilderEncoder(); $this->codec = $options['codec'] ?? null; $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); @@ -170,6 +186,7 @@ public function __construct(private Manager $manager, private string $databaseNa public function __debugInfo() { return [ + 'builderEncoder' => $this->builderEncoder, 'codec' => $this->codec, 'collectionName' => $this->collectionName, 'databaseName' => $this->databaseName, @@ -1084,6 +1101,7 @@ public function watch(array $pipeline = [], array $options = []) public function withOptions(array $options = []) { $options += [ + 'builderEncoder' => $this->builderEncoder, 'codec' => $this->codec, 'readConcern' => $this->readConcern, 'readPreference' => $this->readPreference, diff --git a/src/Database.php b/src/Database.php index d466dcb6a..8b7742b70 100644 --- a/src/Database.php +++ b/src/Database.php @@ -18,6 +18,10 @@ namespace MongoDB; use Iterator; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; +use MongoDB\Builder\BuilderEncoder; +use MongoDB\Codec\Encoder; use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\Cursor; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; @@ -45,6 +49,7 @@ use MongoDB\Operation\ModifyCollection; use MongoDB\Operation\RenameCollection; use MongoDB\Operation\Watch; +use stdClass; use Throwable; use Traversable; @@ -61,6 +66,9 @@ class Database private const WIRE_VERSION_FOR_READ_CONCERN_WITH_WRITE_STAGE = 8; + /** @psalm-var Encoder */ + private readonly Encoder $builderEncoder; + private ReadConcern $readConcern; private ReadPreference $readPreference; @@ -77,6 +85,9 @@ class Database * * Supported options: * + * * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and + * aggregation builders. If not given, the default encoder will be used. + * * * readConcern (MongoDB\Driver\ReadConcern): The default read concern to * use for database operations and selected collections. Defaults to the * Manager's read concern. @@ -102,6 +113,10 @@ public function __construct(private Manager $manager, private string $databaseNa throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName); } + if (isset($options['builderEncoder']) && ! $options['builderEncoder'] instanceof Encoder) { + throw InvalidArgumentException::invalidType('"builderEncoder" option', $options['builderEncoder'], Encoder::class); + } + if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) { throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class); } @@ -118,6 +133,7 @@ public function __construct(private Manager $manager, private string $databaseNa throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); } + $this->builderEncoder = $options['builderEncoder'] ?? new BuilderEncoder(); $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); $this->typeMap = $options['typeMap'] ?? self::DEFAULT_TYPE_MAP; @@ -133,6 +149,7 @@ public function __construct(private Manager $manager, private string $databaseNa public function __debugInfo() { return [ + 'builderEncoder' => $this->builderEncoder, 'databaseName' => $this->databaseName, 'manager' => $this->manager, 'readConcern' => $this->readConcern, @@ -553,6 +570,7 @@ public function renameCollection(string $fromCollectionName, string $toCollectio public function selectCollection(string $collectionName, array $options = []) { $options += [ + 'builderEncoder' => $this->builderEncoder, 'readConcern' => $this->readConcern, 'readPreference' => $this->readPreference, 'typeMap' => $this->typeMap, diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 04855d479..584714c98 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -3,6 +3,7 @@ namespace MongoDB\Tests; use MongoDB\Client; +use MongoDB\Codec\Encoder; use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException; use MongoDB\Driver\ReadConcern; @@ -45,6 +46,10 @@ public function provideInvalidConstructorDriverOptions() { $options = []; + foreach ($this->getInvalidObjectValues() as $value) { + $options[][] = ['builderEncoder' => $value]; + } + foreach ($this->getInvalidArrayValues(true) as $value) { $options[][] = ['typeMap' => $value]; } @@ -85,6 +90,7 @@ public function testSelectCollectionInheritsOptions(): void ]; $driverOptions = [ + 'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class), 'typeMap' => ['root' => 'array'], ]; @@ -92,6 +98,7 @@ public function testSelectCollectionInheritsOptions(): void $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $debug = $collection->__debugInfo(); + $this->assertSame($builderEncoder, $debug['builderEncoder']); $this->assertInstanceOf(ReadConcern::class, $debug['readConcern']); $this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel()); $this->assertInstanceOf(ReadPreference::class, $debug['readPreference']); @@ -105,6 +112,7 @@ public function testSelectCollectionInheritsOptions(): void public function testSelectCollectionPassesOptions(): void { $collectionOptions = [ + 'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class), 'readConcern' => new ReadConcern(ReadConcern::LOCAL), 'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED), 'typeMap' => ['root' => 'array'], @@ -115,6 +123,7 @@ public function testSelectCollectionPassesOptions(): void $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName(), $collectionOptions); $debug = $collection->__debugInfo(); + $this->assertSame($builderEncoder, $debug['builderEncoder']); $this->assertInstanceOf(ReadConcern::class, $debug['readConcern']); $this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel()); $this->assertInstanceOf(ReadPreference::class, $debug['readPreference']); @@ -129,11 +138,19 @@ public function testGetSelectsDatabaseAndInheritsOptions(): void { $uriOptions = ['w' => WriteConcern::MAJORITY]; - $client = new Client(static::getUri(), $uriOptions); + $driverOptions = [ + 'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class), + 'typeMap' => ['root' => 'array'], + ]; + + $client = new Client(static::getUri(), $uriOptions, $driverOptions); $database = $client->{$this->getDatabaseName()}; $debug = $database->__debugInfo(); + $this->assertSame($builderEncoder, $debug['builderEncoder']); $this->assertSame($this->getDatabaseName(), $debug['databaseName']); + $this->assertIsArray($debug['typeMap']); + $this->assertSame(['root' => 'array'], $debug['typeMap']); $this->assertInstanceOf(WriteConcern::class, $debug['writeConcern']); $this->assertSame(WriteConcern::MAJORITY, $debug['writeConcern']->getW()); } @@ -147,6 +164,7 @@ public function testSelectDatabaseInheritsOptions(): void ]; $driverOptions = [ + 'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class), 'typeMap' => ['root' => 'array'], ]; @@ -154,6 +172,7 @@ public function testSelectDatabaseInheritsOptions(): void $database = $client->selectDatabase($this->getDatabaseName()); $debug = $database->__debugInfo(); + $this->assertSame($builderEncoder, $debug['builderEncoder']); $this->assertInstanceOf(ReadConcern::class, $debug['readConcern']); $this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel()); $this->assertInstanceOf(ReadPreference::class, $debug['readPreference']); @@ -167,6 +186,7 @@ public function testSelectDatabaseInheritsOptions(): void public function testSelectDatabasePassesOptions(): void { $databaseOptions = [ + 'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class), 'readConcern' => new ReadConcern(ReadConcern::LOCAL), 'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED), 'typeMap' => ['root' => 'array'], diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index d95b07737..e4f6c649a 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -4,6 +4,7 @@ use Closure; use MongoDB\BSON\Javascript; +use MongoDB\Codec\Encoder; use MongoDB\Collection; use MongoDB\Database; use MongoDB\Driver\BulkWrite; @@ -66,6 +67,7 @@ public function testConstructorOptionTypeChecks(array $options): void public function provideInvalidConstructorOptions(): array { return $this->createOptionDataProvider([ + 'builderEncoder' => $this->getInvalidObjectValues(), 'codec' => $this->getInvalidDocumentCodecValues(), 'readConcern' => $this->getInvalidReadConcernValues(), 'readPreference' => $this->getInvalidReadPreferenceValues(), @@ -396,6 +398,7 @@ public function testWithOptionsInheritsOptions(): void public function testWithOptionsPassesOptions(): void { $collectionOptions = [ + 'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class), 'readConcern' => new ReadConcern(ReadConcern::LOCAL), 'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED), 'typeMap' => ['root' => 'array'], @@ -405,6 +408,7 @@ public function testWithOptionsPassesOptions(): void $clone = $this->collection->withOptions($collectionOptions); $debug = $clone->__debugInfo(); + $this->assertSame($builderEncoder, $debug['builderEncoder']); $this->assertInstanceOf(ReadConcern::class, $debug['readConcern']); $this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel()); $this->assertInstanceOf(ReadPreference::class, $debug['readPreference']); diff --git a/tests/TestCase.php b/tests/TestCase.php index 287dc0b59..53e46fb82 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -18,7 +18,6 @@ use Traversable; use function array_map; -use function array_merge; use function array_values; use function call_user_func; use function get_debug_type; @@ -209,7 +208,7 @@ protected function getCollectionName(): string */ protected function getInvalidArrayValues(bool $includeNull = false): array { - return array_merge([123, 3.14, 'foo', true, new stdClass()], $includeNull ? [null] : []); + return [123, 3.14, 'foo', true, new stdClass(), ...($includeNull ? [null] : [])]; } /** @@ -217,7 +216,7 @@ protected function getInvalidArrayValues(bool $includeNull = false): array */ protected function getInvalidBooleanValues(bool $includeNull = false): array { - return array_merge([123, 3.14, 'foo', [], new stdClass()], $includeNull ? [null] : []); + return [123, 3.14, 'foo', [], new stdClass(), ...($includeNull ? [null] : [])]; } /** @@ -225,7 +224,12 @@ protected function getInvalidBooleanValues(bool $includeNull = false): array */ protected function getInvalidDocumentValues(bool $includeNull = false): array { - return array_merge([123, 3.14, 'foo', true, PackedArray::fromPHP([])], $includeNull ? [null] : []); + return [123, 3.14, 'foo', true, PackedArray::fromPHP([]), ...($includeNull ? [null] : [])]; + } + + protected function getInvalidObjectValues(bool $includeNull = false): array + { + return [123, 3.14, 'foo', true, [], new stdClass(), ...($includeNull ? [null] : [])]; } protected function getInvalidDocumentCodecValues(): array @@ -246,7 +250,7 @@ protected function getInvalidHintValues() */ protected function getInvalidIntegerValues(bool $includeNull = false): array { - return array_merge([3.14, 'foo', true, [], new stdClass()], $includeNull ? [null] : []); + return [3.14, 'foo', true, [], new stdClass(), ...($includeNull ? [null] : [])]; } /** @@ -254,19 +258,17 @@ protected function getInvalidIntegerValues(bool $includeNull = false): array */ protected function getInvalidReadConcernValues(bool $includeNull = false): array { - return array_merge( - [ - 123, - 3.14, - 'foo', - true, - [], - new stdClass(), - new ReadPreference(ReadPreference::PRIMARY), - new WriteConcern(1), - ], - $includeNull ? ['null' => null] : [], - ); + return [ + 123, + 3.14, + 'foo', + true, + [], + new stdClass(), + new ReadPreference(ReadPreference::PRIMARY), + new WriteConcern(1), + ...($includeNull ? ['null' => null] : []), + ]; } /** @@ -274,19 +276,17 @@ protected function getInvalidReadConcernValues(bool $includeNull = false): array */ protected function getInvalidReadPreferenceValues(bool $includeNull = false): array { - return array_merge( - [ - 123, - 3.14, - 'foo', - true, - [], - new stdClass(), - new ReadConcern(), - new WriteConcern(1), - ], - $includeNull ? ['null' => null] : [], - ); + return [ + 123, + 3.14, + 'foo', + true, + [], + new stdClass(), + new ReadConcern(), + new WriteConcern(1), + ...($includeNull ? ['null' => null] : []), + ]; } /** @@ -294,20 +294,18 @@ protected function getInvalidReadPreferenceValues(bool $includeNull = false): ar */ protected function getInvalidSessionValues(bool $includeNull = false): array { - return array_merge( - [ - 123, - 3.14, - 'foo', - true, - [], - new stdClass(), - new ReadConcern(), - new ReadPreference(ReadPreference::PRIMARY), - new WriteConcern(1), - ], - $includeNull ? ['null' => null] : [], - ); + return [ + 123, + 3.14, + 'foo', + true, + [], + new stdClass(), + new ReadConcern(), + new ReadPreference(ReadPreference::PRIMARY), + new WriteConcern(1), + ...($includeNull ? ['null' => null] : []), + ]; } /** @@ -315,7 +313,7 @@ protected function getInvalidSessionValues(bool $includeNull = false): array */ protected function getInvalidStringValues(bool $includeNull = false): array { - return array_merge([123, 3.14, true, [], new stdClass()], $includeNull ? [null] : []); + return [123, 3.14, true, [], new stdClass(), ...($includeNull ? [null] : [])]; } /** @@ -323,19 +321,17 @@ protected function getInvalidStringValues(bool $includeNull = false): array */ protected function getInvalidWriteConcernValues(bool $includeNull = false): array { - return array_merge( - [ - 123, - 3.14, - 'foo', - true, - [], - new stdClass(), - new ReadConcern(), - new ReadPreference(ReadPreference::PRIMARY), - ], - $includeNull ? ['null' => null] : [], - ); + return [ + 123, + 3.14, + 'foo', + true, + [], + new stdClass(), + new ReadConcern(), + new ReadPreference(ReadPreference::PRIMARY), + ...($includeNull ? ['null' => null] : []), + ]; } /**