Skip to content

Commit 895f41e

Browse files
committed
Extract driver options into value objects
So we can unit test options, as we're transforming some of the options that are passed
1 parent 82719c4 commit 895f41e

File tree

6 files changed

+491
-0
lines changed

6 files changed

+491
-0
lines changed

psalm.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
<!-- This is often the result of type checks due to missing native types -->
4040
<DocblockTypeContradiction errorLevel="info" />
4141

42+
<!-- We still want to check types at runtime for users not using Psalm -->
43+
<RedundantConditionGivenDocblockType errorLevel="info" />
44+
4245
<!-- If the result of getenv is falsy, using the default URI is fine -->
4346
<RiskyTruthyFalsyComparison>
4447
<errorLevel type="suppress">

src/Client.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public function __construct(?string $uri = null, array $uriOptions = [], array $
141141
$driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]);
142142

143143
$this->manager = new Manager($uri, $uriOptions, $driverOptions);
144+
144145
$this->readConcern = $this->manager->getReadConcern();
145146
$this->readPreference = $this->manager->getReadPreference();
146147
$this->writeConcern = $this->manager->getWriteConcern();
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace MongoDB\Model;
4+
5+
use MongoDB\Client;
6+
use MongoDB\Driver\Manager;
7+
use MongoDB\Exception\InvalidArgumentException;
8+
use stdClass;
9+
10+
use function array_diff_key;
11+
use function array_filter;
12+
use function is_array;
13+
use function sprintf;
14+
15+
/** @internal */
16+
final class AutoEncryptionOptions
17+
{
18+
private const KEY_KEY_VAULT_CLIENT = 'keyVaultClient';
19+
private const KEY_KMS_PROVIDERS = 'kmsProviders';
20+
21+
private function __construct(
22+
private readonly ?Manager $keyVaultClient,
23+
private readonly array|stdClass|null $kmsProviders,
24+
private readonly array $miscOptions,
25+
) {
26+
}
27+
28+
/** @param array{kmsProviders?: stdClass|array<string, array>, keyVaultClient?: Client|Manager} $options */
29+
public static function fromArray(array $options): self
30+
{
31+
// The server requires an empty document for automatic credentials.
32+
if (isset($options[self::KEY_KMS_PROVIDERS]) && is_array($options[self::KEY_KMS_PROVIDERS])) {
33+
foreach ($options[self::KEY_KMS_PROVIDERS] as $name => $provider) {
34+
if ($provider === []) {
35+
$options[self::KEY_KMS_PROVIDERS][$name] = new stdClass();
36+
}
37+
}
38+
}
39+
40+
$keyVaultClient = $options[self::KEY_KEY_VAULT_CLIENT] ?? null;
41+
42+
if ($keyVaultClient !== null && ! $keyVaultClient instanceof Client && ! $keyVaultClient instanceof Manager) {
43+
throw InvalidArgumentException::invalidType(
44+
sprintf('"%s" option', self::KEY_KEY_VAULT_CLIENT),
45+
$keyVaultClient,
46+
[Client::class, Manager::class],
47+
);
48+
}
49+
50+
return new self(
51+
keyVaultClient: $keyVaultClient instanceof Client ? $keyVaultClient->getManager() : $keyVaultClient,
52+
kmsProviders: $options[self::KEY_KMS_PROVIDERS] ?? null,
53+
miscOptions: array_diff_key($options, [self::KEY_KEY_VAULT_CLIENT => 1, self::KEY_KMS_PROVIDERS => 1]),
54+
);
55+
}
56+
57+
public function toArray(): array
58+
{
59+
return array_filter(
60+
[
61+
self::KEY_KEY_VAULT_CLIENT => $this->keyVaultClient,
62+
self::KEY_KMS_PROVIDERS => $this->kmsProviders,
63+
] + $this->miscOptions,
64+
static fn ($option) => $option !== null,
65+
);
66+
}
67+
}

