Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 23 additions & 124 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@

namespace MongoDB;

use Composer\InstalledVersions;
use Iterator;
use MongoDB\BSON\Document;
use MongoDB\BSON\PackedArray;
use MongoDB\Builder\BuilderEncoder;
use MongoDB\Builder\Pipeline;
use MongoDB\Codec\Encoder;
use MongoDB\Driver\BulkWriteCommand;
use MongoDB\Driver\BulkWriteCommandResult;
use MongoDB\Driver\ClientEncryption;
Expand All @@ -38,36 +33,22 @@
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnexpectedValueException;
use MongoDB\Exception\UnsupportedException;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
use MongoDB\Model\AutoEncryptionOptions;
use MongoDB\Model\DatabaseInfo;
use MongoDB\Model\DriverOptions;
use MongoDB\Operation\ClientBulkWriteCommand;
use MongoDB\Operation\DropDatabase;
use MongoDB\Operation\ListDatabaseNames;
use MongoDB\Operation\ListDatabases;
use MongoDB\Operation\Watch;
use stdClass;
use Stringable;
use Throwable;

use function array_diff_key;
use function is_array;
use function is_string;

class Client implements Stringable
{
public const DEFAULT_URI = 'mongodb://127.0.0.1/';

private const DEFAULT_TYPE_MAP = [
'array' => BSONArray::class,
'document' => BSONDocument::class,
'root' => BSONDocument::class,
];

private const HANDSHAKE_SEPARATOR = '/';

private static ?string $version = null;

private Manager $manager;

private ReadConcern $readConcern;
Expand All @@ -76,14 +57,9 @@

private string $uri;

private array $typeMap;

/** @psalm-var Encoder<array|stdClass|Document|PackedArray, mixed> */
private readonly Encoder $builderEncoder;

private WriteConcern $writeConcern;

private bool $autoEncryptionEnabled;
private DriverOptions $driverOptions;

/**
* Constructs a new Client instance.
Expand Down Expand Up @@ -113,34 +89,14 @@
*/
public function __construct(?string $uri = null, array $uriOptions = [], array $driverOptions = [])
{
$driverOptions += ['typeMap' => self::DEFAULT_TYPE_MAP];

if (! is_array($driverOptions['typeMap'])) {
throw InvalidArgumentException::invalidType('"typeMap" driver option', $driverOptions['typeMap'], 'array');
}

if (isset($driverOptions['autoEncryption']) && is_array($driverOptions['autoEncryption'])) {
$driverOptions['autoEncryption'] = $this->prepareEncryptionOptions($driverOptions['autoEncryption']);
}

if (isset($driverOptions['builderEncoder']) && ! $driverOptions['builderEncoder'] instanceof Encoder) {
throw InvalidArgumentException::invalidType('"builderEncoder" option', $driverOptions['builderEncoder'], Encoder::class);
}

$driverOptions['driver'] = $this->mergeDriverInfo($driverOptions['driver'] ?? []);
$this->driverOptions = DriverOptions::fromArray($driverOptions);

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

/* Database and Collection objects may need to know whether auto
* encryption is enabled for dropping collections. Track this via an
* internal option until PHPC-2615 is implemented. */
$this->autoEncryptionEnabled = isset($driverOptions['autoEncryption']['keyVaultNamespace']);

$driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]);
$driverOptions = array_diff_key($this->driverOptions->toArray(), ['builderEncoder' => 1, 'typeMap' => 1]);

$this->manager = new Manager($uri, $uriOptions, $driverOptions);

$this->readConcern = $this->manager->getReadConcern();
$this->readPreference = $this->manager->getReadPreference();
$this->writeConcern = $this->manager->getWriteConcern();
Expand All @@ -156,8 +112,8 @@
return [
'manager' => $this->manager,
'uri' => $this->uri,
'typeMap' => $this->typeMap,
'builderEncoder' => $this->builderEncoder,
'typeMap' => $this->driverOptions->typeMap,
'builderEncoder' => $this->driverOptions->builderEncoder,
'writeConcern' => $this->writeConcern,
];
}
Expand Down Expand Up @@ -229,9 +185,9 @@
*/
public function createClientEncryption(array $options): ClientEncryption
{
$options = $this->prepareEncryptionOptions($options);
$options = AutoEncryptionOptions::fromArray($options);

return $this->manager->createClientEncryption($options);
return $this->manager->createClientEncryption($options->toArray());
}

