Skip to content
Merged
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
22 changes: 0 additions & 22 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -217,28 +217,6 @@
<code><![CDATA[$query::NAME]]></code>
</UndefinedConstant>
</file>
<file src="src/Client.php">
<MixedArgument>
<code><![CDATA[$driverOptions['driver'] ?? []]]></code>
<code><![CDATA[$pipeline]]></code>
</MixedArgument>
<MixedArrayAssignment>
<code><![CDATA[$options['kmsProviders'][$name]]]></code>
</MixedArrayAssignment>
<MixedAssignment>
<code><![CDATA[$mergedDriver['platform']]]></code>
<code><![CDATA[$provider]]></code>
</MixedAssignment>
<MixedPropertyTypeCoercion>
<code><![CDATA[$driverOptions['builderEncoder'] ?? new BuilderEncoder()]]></code>
</MixedPropertyTypeCoercion>
<NamedArgumentNotAllowed>
<code><![CDATA[$pipeline]]></code>
</NamedArgumentNotAllowed>
<PossiblyInvalidArgument>
<code><![CDATA[$pipeline]]></code>
</PossiblyInvalidArgument>
</file>
<file src="src/ClientBulkWrite.php">
<PossiblyInvalidArgument>
<code><![CDATA[$document]]></code>
Expand Down
3 changes: 3 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
<!-- This is often the result of type checks due to missing native types -->
<DocblockTypeContradiction errorLevel="info" />

<!-- We still want to check types at runtime for users not using Psalm -->
<RedundantConditionGivenDocblockType errorLevel="info" />

<!-- If the result of getenv is falsy, using the default URI is fine -->
<RiskyTruthyFalsyComparison>
<errorLevel type="suppress">
Expand Down
154 changes: 28 additions & 126 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,24 @@
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;

/** @psalm-import-type stage from Builder\Pipeline */
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 +59,9 @@ class Client implements Stringable

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 +91,14 @@ class Client implements Stringable
*/
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 +114,8 @@ public function __debugInfo(): array
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 @@ -225,13 +183,13 @@ public function bulkWrite(BulkWriteCommand|ClientBulkWrite $bulk, array $options
/**
* Returns a ClientEncryption instance for explicit encryption and decryption
*
* @param array $options Encryption options
* @param array{kmsProviders?: stdClass|array<string, array>, keyVaultClient?: Client|Manager} $options
*/
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 +226,11 @@ public function dropDatabase(string $databaseName, array $options = []): void
*/
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 +245,11 @@ public function getCollection(string $databaseName, string $collectionName, arra
*/
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 +285,7 @@ public function getReadPreference(): ReadPreference
*/
public function getTypeMap(): array
{
return $this->typeMap;
return $this->driverOptions->typeMap;
}

/**
Expand Down Expand Up @@ -418,8 +384,8 @@ public function startSession(array $options = []): Session
* Create a change stream for watching changes to the cluster.
*
* @see Watch::__construct() for supported options
* @param array $pipeline Aggregation pipeline
* @param array $options Command options
* @psalm-param list<stage> $pipeline Aggregation pipeline
* @param array $options Command options
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function watch(array $pipeline = [], array $options = []): ChangeStream
Expand All @@ -428,7 +394,8 @@ public function watch(array $pipeline = [], array $options = []): ChangeStream
$pipeline = new Pipeline(...$pipeline);
}

$pipeline = $this->builderEncoder->encodeIfSupported($pipeline);
/** @var array<array-key, mixed> $pipeline */
$pipeline = $this->driverOptions->builderEncoder->encodeIfSupported($pipeline);

if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
Expand All @@ -441,76 +408,11 @@ public function watch(array $pipeline = [], array $options = []): ChangeStream
}

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

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

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;
}
}
67 changes: 67 additions & 0 deletions src/Model/AutoEncryptionOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?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|stdClass|null $kmsProviders,
private readonly array $miscOptions,
) {
}

/** @param array{kmsProviders?: stdClass|array<string, array>, keyVaultClient?: Client|Manager} $options */
public static function fromArray(array $options): self
{
// 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) {
if ($provider === []) {
$options[self::KEY_KMS_PROVIDERS][$name] = new stdClass();
}
}
}

$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],
);
}

return new self(
keyVaultClient: $keyVaultClient instanceof Client ? $keyVaultClient->getManager() : $keyVaultClient,
kmsProviders: $options[self::KEY_KMS_PROVIDERS] ?? null,
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,
static fn ($option) => $option !== null,
);
}
}
Loading
Loading