src/Model/DriverOptions.php

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
namespace MongoDB\Model;
4+
5+
use Composer\InstalledVersions;
6+
use MongoDB\Builder\BuilderEncoder;
7+
use MongoDB\Client;
8+
use MongoDB\Codec\Encoder;
9+
use MongoDB\Driver\Manager;
10+
use MongoDB\Exception\InvalidArgumentException;
11+
use stdClass;
12+
use Throwable;
13+
14+
use function array_diff_key;
15+
use function array_filter;
16+
use function is_array;
17+
use function is_string;
18+
use function sprintf;
19+
use function trim;
20+
21+
/** @internal */
22+
final class DriverOptions
23+
{
24+
private const KEY_TYPE_MAP = 'typeMap';
25+
private const KEY_BUILDER_ENCODER = 'builderEncoder';
26+
private const KEY_AUTO_ENCRYPTION = 'autoEncryption';
27+
private const KEY_DRIVER = 'driver';
28+
private const DEFAULT_TYPE_MAP = [
29+
'array' => BSONArray::class,
30+
'document' => BSONDocument::class,
31+
'root' => BSONDocument::class,
32+
];
33+
34+
private const HANDSHAKE_SEPARATOR = '/';
35+
36+
private static ?string $version = null;
37+
38+
public array $driver;
39+
40+
/**
41+
* @param array|null $autoEncryption
42+
* @param array{name?: string, version?: string, platform?: string} $driver
43+
*/
44+
private function __construct(
45+
public readonly array $typeMap,
46+
public readonly Encoder $builderEncoder,
47+
public readonly ?array $autoEncryption,
48+
private readonly array $miscOptions,
49+
array $driver,
50+
) {
51+
$this->driver = $this->mergeDriverInfo($driver);
52+
}
53+
54+
public static function fromArray(array $options): self
55+
{
56+
$options += [self::KEY_TYPE_MAP => self::DEFAULT_TYPE_MAP];
57+
58+
if (! is_array($options[self::KEY_TYPE_MAP])) {
59+
throw InvalidArgumentException::invalidType(
60+
sprintf('"%s" driver option', self::KEY_TYPE_MAP),
61+
$options[self::KEY_TYPE_MAP],
62+
'array',
63+
);
64+
}
65+
66+
if (isset($options[self::KEY_BUILDER_ENCODER]) && ! $options[self::KEY_BUILDER_ENCODER] instanceof Encoder) {
67+
throw InvalidArgumentException::invalidType(
68+
sprintf('"%s" option', self::KEY_BUILDER_ENCODER),
69+
$options[self::KEY_BUILDER_ENCODER],
70+
Encoder::class,
71+
);
72+
}
73+
74+
/** @var array{kmsProviders?: stdClass|array<string, array>, keyVaultClient?: Client|Manager} $autoEncryptionOptions */
75+
$autoEncryptionOptions = $options[self::KEY_AUTO_ENCRYPTION] ?? [];
76+
$autoEncryption = ! empty($autoEncryptionOptions)
77+
? AutoEncryptionOptions::fromArray($autoEncryptionOptions)->toArray()
78+
: null;
79+
80+
/** @var array{name?: string, version?: string, platform?: string} $driver $driver */
81+
$driver = $options[self::KEY_DRIVER] ?? [];
82+
83+
return new self(
84+
typeMap: $options[self::KEY_TYPE_MAP],
85+
builderEncoder: $options[self::KEY_BUILDER_ENCODER] ?? new BuilderEncoder(),
86+
autoEncryption: $autoEncryption,
87+
miscOptions: array_diff_key($options, [
88+
self::KEY_TYPE_MAP => 1,
89+
self::KEY_BUILDER_ENCODER => 1,
90+
self::KEY_AUTO_ENCRYPTION => 1,
91+
self::KEY_DRIVER => 1,
92+
]),
93+
driver: $driver,
94+
);
95+
}
96+
97+
public function isAutoEncryptionEnabled(): bool
98+
{
99+
return isset($this->autoEncryption['keyVaultNamespace']);
100+
}
101+
102+
public function toArray(): array
103+
{
104+
return array_filter(
105+
[
106+
'typeMap' => $this->typeMap,
107+
'builderEncoder' => $this->builderEncoder,
108+
'autoEncryption' => $this->autoEncryption,
109+
'driver' => $this->driver,
110+
] + $this->miscOptions,
111+
static fn ($option) => $option !== null,
112+
);
113+
}
114+
115+
private static function getVersion(): string
116+
{
117+
if (self::$version === null) {
118+
try {
119+
self::$version = InstalledVersions::getPrettyVersion('mongodb/mongodb') ?? 'unknown';
120+
} catch (Throwable) {
121+
self::$version = 'error';
122+
}
123+
}
124+
125+
return self::$version;
126+
}
127+
128+
/** @param array{name?: string, version?: string, platform?: string} $driver */
129+
private function mergeDriverInfo(array $driver): array
130+
{
131+
if (isset($driver['name'])) {
132+
if (! is_string($driver['name'])) {
133+
throw InvalidArgumentException::invalidType(
134+
'"name" handshake option',
135+
$driver['name'],
136+
'string',
137+
);
138+
}
139+
}
140+
141+
if (isset($driver['version'])) {
142+
if (! is_string($driver['version'])) {
143+
throw InvalidArgumentException::invalidType('"version" handshake option', $driver['version'], 'string');
144+
}
145+
}
146+
147+
$mergedDriver = [
148+
'name' => 'PHPLIB',
149+
'version' => self::getVersion(),
150+
];
151+
152+
if (isset($driver['name'])) {
153+
$mergedDriver['name'] .= self::HANDSHAKE_SEPARATOR . $driver['name'];
154+
}
155+
156+
if (isset($driver['version'])) {
157+
$mergedDriver['version'] .= self::HANDSHAKE_SEPARATOR . $driver['version'];
158+
}
159+
160+
if ($this->isAutoEncryptionEnabled()) {
161+
$driver['platform'] = trim(sprintf('iue %s', $driver['platform'] ?? ''));
162+
}
163+
164+
if (isset($driver['platform'])) {
165+
$mergedDriver['platform'] = $driver['platform'];
166+
}
167+
168+
return $mergedDriver;
169+
}
170+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Model;
4+
5+
use Generator;
6+
use MongoDB\Client;
7+
use MongoDB\Exception\InvalidArgumentException;
8+
use MongoDB\Model\AutoEncryptionOptions;
9+
use PHPUnit\Framework\Attributes\DataProvider;
10+
use PHPUnit\Framework\TestCase;
11+
use stdClass;
12+
13+
class AutoEncryptionOptionsTest extends TestCase
14+
{
15+
#[DataProvider('fromArrayProvider')]
16+
public function testFromArray(array $options, array $expected): void
17+
{
18+
$actual = AutoEncryptionOptions::fromArray($options);
19+
$this->assertEquals($expected, $actual->toArray());
20+
}
21+
22+
public function testFromArrayFailsForInvalidOptions(): void
23+
{
24+
$this->expectException(InvalidArgumentException::class);
25+
26+
AutoEncryptionOptions::fromArray([
27+
'keyVaultClient' => new stdClass(),
28+
]);
29+
}
30+
31+
public static function fromArrayProvider(): Generator
32+
{
33+
$client = new Client();
34+
35+
yield 'with manager passed for `keyVaultClient`' => [
36+
[
37+
'keyVaultClient' => $client->getManager(),
38+
'kmsProviders' => new stdClass(),
39+
],
40+
[
41+
'keyVaultClient' => $client->getManager(),
42+
'kmsProviders' => new stdClass(),
43+
],
44+
];
45+
46+
yield 'with client passed for `keyVaultClient`' => [
47+
['keyVaultClient' => $client],
48+
[
49+
'keyVaultClient' => $client->getManager(),
50+
],
51+
];
52+
53+
yield 'with extra options' => [
54+
[
55+
'kmsProviders' => [
56+
'foo' => [],
57+
'aws' => ['foo' => 'bar'],
58+
],
59+
'tlsProviders' => [
60+
['foo' => 'bar'],
61+
],
62+
'disableClientPersistence' => false,
63+
],
64+
[
65+
'kmsProviders' => [
66+
'foo' => new stdClass(),
67+
'aws' => ['foo' => 'bar'],
68+
],
69+
'tlsProviders' => [
70+
['foo' => 'bar'],
71+
],
72+
'disableClientPersistence' => false,
73+
],
74+
];
75+
}
76+
}

0 commit comments

Comments
 (0)