/**
Expand Down Expand Up @@ -268,7 +224,11 @@
*/
public function getCollection(string $databaseName, string $collectionName, array $options = []): Collection
{
$options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder, 'autoEncryptionEnabled' => $this->autoEncryptionEnabled];
$options += [
'typeMap' => $this->driverOptions->typeMap,
'builderEncoder' => $this->driverOptions->builderEncoder,
'autoEncryptionEnabled' => $this->driverOptions->isAutoEncryptionEnabled(),
];

return new Collection($this->manager, $databaseName, $collectionName, $options);
}
Expand All @@ -283,7 +243,11 @@
*/
public function getDatabase(string $databaseName, array $options = []): Database
{
$options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder, 'autoEncryptionEnabled' => $this->autoEncryptionEnabled];
$options += [
'typeMap' => $this->driverOptions->typeMap,
'builderEncoder' => $this->driverOptions->builderEncoder,
'autoEncryptionEnabled' => $this->driverOptions->isAutoEncryptionEnabled(),
];

return new Database($this->manager, $databaseName, $options);
}
Expand Down Expand Up @@ -319,7 +283,7 @@
*/
public function getTypeMap(): array
{
return $this->typeMap;
return $this->driverOptions->typeMap;
}

/**
Expand Down Expand Up @@ -428,7 +392,7 @@
$pipeline = new Pipeline(...$pipeline);
}

$pipeline = $this->builderEncoder->encodeIfSupported($pipeline);
$pipeline = $this->driverOptions->builderEncoder->encodeIfSupported($pipeline);

Check failure on line 395 in src/Client.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedAssignment

src/Client.php:395:9: MixedAssignment: Unable to determine the type that $pipeline is being assigned to (see https://psalm.dev/032)

if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
Expand All @@ -441,76 +405,11 @@
}

if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
$options['typeMap'] = $this->driverOptions->typeMap;
}

$operation = new Watch($this->manager, null, null, $pipeline, $options);

Check failure on line 411 in src/Client.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedArgument

src/Client.php:411:60: MixedArgument: Argument 4 of MongoDB\Operation\Watch::__construct cannot be mixed, expecting array<array-key, mixed> (see https://psalm.dev/030)

return $operation->execute($server);
}

private static function getVersion(): string
{
if (self::$version === null) {
try {
self::$version = InstalledVersions::getPrettyVersion('mongodb/mongodb') ?? 'unknown';
} catch (Throwable) {
self::$version = 'error';
}
}

return self::$version;
}

private function mergeDriverInfo(array $driver): array
{
$mergedDriver = [
'name' => 'PHPLIB',
'version' => self::getVersion(),
];

if (isset($driver['name'])) {
if (! is_string($driver['name'])) {
throw InvalidArgumentException::invalidType('"name" handshake option', $driver['name'], 'string');
}

$mergedDriver['name'] .= self::HANDSHAKE_SEPARATOR . $driver['name'];
}

if (isset($driver['version'])) {
if (! is_string($driver['version'])) {
throw InvalidArgumentException::invalidType('"version" handshake option', $driver['version'], 'string');
}

$mergedDriver['version'] .= self::HANDSHAKE_SEPARATOR . $driver['version'];
}

if (isset($driver['platform'])) {
$mergedDriver['platform'] = $driver['platform'];
}

return $mergedDriver;
}

private function prepareEncryptionOptions(array $options): array
{
if (isset($options['keyVaultClient'])) {
if ($options['keyVaultClient'] instanceof self) {
$options['keyVaultClient'] = $options['keyVaultClient']->manager;
} elseif (! $options['keyVaultClient'] instanceof Manager) {
throw InvalidArgumentException::invalidType('"keyVaultClient" option', $options['keyVaultClient'], [self::class, Manager::class]);
}
}

// The server requires an empty document for automatic credentials.
if (isset($options['kmsProviders']) && is_array($options['kmsProviders'])) {
foreach ($options['kmsProviders'] as $name => $provider) {
if ($provider === []) {
$options['kmsProviders'][$name] = new stdClass();
}
}
}

return $options;
}
}
74 changes: 74 additions & 0 deletions src/Model/AutoEncryptionOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace MongoDB\Model;

use MongoDB\Client;
use MongoDB\Driver\Manager;
use MongoDB\Exception\InvalidArgumentException;
use stdClass;

use function array_diff_key;
use function array_filter;
use function is_array;
use function sprintf;

/** @internal */
final class AutoEncryptionOptions
{
private const KEY_KEY_VAULT_CLIENT = 'keyVaultClient';
private const KEY_KMS_PROVIDERS = 'kmsProviders';

private function __construct(
private readonly ?Manager $keyVaultClient,
private readonly array $kmsProviders,
private readonly array $miscOptions,
) {
}

public static function fromArray(array $options): self
{
self::ensureValidArrayOptions($options);

// The server requires an empty document for automatic credentials.
if (isset($options[self::KEY_KMS_PROVIDERS]) && is_array($options[self::KEY_KMS_PROVIDERS])) {
foreach ($options[self::KEY_KMS_PROVIDERS] as $name => $provider) {

Check failure on line 34 in src/Model/AutoEncryptionOptions.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedAssignment

src/Model/AutoEncryptionOptions.php:34:68: MixedAssignment: Unable to determine the type that $provider is being assigned to (see https://psalm.dev/032)
if ($provider === []) {
$options[self::KEY_KMS_PROVIDERS][$name] = new stdClass();

Check failure on line 36 in src/Model/AutoEncryptionOptions.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedArrayAssignment

src/Model/AutoEncryptionOptions.php:36:21: MixedArrayAssignment: Cannot access array value on mixed variable $options['kmsProviders'][$name] (see https://psalm.dev/117)
}
}
}

$keyVaultClient = $options[self::KEY_KEY_VAULT_CLIENT];

Check failure on line 41 in src/Model/AutoEncryptionOptions.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedAssignment

src/Model/AutoEncryptionOptions.php:41:9: MixedAssignment: Unable to determine the type that $keyVaultClient is being assigned to (see https://psalm.dev/032)

return new self(
keyVaultClient: $keyVaultClient instanceof Client ? $keyVaultClient->getManager() : $keyVaultClient,

Check failure on line 44 in src/Model/AutoEncryptionOptions.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedArgument

src/Model/AutoEncryptionOptions.php:44:29: MixedArgument: Argument 1 of MongoDB\Model\AutoEncryptionOptions::__construct cannot be MongoDB\Driver\Manager|mixed, expecting MongoDB\Driver\Manager|null (see https://psalm.dev/030)
kmsProviders: $options[self::KEY_KMS_PROVIDERS] ?? [],

Check failure on line 45 in src/Model/AutoEncryptionOptions.php

View workflow job for this annotation

GitHub Actions / Psalm

MixedArgument

src/Model/AutoEncryptionOptions.php:45:27: MixedArgument: Argument 2 of MongoDB\Model\AutoEncryptionOptions::__construct cannot be array<never, never>|mixed, expecting array<array-key, mixed> (see https://psalm.dev/030)
miscOptions: array_diff_key($options, [self::KEY_KEY_VAULT_CLIENT => 1, self::KEY_KMS_PROVIDERS => 1]),
);
}

public function toArray(): array
{
return array_filter(
[
self::KEY_KEY_VAULT_CLIENT => $this->keyVaultClient,
self::KEY_KMS_PROVIDERS => $this->kmsProviders,
] + $this->miscOptions,
fn ($option) => $option !== null,
);
}

/** @throws InvalidArgumentException */
private static function ensureValidArrayOptions(array $options): void
{
$keyVaultClient = $options[self::KEY_KEY_VAULT_CLIENT] ?? null;

if ($keyVaultClient !== null && ! $keyVaultClient instanceof Client && ! $keyVaultClient instanceof Manager) {
throw InvalidArgumentException::invalidType(
sprintf('"%s" option', self::KEY_KEY_VAULT_CLIENT),
$keyVaultClient,
[Client::class, Manager::class],
);
}
}
}
Loading
Loading