Skip to content

Commit be2df01

Browse files
authored
PHPLIB-1505 Add driver option "builderEncoder" (#1382)
1 parent d2a9458 commit be2df01

7 files changed

+151
-64
lines changed

psalm-baseline.xml

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="5.25.0@01a8eb06b9e9cc6cfb6a320bf9fb14331919d505">
2+
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
33
<file src="examples/atlas_search.php">
44
<MixedArgument>
55
<code><![CDATA[$document['name']]]></code>
@@ -195,6 +195,9 @@
195195
<MixedAssignment>
196196
<code><![CDATA[$mergedDriver['platform']]]></code>
197197
</MixedAssignment>
198+
<MixedPropertyTypeCoercion>
199+
<code><![CDATA[$driverOptions['builderEncoder'] ?? new BuilderEncoder()]]></code>
200+
</MixedPropertyTypeCoercion>
198201
</file>
199202
<file src="src/Codec/EncodeIfSupported.php">
200203
<ArgumentTypeCoercion>
@@ -216,6 +219,11 @@
216219
<code><![CDATA[$value]]></code>
217220
</MixedArgumentTypeCoercion>
218221
</file>
222+
<file src="src/Collection.php">
223+
<MixedPropertyTypeCoercion>
224+
<code><![CDATA[$options['builderEncoder'] ?? new BuilderEncoder()]]></code>
225+
</MixedPropertyTypeCoercion>
226+
</file>
219227
<file src="src/Command/ListCollections.php">
220228
<MixedAssignment>
221229
<code><![CDATA[$cmd[$option]]]></code>
@@ -228,6 +236,11 @@
228236
<code><![CDATA[$options['session']]]></code>
229237
</MixedAssignment>
230238
</file>
239+
<file src="src/Database.php">
240+
<MixedPropertyTypeCoercion>
241+
<code><![CDATA[$options['builderEncoder'] ?? new BuilderEncoder()]]></code>
242+
</MixedPropertyTypeCoercion>
243+
</file>
231244
<file src="src/GridFS/Bucket.php">
232245
<MixedArgument>
233246
<code><![CDATA[$options['revision']]]></code>

src/Client.php

+21-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
use Composer\InstalledVersions;
2121
use Iterator;
22+
use MongoDB\BSON\Document;
23+
use MongoDB\BSON\PackedArray;
24+
use MongoDB\Builder\BuilderEncoder;
25+
use MongoDB\Codec\Encoder;
2226
use MongoDB\Driver\ClientEncryption;
2327
use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
2428
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
@@ -38,8 +42,10 @@
3842
use MongoDB\Operation\ListDatabaseNames;
3943
use MongoDB\Operation\ListDatabases;
4044
use MongoDB\Operation\Watch;
45+
use stdClass;
4146
use Throwable;
4247

48+
use function array_diff_key;
4349
use function is_array;
4450
use function is_string;
4551

@@ -67,6 +73,9 @@ class Client
6773

6874
private array $typeMap;
6975

76+
/** @psalm-var Encoder<array|stdClass|Document|PackedArray, mixed> */
77+
private readonly Encoder $builderEncoder;
78+
7079
private WriteConcern $writeConcern;
7180

7281
/**
@@ -78,6 +87,9 @@ class Client
7887
*
7988
* Supported driver-specific options:
8089
*
90+
* * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and
91+
* aggregation builders. If not given, the default encoder will be used.
92+
*
8193
* * typeMap (array): Default type map for cursors and BSON documents.
8294
*
8395
* Other options are documented in MongoDB\Driver\Manager::__construct().
@@ -108,12 +120,17 @@ public function __construct(?string $uri = null, array $uriOptions = [], array $
108120
}
109121
}
110122

123+
if (isset($driverOptions['builderEncoder']) && ! $driverOptions['builderEncoder'] instanceof Encoder) {
124+
throw InvalidArgumentException::invalidType('"builderEncoder" option', $driverOptions['builderEncoder'], Encoder::class);
125+
}
126+
111127
$driverOptions['driver'] = $this->mergeDriverInfo($driverOptions['driver'] ?? []);
112128

113129
$this->uri = $uri ?? self::DEFAULT_URI;
130+
$this->builderEncoder = $driverOptions['builderEncoder'] ?? new BuilderEncoder();
114131
$this->typeMap = $driverOptions['typeMap'];
115132

116-
unset($driverOptions['typeMap']);
133+
$driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]);
117134

118135
$this->manager = new Manager($uri, $uriOptions, $driverOptions);
119136
$this->readConcern = $this->manager->getReadConcern();
@@ -133,6 +150,7 @@ public function __debugInfo()
133150
'manager' => $this->manager,
134151
'uri' => $this->uri,
135152
'typeMap' => $this->typeMap,
153+
'builderEncoder' => $this->builderEncoder,
136154
'writeConcern' => $this->writeConcern,
137155
];
138156
}
@@ -329,7 +347,7 @@ final public function removeSubscriber(Subscriber $subscriber): void
329347
*/
330348
public function selectCollection(string $databaseName, string $collectionName, array $options = [])
331349
{
332-
$options += ['typeMap' => $this->typeMap];
350+
$options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder];
333351

334352
return new Collection($this->manager, $databaseName, $collectionName, $options);
335353
}
@@ -345,7 +363,7 @@ public function selectCollection(string $databaseName, string $collectionName, a
345363
*/
346364
public function selectDatabase(string $databaseName, array $options = [])
347365
{
348-
$options += ['typeMap' => $this->typeMap];
366+
$options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder];
349367

350368
return new Database($this->manager, $databaseName, $options);
351369
}

src/Collection.php

+18
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919

2020
use Countable;
2121
use Iterator;
22+
use MongoDB\BSON\Document;
2223
use MongoDB\BSON\JavascriptInterface;
24+
use MongoDB\BSON\PackedArray;
25+
use MongoDB\Builder\BuilderEncoder;
2326
use MongoDB\Codec\DocumentCodec;
27+
use MongoDB\Codec\Encoder;
2428
use MongoDB\Driver\CursorInterface;
2529
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2630
use MongoDB\Driver\Manager;
@@ -66,6 +70,7 @@
6670
use MongoDB\Operation\UpdateOne;
6771
use MongoDB\Operation\UpdateSearchIndex;
6872
use MongoDB\Operation\Watch;
73+
use stdClass;
6974

7075
use function array_diff_key;
7176
use function array_intersect_key;
@@ -84,6 +89,9 @@ class Collection
8489

8590
private const WIRE_VERSION_FOR_READ_CONCERN_WITH_WRITE_STAGE = 8;
8691

92+
/** @psalm-var Encoder<array|stdClass|Document|PackedArray, mixed> */
93+
private readonly Encoder $builderEncoder;
94+
8795
private ?DocumentCodec $codec = null;
8896

8997
private ReadConcern $readConcern;
@@ -102,6 +110,9 @@ class Collection
102110
*
103111
* Supported options:
104112
*
113+
* * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and
114+
* aggregation builders. If not given, the default encoder will be used.
115+
*
105116
* * codec (MongoDB\Codec\DocumentCodec): Codec used to decode documents
106117
* from BSON to PHP objects.
107118
*
@@ -134,6 +145,10 @@ public function __construct(private Manager $manager, private string $databaseNa
134145
throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName);
135146
}
136147

148+
if (isset($options['builderEncoder']) && ! $options['builderEncoder'] instanceof Encoder) {
149+
throw InvalidArgumentException::invalidType('"builderEncoder" option', $options['builderEncoder'], Encoder::class);
150+
}
151+
137152
if (isset($options['codec']) && ! $options['codec'] instanceof DocumentCodec) {
138153
throw InvalidArgumentException::invalidType('"codec" option', $options['codec'], DocumentCodec::class);
139154
}
@@ -154,6 +169,7 @@ public function __construct(private Manager $manager, private string $databaseNa
154169
throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
155170
}
156171

172+
$this->builderEncoder = $options['builderEncoder'] ?? new BuilderEncoder();
157173
$this->codec = $options['codec'] ?? null;
158174
$this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern();
159175
$this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference();
@@ -170,6 +186,7 @@ public function __construct(private Manager $manager, private string $databaseNa
170186
public function __debugInfo()
171187
{
172188
return [
189+
'builderEncoder' => $this->builderEncoder,
173190
'codec' => $this->codec,
174191
'collectionName' => $this->collectionName,
175192
'databaseName' => $this->databaseName,
@@ -1084,6 +1101,7 @@ public function watch(array $pipeline = [], array $options = [])
10841101
public function withOptions(array $options = [])
10851102
{
10861103
$options += [
1104+
'builderEncoder' => $this->builderEncoder,
10871105
'codec' => $this->codec,
10881106
'readConcern' => $this->readConcern,
10891107
'readPreference' => $this->readPreference,

src/Database.php

+18
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
namespace MongoDB;
1919

2020
use Iterator;
21+
use MongoDB\BSON\Document;
22+
use MongoDB\BSON\PackedArray;
23+
use MongoDB\Builder\BuilderEncoder;
24+
use MongoDB\Codec\Encoder;
2125
use MongoDB\Driver\ClientEncryption;
2226
use MongoDB\Driver\Cursor;
2327
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
@@ -45,6 +49,7 @@
4549
use MongoDB\Operation\ModifyCollection;
4650
use MongoDB\Operation\RenameCollection;
4751
use MongoDB\Operation\Watch;
52+
use stdClass;
4853
use Throwable;
4954
use Traversable;
5055

@@ -61,6 +66,9 @@ class Database
6166

6267
private const WIRE_VERSION_FOR_READ_CONCERN_WITH_WRITE_STAGE = 8;
6368

69+
/** @psalm-var Encoder<array|stdClass|Document|PackedArray, mixed> */
70+
private readonly Encoder $builderEncoder;
71+
6472
private ReadConcern $readConcern;
6573

6674
private ReadPreference $readPreference;
@@ -77,6 +85,9 @@ class Database
7785
*
7886
* Supported options:
7987
*
88+
* * builderEncoder (MongoDB\Builder\Encoder): Encoder for query and
89+
* aggregation builders. If not given, the default encoder will be used.
90+
*
8091
* * readConcern (MongoDB\Driver\ReadConcern): The default read concern to
8192
* use for database operations and selected collections. Defaults to the
8293
* Manager's read concern.
@@ -102,6 +113,10 @@ public function __construct(private Manager $manager, private string $databaseNa
102113
throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName);
103114
}
104115

116+
if (isset($options['builderEncoder']) && ! $options['builderEncoder'] instanceof Encoder) {
117+
throw InvalidArgumentException::invalidType('"builderEncoder" option', $options['builderEncoder'], Encoder::class);
118+
}
119+
105120
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
106121
throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class);
107122
}
@@ -118,6 +133,7 @@ public function __construct(private Manager $manager, private string $databaseNa
118133
throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
119134
}
120135

136+
$this->builderEncoder = $options['builderEncoder'] ?? new BuilderEncoder();
121137
$this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern();
122138
$this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference();
123139
$this->typeMap = $options['typeMap'] ?? self::DEFAULT_TYPE_MAP;
@@ -133,6 +149,7 @@ public function __construct(private Manager $manager, private string $databaseNa
133149
public function __debugInfo()
134150
{
135151
return [
152+
'builderEncoder' => $this->builderEncoder,
136153
'databaseName' => $this->databaseName,
137154
'manager' => $this->manager,
138155
'readConcern' => $this->readConcern,
@@ -553,6 +570,7 @@ public function renameCollection(string $fromCollectionName, string $toCollectio
553570
public function selectCollection(string $collectionName, array $options = [])
554571
{
555572
$options += [
573+
'builderEncoder' => $this->builderEncoder,
556574
'readConcern' => $this->readConcern,
557575
'readPreference' => $this->readPreference,
558576
'typeMap' => $this->typeMap,

tests/ClientTest.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace MongoDB\Tests;
44

55
use MongoDB\Client;
6+
use MongoDB\Codec\Encoder;
67
use MongoDB\Driver\ClientEncryption;
78
use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
89
use MongoDB\Driver\ReadConcern;
@@ -45,6 +46,10 @@ public function provideInvalidConstructorDriverOptions()
4546
{
4647
$options = [];
4748

49+
foreach ($this->getInvalidObjectValues() as $value) {
50+
$options[][] = ['builderEncoder' => $value];
51+
}
52+
4853
foreach ($this->getInvalidArrayValues(true) as $value) {
4954
$options[][] = ['typeMap' => $value];
5055
}
@@ -85,13 +90,15 @@ public function testSelectCollectionInheritsOptions(): void
8590
];
8691

8792
$driverOptions = [
93+
'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class),
8894
'typeMap' => ['root' => 'array'],
8995
];
9096

9197
$client = new Client(static::getUri(), $uriOptions, $driverOptions);
9298
$collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName());
9399
$debug = $collection->__debugInfo();
94100

101+
$this->assertSame($builderEncoder, $debug['builderEncoder']);
95102
$this->assertInstanceOf(ReadConcern::class, $debug['readConcern']);
96103
$this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel());
97104
$this->assertInstanceOf(ReadPreference::class, $debug['readPreference']);
@@ -105,6 +112,7 @@ public function testSelectCollectionInheritsOptions(): void
105112
public function testSelectCollectionPassesOptions(): void
106113
{
107114
$collectionOptions = [
115+
'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class),
108116
'readConcern' => new ReadConcern(ReadConcern::LOCAL),
109117
'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED),
110118
'typeMap' => ['root' => 'array'],
@@ -115,6 +123,7 @@ public function testSelectCollectionPassesOptions(): void
115123
$collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName(), $collectionOptions);
116124
$debug = $collection->__debugInfo();
117125

126+
$this->assertSame($builderEncoder, $debug['builderEncoder']);
118127
$this->assertInstanceOf(ReadConcern::class, $debug['readConcern']);
119128
$this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel());
120129
$this->assertInstanceOf(ReadPreference::class, $debug['readPreference']);
@@ -129,11 +138,19 @@ public function testGetSelectsDatabaseAndInheritsOptions(): void
129138
{
130139
$uriOptions = ['w' => WriteConcern::MAJORITY];
131140

132-
$client = new Client(static::getUri(), $uriOptions);
141+
$driverOptions = [
142+
'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class),
143+
'typeMap' => ['root' => 'array'],
144+
];
145+
146+
$client = new Client(static::getUri(), $uriOptions, $driverOptions);
133147
$database = $client->{$this->getDatabaseName()};
134148
$debug = $database->__debugInfo();
135149

150+
$this->assertSame($builderEncoder, $debug['builderEncoder']);
136151
$this->assertSame($this->getDatabaseName(), $debug['databaseName']);
152+
$this->assertIsArray($debug['typeMap']);
153+
$this->assertSame(['root' => 'array'], $debug['typeMap']);
137154
$this->assertInstanceOf(WriteConcern::class, $debug['writeConcern']);
138155
$this->assertSame(WriteConcern::MAJORITY, $debug['writeConcern']->getW());
139156
}
@@ -147,13 +164,15 @@ public function testSelectDatabaseInheritsOptions(): void
147164
];
148165

149166
$driverOptions = [
167+
'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class),
150168
'typeMap' => ['root' => 'array'],
151169
];
152170

153171
$client = new Client(static::getUri(), $uriOptions, $driverOptions);
154172
$database = $client->selectDatabase($this->getDatabaseName());
155173
$debug = $database->__debugInfo();
156174

175+
$this->assertSame($builderEncoder, $debug['builderEncoder']);
157176
$this->assertInstanceOf(ReadConcern::class, $debug['readConcern']);
158177
$this->assertSame(ReadConcern::LOCAL, $debug['readConcern']->getLevel());
159178
$this->assertInstanceOf(ReadPreference::class, $debug['readPreference']);
@@ -167,6 +186,7 @@ public function testSelectDatabaseInheritsOptions(): void
167186
public function testSelectDatabasePassesOptions(): void
168187
{
169188
$databaseOptions = [
189+
'builderEncoder' => $builderEncoder = $this->createMock(Encoder::class),
170190
'readConcern' => new ReadConcern(ReadConcern::LOCAL),
171191
'readPreference' => new ReadPreference(ReadPreference::SECONDARY_PREFERRED),
172192
'typeMap' => ['root' => 'array'],

0 commit comments

Comments
 (0)