From e941b105ac0a8313cbbabc22a2c7e2cf36c1099d Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Fri, 22 Jan 2021 14:39:47 +0100 Subject: [PATCH] MDL-70309 libraries: upgrade mongodb to version 1.8.0 --- cache/stores/mongodb/MongoDB/ChangeStream.php | 40 +++-- cache/stores/mongodb/MongoDB/Client.php | 105 +++++++++++- cache/stores/mongodb/MongoDB/Collection.php | 10 +- .../MongoDB/Command/ListCollections.php | 139 ++++++++++++++++ .../mongodb/MongoDB/Command/ListDatabases.php | 156 ++++++++++++++++++ cache/stores/mongodb/MongoDB/Database.php | 29 +++- .../Exception/InvalidArgumentException.php | 35 +++- .../Exception/ResumeTokenException.php | 4 +- .../Exception/UnsupportedException.php | 31 ++++ .../stores/mongodb/MongoDB/GridFS/Bucket.php | 38 +++-- .../MongoDB/GridFS/CollectionWrapper.php | 46 +++++- .../GridFS/Exception/StreamException.php | 46 ++++++ .../mongodb/MongoDB/GridFS/StreamWrapper.php | 18 +- .../mongodb/MongoDB/GridFS/WritableStream.php | 6 +- .../mongodb/MongoDB/MapReduceResult.php | 4 +- .../mongodb/MongoDB/Model/CachingIterator.php | 35 ++-- .../MongoDB/Model/CallbackIterator.php | 88 ++++++++++ .../MongoDB/Model/ChangeStreamIterator.php | 17 +- .../Model/CollectionInfoCommandIterator.php | 22 ++- .../mongodb/MongoDB/Model/DatabaseInfo.php | 2 +- .../Model/IndexInfoIteratorIterator.php | 23 ++- .../mongodb/MongoDB/Model/IndexInput.php | 8 - .../mongodb/MongoDB/Operation/Aggregate.php | 48 ++++-- .../MongoDB/Operation/CreateIndexes.php | 26 ++- .../mongodb/MongoDB/Operation/Delete.php | 26 +++ .../mongodb/MongoDB/Operation/DeleteMany.php | 7 + .../mongodb/MongoDB/Operation/DeleteOne.php | 7 + .../MongoDB/Operation/DropCollection.php | 17 +- .../mongodb/MongoDB/Operation/Explain.php | 7 + .../mongodb/MongoDB/Operation/Explainable.php | 4 +- .../stores/mongodb/MongoDB/Operation/Find.php | 19 ++- .../MongoDB/Operation/FindAndModify.php | 54 +++++- .../MongoDB/Operation/FindOneAndDelete.php | 7 + .../MongoDB/Operation/FindOneAndReplace.php | 7 + .../MongoDB/Operation/FindOneAndUpdate.php | 7 + .../MongoDB/Operation/ListCollectionNames.php | 79 +++++++++ .../MongoDB/Operation/ListCollections.php | 73 +------- .../MongoDB/Operation/ListDatabaseNames.php | 85 ++++++++++ .../MongoDB/Operation/ListDatabases.php | 76 ++------- .../mongodb/MongoDB/Operation/ListIndexes.php | 2 +- .../mongodb/MongoDB/Operation/MapReduce.php | 47 ++++++ .../MongoDB/Operation/ModifyCollection.php | 2 +- .../mongodb/MongoDB/Operation/ReplaceOne.php | 7 + .../mongodb/MongoDB/Operation/Update.php | 28 +++- .../mongodb/MongoDB/Operation/UpdateMany.php | 7 + .../mongodb/MongoDB/Operation/UpdateOne.php | 7 + .../mongodb/MongoDB/Operation/Watch.php | 6 +- .../MongoDB/Operation/WithTransaction.php | 3 +- 48 files changed, 1286 insertions(+), 274 deletions(-) create mode 100644 cache/stores/mongodb/MongoDB/Command/ListCollections.php create mode 100644 cache/stores/mongodb/MongoDB/Command/ListDatabases.php create mode 100644 cache/stores/mongodb/MongoDB/GridFS/Exception/StreamException.php create mode 100644 cache/stores/mongodb/MongoDB/Model/CallbackIterator.php create mode 100644 cache/stores/mongodb/MongoDB/Operation/ListCollectionNames.php create mode 100644 cache/stores/mongodb/MongoDB/Operation/ListDatabaseNames.php diff --git a/cache/stores/mongodb/MongoDB/ChangeStream.php b/cache/stores/mongodb/MongoDB/ChangeStream.php index 3c360fa8633..fcf16dde866 100644 --- a/cache/stores/mongodb/MongoDB/ChangeStream.php +++ b/cache/stores/mongodb/MongoDB/ChangeStream.php @@ -42,13 +42,33 @@ class ChangeStream implements Iterator */ const CURSOR_NOT_FOUND = 43; - /** @var array */ - private static $nonResumableErrorCodes = [ - 136, // CappedPositionLost - 237, // CursorKilled - 11601, // Interrupted + /** @var int */ + private static $cursorNotFound = 43; + + /** @var int[] */ + private static $resumableErrorCodes = [ + 6, // HostUnreachable + 7, // HostNotFound + 89, // NetworkTimeout + 91, // ShutdownInProgress + 189, // PrimarySteppedDown + 262, // ExceededTimeLimit + 9001, // SocketException + 10107, // NotMaster + 11600, // InterruptedAtShutdown + 11602, // InterruptedDueToReplStateChange + 13435, // NotMasterNoSlaveOk + 13436, // NotMasterOrSecondary + 63, // StaleShardVersion + 150, // StaleEpoch + 13388, // StaleConfig + 234, // RetryChangeStream + 133, // FailedToSatisfyReadPreference ]; + /** @var int */ + private static $wireVersionForResumableChangeStreamError = 9; + /** @var callable */ private $resumeCallable; @@ -180,15 +200,15 @@ class ChangeStream implements Iterator return false; } - if ($exception->hasErrorLabel('NonResumableChangeStreamError')) { - return false; + if ($exception->getCode() === self::$cursorNotFound) { + return true; } - if (in_array($exception->getCode(), self::$nonResumableErrorCodes)) { - return false; + if (server_supports_feature($this->iterator->getServer(), self::$wireVersionForResumableChangeStreamError)) { + return $exception->hasErrorLabel('ResumableChangeStreamError'); } - return true; + return in_array($exception->getCode(), self::$resumableErrorCodes); } /** diff --git a/cache/stores/mongodb/MongoDB/Client.php b/cache/stores/mongodb/MongoDB/Client.php index 20310802d07..f3215d34952 100644 --- a/cache/stores/mongodb/MongoDB/Client.php +++ b/cache/stores/mongodb/MongoDB/Client.php @@ -17,6 +17,9 @@ namespace MongoDB; +use Iterator; +use Jean85\PrettyVersions; +use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Manager; @@ -31,9 +34,12 @@ use MongoDB\Model\BSONArray; use MongoDB\Model\BSONDocument; use MongoDB\Model\DatabaseInfoIterator; use MongoDB\Operation\DropDatabase; +use MongoDB\Operation\ListDatabaseNames; use MongoDB\Operation\ListDatabases; use MongoDB\Operation\Watch; +use Throwable; use function is_array; +use function is_string; class Client { @@ -50,6 +56,12 @@ class Client /** @var integer */ private static $wireVersionForWritableCommandWriteConcern = 5; + /** @var string */ + private static $handshakeSeparator = ' / '; + + /** @var string|null */ + private static $version; + /** @var Manager */ private $manager; @@ -95,12 +107,22 @@ class Client { $driverOptions += ['typeMap' => self::$defaultTypeMap]; - if (isset($driverOptions['typeMap']) && ! is_array($driverOptions['typeMap'])) { + if (! is_array($driverOptions['typeMap'])) { throw InvalidArgumentException::invalidType('"typeMap" driver option', $driverOptions['typeMap'], 'array'); } + if (isset($driverOptions['autoEncryption']['keyVaultClient'])) { + if ($driverOptions['autoEncryption']['keyVaultClient'] instanceof self) { + $driverOptions['autoEncryption']['keyVaultClient'] = $driverOptions['autoEncryption']['keyVaultClient']->manager; + } elseif (! $driverOptions['autoEncryption']['keyVaultClient'] instanceof Manager) { + throw InvalidArgumentException::invalidType('"keyVaultClient" autoEncryption option', $driverOptions['autoEncryption']['keyVaultClient'], [self::class, Manager::class]); + } + } + + $driverOptions['driver'] = $this->mergeDriverInfo($driverOptions['driver'] ?? []); + $this->uri = (string) $uri; - $this->typeMap = isset($driverOptions['typeMap']) ? $driverOptions['typeMap'] : null; + $this->typeMap = $driverOptions['typeMap'] ?? null; unset($driverOptions['typeMap']); @@ -153,6 +175,26 @@ class Client return $this->uri; } + /** + * Returns a ClientEncryption instance for explicit encryption and decryption + * + * @param array $options Encryption options + * + * @return ClientEncryption + */ + public function createClientEncryption(array $options) + { + 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]); + } + } + + return $this->manager->createClientEncryption($options); + } + /** * Drop a database. * @@ -233,6 +275,22 @@ class Client return $this->writeConcern; } + /** + * List database names. + * + * @see ListDatabaseNames::__construct() for supported options + * @throws UnexpectedValueException if the command response was malformed + * @throws InvalidArgumentException for parameter/option parsing errors + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function listDatabaseNames(array $options = []) : Iterator + { + $operation = new ListDatabaseNames($options); + $server = select_server($this->manager, $options); + + return $operation->execute($server); + } + /** * List databases. * @@ -325,4 +383,47 @@ class Client return $operation->execute($server); } + + private static function getVersion() : string + { + if (self::$version === null) { + try { + self::$version = PrettyVersions::getVersion('mongodb/mongodb')->getPrettyVersion(); + } catch (Throwable $t) { + return 'unknown'; + } + } + + 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::$handshakeSeparator . $driver['name']; + } + + if (isset($driver['version'])) { + if (! is_string($driver['version'])) { + throw InvalidArgumentException::invalidType('"version" handshake option', $driver['version'], 'string'); + } + + $mergedDriver['version'] .= self::$handshakeSeparator . $driver['version']; + } + + if (isset($driver['platform'])) { + $mergedDriver['platform'] = $driver['platform']; + } + + return $mergedDriver; + } } diff --git a/cache/stores/mongodb/MongoDB/Collection.php b/cache/stores/mongodb/MongoDB/Collection.php index 82386445b98..a449c7edc55 100644 --- a/cache/stores/mongodb/MongoDB/Collection.php +++ b/cache/stores/mongodb/MongoDB/Collection.php @@ -162,10 +162,10 @@ class Collection $this->manager = $manager; $this->databaseName = (string) $databaseName; $this->collectionName = (string) $collectionName; - $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern(); - $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference(); - $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap; - $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern(); + $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); + $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); + $this->typeMap = $options['typeMap'] ?? self::$defaultTypeMap; + $this->writeConcern = $options['writeConcern'] ?? $this->manager->getWriteConcern(); } /** @@ -356,7 +356,7 @@ class Collection */ public function createIndex($key, array $options = []) { - $commandOptionKeys = ['maxTimeMS' => 1, 'session' => 1, 'writeConcern' => 1]; + $commandOptionKeys = ['commitQuorum' => 1, 'maxTimeMS' => 1, 'session' => 1, 'writeConcern' => 1]; $indexOptions = array_diff_key($options, $commandOptionKeys); $commandOptions = array_intersect_key($options, $commandOptionKeys); diff --git a/cache/stores/mongodb/MongoDB/Command/ListCollections.php b/cache/stores/mongodb/MongoDB/Command/ListCollections.php new file mode 100644 index 00000000000..4f85f13712c --- /dev/null +++ b/cache/stores/mongodb/MongoDB/Command/ListCollections.php @@ -0,0 +1,139 @@ +databaseName = (string) $databaseName; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return CachingIterator + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function execute(Server $server) + { + $cmd = ['listCollections' => 1]; + + if (! empty($this->options['filter'])) { + $cmd['filter'] = (object) $this->options['filter']; + } + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + if (isset($this->options['nameOnly'])) { + $cmd['nameOnly'] = $this->options['nameOnly']; + } + + $cursor = $server->executeReadCommand($this->databaseName, new Command($cmd), $this->createOptions()); + $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); + + return new CachingIterator($cursor); + } + + /** + * Create options for executing the command. + * + * Note: read preference is intentionally omitted, as the spec requires that + * the command be executed on the primary. + * + * @see http://php.net/manual/en/mongodb-driver-server.executecommand.php + * @return array + */ + private function createOptions() + { + $options = []; + + if (isset($this->options['session'])) { + $options['session'] = $this->options['session']; + } + + return $options; + } +} diff --git a/cache/stores/mongodb/MongoDB/Command/ListDatabases.php b/cache/stores/mongodb/MongoDB/Command/ListDatabases.php new file mode 100644 index 00000000000..c9fa58fc869 --- /dev/null +++ b/cache/stores/mongodb/MongoDB/Command/ListDatabases.php @@ -0,0 +1,156 @@ +options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return array An array of database info structures + * @throws UnexpectedValueException if the command response was malformed + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function execute(Server $server) + { + $cmd = ['listDatabases' => 1]; + + if (isset($this->options['authorizedDatabases'])) { + $cmd['authorizedDatabases'] = $this->options['authorizedDatabases']; + } + + if (! empty($this->options['filter'])) { + $cmd['filter'] = (object) $this->options['filter']; + } + + if (isset($this->options['maxTimeMS'])) { + $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + } + + if (isset($this->options['nameOnly'])) { + $cmd['nameOnly'] = $this->options['nameOnly']; + } + + $cursor = $server->executeReadCommand('admin', new Command($cmd), $this->createOptions()); + $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); + $result = current($cursor->toArray()); + + if (! isset($result['databases']) || ! is_array($result['databases'])) { + throw new UnexpectedValueException('listDatabases command did not return a "databases" array'); + } + + return $result['databases']; + } + + /** + * Create options for executing the command. + * + * Note: read preference is intentionally omitted, as the spec requires that + * the command be executed on the primary. + * + * @see http://php.net/manual/en/mongodb-driver-server.executecommand.php + * @return array + */ + private function createOptions() + { + $options = []; + + if (isset($this->options['session'])) { + $options['session'] = $this->options['session']; + } + + return $options; + } +} diff --git a/cache/stores/mongodb/MongoDB/Database.php b/cache/stores/mongodb/MongoDB/Database.php index 323a69bfb34..fadc64be57e 100644 --- a/cache/stores/mongodb/MongoDB/Database.php +++ b/cache/stores/mongodb/MongoDB/Database.php @@ -17,6 +17,7 @@ namespace MongoDB; +use Iterator; use MongoDB\Driver\Cursor; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Manager; @@ -35,6 +36,7 @@ use MongoDB\Operation\CreateCollection; use MongoDB\Operation\DatabaseCommand; use MongoDB\Operation\DropCollection; use MongoDB\Operation\DropDatabase; +use MongoDB\Operation\ListCollectionNames; use MongoDB\Operation\ListCollections; use MongoDB\Operation\ModifyCollection; use MongoDB\Operation\Watch; @@ -129,10 +131,10 @@ class Database $this->manager = $manager; $this->databaseName = (string) $databaseName; - $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern(); - $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference(); - $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap; - $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern(); + $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); + $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); + $this->typeMap = $options['typeMap'] ?? self::$defaultTypeMap; + $this->writeConcern = $options['writeConcern'] ?? $this->manager->getWriteConcern(); } /** @@ -249,10 +251,6 @@ class Database */ public function command($command, array $options = []) { - if (! isset($options['readPreference']) && ! is_in_transaction($options)) { - $options['readPreference'] = $this->readPreference; - } - if (! isset($options['typeMap'])) { $options['typeMap'] = $this->typeMap; } @@ -408,6 +406,21 @@ class Database return $this->writeConcern; } + /** + * Returns the names of all collections in this database + * + * @see ListCollectionNames::__construct() for supported options + * @throws InvalidArgumentException for parameter/option parsing errors + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function listCollectionNames(array $options = []) : Iterator + { + $operation = new ListCollectionNames($this->databaseName, $options); + $server = select_server($this->manager, $options); + + return $operation->execute($server); + } + /** * Returns information for all collections in this database. * diff --git a/cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php b/cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php index e3b44478b27..83271c180ed 100644 --- a/cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php +++ b/cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php @@ -18,9 +18,11 @@ namespace MongoDB\Exception; use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException; -use function get_class; -use function gettype; -use function is_object; +use function array_pop; +use function count; +use function get_debug_type; +use function implode; +use function is_array; use function sprintf; class InvalidArgumentException extends DriverInvalidArgumentException implements Exception @@ -28,13 +30,32 @@ class InvalidArgumentException extends DriverInvalidArgumentException implements /** * Thrown when an argument or option has an invalid type. * - * @param string $name Name of the argument or option - * @param mixed $value Actual value (used to derive the type) - * @param string $expectedType Expected type + * @param string $name Name of the argument or option + * @param mixed $value Actual value (used to derive the type) + * @param string|string[] $expectedType Expected type * @return self */ public static function invalidType($name, $value, $expectedType) { - return new static(sprintf('Expected %s to have type "%s" but found "%s"', $name, $expectedType, is_object($value) ? get_class($value) : gettype($value))); + if (is_array($expectedType)) { + switch (count($expectedType)) { + case 1: + $typeString = array_pop($expectedType); + break; + + case 2: + $typeString = implode('" or "', $expectedType); + break; + + default: + $lastType = array_pop($expectedType); + $typeString = sprintf('%s", or "%s', implode('", "', $expectedType), $lastType); + break; + } + + $expectedType = $typeString; + } + + return new static(sprintf('Expected %s to have type "%s" but found "%s"', $name, $expectedType, get_debug_type($value))); } } diff --git a/cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php b/cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php index 20036470c7e..362e72fa856 100644 --- a/cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php +++ b/cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php @@ -17,7 +17,7 @@ namespace MongoDB\Exception; -use function gettype; +use function get_debug_type; use function sprintf; class ResumeTokenException extends RuntimeException @@ -30,7 +30,7 @@ class ResumeTokenException extends RuntimeException */ public static function invalidType($value) { - return new static(sprintf('Expected resume token to have type "array or object" but found "%s"', gettype($value))); + return new static(sprintf('Expected resume token to have type "array or object" but found "%s"', get_debug_type($value))); } /** diff --git a/cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php b/cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php index 918de9925d0..545580285ab 100644 --- a/cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php +++ b/cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php @@ -19,6 +19,16 @@ namespace MongoDB\Exception; class UnsupportedException extends RuntimeException { + /** + * Thrown when a command's allowDiskUse option is not supported by a server. + * + * @return self + */ + public static function allowDiskUseNotSupported() + { + return new static('The "allowDiskUse" option is not supported by the server executing this operation'); + } + /** * Thrown when array filters are not supported by a server. * @@ -39,6 +49,17 @@ class UnsupportedException extends RuntimeException return new static('Collations are not supported by the server executing this operation'); } + /** + * Thrown when the commitQuorum option for createIndexes is not supported + * by a server. + * + * @return self + */ + public static function commitQuorumNotSupported() + { + return new static('The "commitQuorum" option is not supported by the server executing this operation'); + } + /** * Thrown when explain is not supported by a server. * @@ -49,6 +70,16 @@ class UnsupportedException extends RuntimeException return new static('Explain is not supported by the server executing this operation'); } + /** + * Thrown when a command's hint option is not supported by a server. + * + * @return self + */ + public static function hintNotSupported() + { + return new static('Hint is not supported by the server executing this operation'); + } + /** * Thrown when a command's readConcern option is not supported by a server. * diff --git a/cache/stores/mongodb/MongoDB/GridFS/Bucket.php b/cache/stores/mongodb/MongoDB/GridFS/Bucket.php index dc1532ce8f3..ac963051538 100644 --- a/cache/stores/mongodb/MongoDB/GridFS/Bucket.php +++ b/cache/stores/mongodb/MongoDB/GridFS/Bucket.php @@ -28,6 +28,7 @@ use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnsupportedException; use MongoDB\GridFS\Exception\CorruptFileException; use MongoDB\GridFS\Exception\FileNotFoundException; +use MongoDB\GridFS\Exception\StreamException; use MongoDB\Model\BSONArray; use MongoDB\Model\BSONDocument; use MongoDB\Operation\Find; @@ -143,19 +144,19 @@ class Bucket 'disableMD5' => false, ]; - if (isset($options['bucketName']) && ! is_string($options['bucketName'])) { + if (! is_string($options['bucketName'])) { throw InvalidArgumentException::invalidType('"bucketName" option', $options['bucketName'], 'string'); } - if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) { + if (! is_integer($options['chunkSizeBytes'])) { throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer'); } - if (isset($options['chunkSizeBytes']) && $options['chunkSizeBytes'] < 1) { + if ($options['chunkSizeBytes'] < 1) { throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes'])); } - if (isset($options['disableMD5']) && ! is_bool($options['disableMD5'])) { + if (! is_bool($options['disableMD5'])) { throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean'); } @@ -180,10 +181,10 @@ class Bucket $this->bucketName = $options['bucketName']; $this->chunkSizeBytes = $options['chunkSizeBytes']; $this->disableMD5 = $options['disableMD5']; - $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern(); - $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference(); - $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap; - $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern(); + $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); + $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); + $this->typeMap = $options['typeMap'] ?? self::$defaultTypeMap; + $this->writeConcern = $options['writeConcern'] ?? $this->manager->getWriteConcern(); $collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'typeMap' => 1, 'writeConcern' => 1]); @@ -238,6 +239,7 @@ class Bucket * @param resource $destination Writable Stream * @throws FileNotFoundException if no file could be selected * @throws InvalidArgumentException if $destination is not a stream + * @throws StreamException if the file could not be uploaded * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ public function downloadToStream($id, $destination) @@ -246,7 +248,10 @@ class Bucket throw InvalidArgumentException::invalidType('$destination', $destination, 'resource'); } - stream_copy_to_stream($this->openDownloadStream($id), $destination); + $source = $this->openDownloadStream($id); + if (@stream_copy_to_stream($source, $destination) === false) { + throw StreamException::downloadFromIdFailed($id, $source, $destination); + } } /** @@ -273,6 +278,7 @@ class Bucket * @param array $options Download options * @throws FileNotFoundException if no file could be selected * @throws InvalidArgumentException if $destination is not a stream + * @throws StreamException if the file could not be uploaded * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ public function downloadToStreamByName($filename, $destination, array $options = []) @@ -281,7 +287,10 @@ class Bucket throw InvalidArgumentException::invalidType('$destination', $destination, 'resource'); } - stream_copy_to_stream($this->openDownloadStreamByName($filename, $options), $destination); + $source = $this->openDownloadStreamByName($filename, $options); + if (@stream_copy_to_stream($source, $destination) === false) { + throw StreamException::downloadFromFilenameFailed($filename, $source, $destination); + } } /** @@ -607,6 +616,7 @@ class Bucket * @param array $options Stream options * @return mixed ID of the newly created GridFS file * @throws InvalidArgumentException if $source is not a GridFS stream + * @throws StreamException if the file could not be uploaded * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ public function uploadFromStream($filename, $source, array $options = []) @@ -616,7 +626,11 @@ class Bucket } $destination = $this->openUploadStream($filename, $options); - stream_copy_to_stream($source, $destination); + + if (@stream_copy_to_stream($source, $destination) === false) { + $destinationUri = $this->createPathForFile($this->getRawFileDocumentForStream($destination)); + throw StreamException::uploadFailed($filename, $source, $destinationUri); + } return $this->getFileIdForStream($destination); } @@ -688,7 +702,7 @@ class Bucket $metadata = stream_get_meta_data($stream); if (! isset($metadata['wrapper_data']) || ! $metadata['wrapper_data'] instanceof StreamWrapper) { - throw InvalidArgumentException::invalidType('$stream wrapper data', isset($metadata['wrapper_data']) ? $metadata['wrapper_data'] : null, StreamWrapper::class); + throw InvalidArgumentException::invalidType('$stream wrapper data', $metadata['wrapper_data'] ?? null, StreamWrapper::class); } return $metadata['wrapper_data']->getFile(); diff --git a/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php b/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php index dd4c1533614..0b7f4309183 100644 --- a/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php +++ b/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php @@ -17,14 +17,18 @@ namespace MongoDB\GridFS; +use ArrayIterator; use MongoDB\Collection; use MongoDB\Driver\Cursor; use MongoDB\Driver\Manager; use MongoDB\Driver\ReadPreference; use MongoDB\Exception\InvalidArgumentException; use MongoDB\UpdateResult; +use MultipleIterator; use stdClass; use function abs; +use function count; +use function is_numeric; use function sprintf; /** @@ -289,13 +293,15 @@ class CollectionWrapper */ private function ensureChunksIndex() { + $expectedIndex = ['files_id' => 1, 'n' => 1]; + foreach ($this->chunksCollection->listIndexes() as $index) { - if ($index->isUnique() && $index->getKey() === ['files_id' => 1, 'n' => 1]) { + if ($index->isUnique() && $this->indexKeysMatch($expectedIndex, $index->getKey())) { return; } } - $this->chunksCollection->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]); + $this->chunksCollection->createIndex($expectedIndex, ['unique' => true]); } /** @@ -303,13 +309,15 @@ class CollectionWrapper */ private function ensureFilesIndex() { + $expectedIndex = ['filename' => 1, 'uploadDate' => 1]; + foreach ($this->filesCollection->listIndexes() as $index) { - if ($index->getKey() === ['filename' => 1, 'uploadDate' => 1]) { + if ($this->indexKeysMatch($expectedIndex, $index->getKey())) { return; } } - $this->filesCollection->createIndex(['filename' => 1, 'uploadDate' => 1]); + $this->filesCollection->createIndex($expectedIndex); } /** @@ -334,6 +342,36 @@ class CollectionWrapper $this->ensureChunksIndex(); } + private function indexKeysMatch(array $expectedKeys, array $actualKeys) : bool + { + if (count($expectedKeys) !== count($actualKeys)) { + return false; + } + + $iterator = new MultipleIterator(MultipleIterator::MIT_NEED_ANY); + $iterator->attachIterator(new ArrayIterator($expectedKeys)); + $iterator->attachIterator(new ArrayIterator($actualKeys)); + + foreach ($iterator as $key => $value) { + list($expectedKey, $actualKey) = $key; + list($expectedValue, $actualValue) = $value; + + if ($expectedKey !== $actualKey) { + return false; + } + + /* Since we don't expect special indexes (e.g. text), we mark any + * index with a non-numeric definition as unequal. All others are + * compared against their int value to avoid differences due to + * some drivers using float values in the key specification. */ + if (! is_numeric($actualValue) || (int) $expectedValue !== (int) $actualValue) { + return false; + } + } + + return true; + } + /** * Returns whether the files collection is empty. * diff --git a/cache/stores/mongodb/MongoDB/GridFS/Exception/StreamException.php b/cache/stores/mongodb/MongoDB/GridFS/Exception/StreamException.php new file mode 100644 index 00000000000..53aa9e351ec --- /dev/null +++ b/cache/stores/mongodb/MongoDB/GridFS/Exception/StreamException.php @@ -0,0 +1,46 @@ + $id])); + $sourceMetadata = stream_get_meta_data($source); + $destinationMetadata = stream_get_meta_data($destination); + + return new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS identifier: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $idString)); + } + + /** @param resource $source */ + public static function uploadFailed(string $filename, $source, string $destinationUri) : self + { + $sourceMetadata = stream_get_meta_data($source); + + return new static(sprintf('Uploading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationUri, $filename)); + } +} diff --git a/cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php b/cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php index a79d4d904ec..a5136b349cc 100644 --- a/cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php +++ b/cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php @@ -17,9 +17,9 @@ namespace MongoDB\GridFS; -use Exception; use MongoDB\BSON\UTCDateTime; use stdClass; +use Throwable; use function explode; use function get_class; use function in_array; @@ -57,6 +57,14 @@ class StreamWrapper /** @var ReadableStream|WritableStream|null */ private $stream; + public function __destruct() + { + /* This destructor is a workaround for PHP trying to use the stream well + * after all objects have been destructed. This can cause autoloading + * issues and possibly segmentation faults during PHP shutdown. */ + $this->stream = null; + } + /** * Return the stream's file document. * @@ -88,6 +96,10 @@ class StreamWrapper */ public function stream_close() { + if (! $this->stream) { + return; + } + $this->stream->close(); } @@ -150,7 +162,7 @@ class StreamWrapper try { return $this->stream->readBytes($length); - } catch (Exception $e) { + } catch (Throwable $e) { trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), E_USER_WARNING); return false; @@ -247,7 +259,7 @@ class StreamWrapper try { return $this->stream->writeBytes($data); - } catch (Exception $e) { + } catch (Throwable $e) { trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), E_USER_WARNING); return false; diff --git a/cache/stores/mongodb/MongoDB/GridFS/WritableStream.php b/cache/stores/mongodb/MongoDB/GridFS/WritableStream.php index d7f1e4c47de..2e12285dd46 100644 --- a/cache/stores/mongodb/MongoDB/GridFS/WritableStream.php +++ b/cache/stores/mongodb/MongoDB/GridFS/WritableStream.php @@ -114,15 +114,15 @@ class WritableStream throw InvalidArgumentException::invalidType('"aliases" option', $options['aliases'], 'array of strings'); } - if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) { + if (! is_integer($options['chunkSizeBytes'])) { throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer'); } - if (isset($options['chunkSizeBytes']) && $options['chunkSizeBytes'] < 1) { + if ($options['chunkSizeBytes'] < 1) { throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes'])); } - if (isset($options['disableMD5']) && ! is_bool($options['disableMD5'])) { + if (! is_bool($options['disableMD5'])) { throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean'); } diff --git a/cache/stores/mongodb/MongoDB/MapReduceResult.php b/cache/stores/mongodb/MongoDB/MapReduceResult.php index cda55a57a7f..aaa36b172f8 100644 --- a/cache/stores/mongodb/MongoDB/MapReduceResult.php +++ b/cache/stores/mongodb/MongoDB/MapReduceResult.php @@ -55,8 +55,8 @@ class MapReduceResult implements IteratorAggregate public function __construct(callable $getIterator, stdClass $result) { $this->getIterator = $getIterator; - $this->executionTimeMS = (integer) $result->timeMillis; - $this->counts = (array) $result->counts; + $this->executionTimeMS = isset($result->timeMillis) ? (integer) $result->timeMillis : 0; + $this->counts = isset($result->counts) ? (array) $result->counts : []; $this->timing = isset($result->timing) ? (array) $result->timing : []; } diff --git a/cache/stores/mongodb/MongoDB/Model/CachingIterator.php b/cache/stores/mongodb/MongoDB/Model/CachingIterator.php index ce56efa0da7..3eb94b22dbb 100644 --- a/cache/stores/mongodb/MongoDB/Model/CachingIterator.php +++ b/cache/stores/mongodb/MongoDB/Model/CachingIterator.php @@ -18,8 +18,8 @@ namespace MongoDB\Model; use Countable; -use Generator; use Iterator; +use IteratorIterator; use Traversable; use function count; use function current; @@ -41,7 +41,7 @@ class CachingIterator implements Countable, Iterator /** @var array */ private $items = []; - /** @var Generator */ + /** @var IteratorIterator */ private $iterator; /** @var boolean */ @@ -52,16 +52,17 @@ class CachingIterator implements Countable, Iterator /** * Initialize the iterator and stores the first item in the cache. This - * effectively rewinds the Traversable and the wrapping Generator, which - * will execute up to its first yield statement. Additionally, this mimics - * behavior of the SPL iterators and allows users to omit an explicit call - * to rewind() before using the other methods. + * effectively rewinds the Traversable and the wrapping IteratorIterator. + * Additionally, this mimics behavior of the SPL iterators and allows users + * to omit an explicit call * to rewind() before using the other methods. * * @param Traversable $traversable */ public function __construct(Traversable $traversable) { - $this->iterator = $this->wrapTraversable($traversable); + $this->iterator = new IteratorIterator($traversable); + + $this->iterator->rewind(); $this->storeCurrentItem(); } @@ -101,8 +102,12 @@ class CachingIterator implements Countable, Iterator public function next() { if (! $this->iteratorExhausted) { + $this->iteratorAdvanced = true; $this->iterator->next(); + $this->storeCurrentItem(); + + $this->iteratorExhausted = ! $this->iterator->valid(); } next($this->items); @@ -156,20 +161,4 @@ class CachingIterator implements Countable, Iterator $this->items[$key] = $this->iterator->current(); } - - /** - * Wraps the Traversable with a Generator. - * - * @param Traversable $traversable - * @return Generator - */ - private function wrapTraversable(Traversable $traversable) - { - foreach ($traversable as $key => $value) { - yield $key => $value; - $this->iteratorAdvanced = true; - } - - $this->iteratorExhausted = true; - } } diff --git a/cache/stores/mongodb/MongoDB/Model/CallbackIterator.php b/cache/stores/mongodb/MongoDB/Model/CallbackIterator.php new file mode 100644 index 00000000000..23b7c103521 --- /dev/null +++ b/cache/stores/mongodb/MongoDB/Model/CallbackIterator.php @@ -0,0 +1,88 @@ +iterator = new IteratorIterator($traversable); + $this->callback = $callback; + } + + /** + * @see http://php.net/iterator.current + * @return mixed + */ + public function current() + { + return ($this->callback)($this->iterator->current()); + } + + /** + * @see http://php.net/iterator.key + * @return mixed + */ + public function key() + { + return $this->iterator->key(); + } + + /** + * @see http://php.net/iterator.next + * @return void + */ + public function next() + { + $this->iterator->next(); + } + + /** + * @see http://php.net/iterator.rewind + * @return void + */ + public function rewind() + { + $this->iterator->rewind(); + } + + /** + * @see http://php.net/iterator.valid + * @return boolean + */ + public function valid() + { + return $this->iterator->valid(); + } +} diff --git a/cache/stores/mongodb/MongoDB/Model/ChangeStreamIterator.php b/cache/stores/mongodb/MongoDB/Model/ChangeStreamIterator.php index cf60a369dc3..6f0a1a89d0b 100644 --- a/cache/stores/mongodb/MongoDB/Model/ChangeStreamIterator.php +++ b/cache/stores/mongodb/MongoDB/Model/ChangeStreamIterator.php @@ -24,6 +24,7 @@ use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandSubscriber; use MongoDB\Driver\Monitoring\CommandSucceededEvent; +use MongoDB\Driver\Server; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\ResumeTokenException; use MongoDB\Exception\UnexpectedValueException; @@ -63,6 +64,9 @@ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber /** @var array|object|null */ private $resumeToken; + /** @var Server */ + private $server; + /** * @internal * @param Cursor $cursor @@ -90,6 +94,7 @@ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber $this->isRewindNop = ($firstBatchSize === 0); $this->postBatchResumeToken = $postBatchResumeToken; $this->resumeToken = $initialResumeToken; + $this->server = $cursor->getServer(); } /** @internal */ @@ -152,6 +157,14 @@ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber return $this->resumeToken; } + /** + * Returns the server the cursor is running on. + */ + public function getServer() : Server + { + return $this->server; + } + /** * @see https://php.net/iteratoriterator.key * @return mixed @@ -230,8 +243,8 @@ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber } $resumeToken = is_array($document) - ? (isset($document['_id']) ? $document['_id'] : null) - : (isset($document->_id) ? $document->_id : null); + ? ($document['_id'] ?? null) + : ($document->_id ?? null); if (! isset($resumeToken)) { $this->isValid = false; diff --git a/cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php b/cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php index c4b7b8daef0..5cfa904b64c 100644 --- a/cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php +++ b/cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php @@ -18,6 +18,7 @@ namespace MongoDB\Model; use IteratorIterator; +use Traversable; /** * CollectionInfoIterator for listCollections command results. @@ -32,6 +33,19 @@ use IteratorIterator; */ class CollectionInfoCommandIterator extends IteratorIterator implements CollectionInfoIterator { + /** @var string|null */ + private $databaseName; + + /** + * @param string|null $databaseName + */ + public function __construct(Traversable $iterator, $databaseName = null) + { + parent::__construct($iterator); + + $this->databaseName = $databaseName; + } + /** * Return the current element as a CollectionInfo instance. * @@ -41,6 +55,12 @@ class CollectionInfoCommandIterator extends IteratorIterator implements Collecti */ public function current() { - return new CollectionInfo(parent::current()); + $info = parent::current(); + + if ($this->databaseName !== null && isset($info['idIndex']) && ! isset($info['idIndex']['ns'])) { + $info['idIndex']['ns'] = $this->databaseName . '.' . $info['name']; + } + + return new CollectionInfo($info); } } diff --git a/cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php b/cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php index 9b656113d6d..385b46497ca 100644 --- a/cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php +++ b/cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php @@ -45,7 +45,7 @@ class DatabaseInfo implements ArrayAccess } /** - * Return the collection info as an array. + * Return the database info as an array. * * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo * @return array diff --git a/cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php b/cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php index 7eb3b6b33a3..c02cf1be156 100644 --- a/cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php +++ b/cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php @@ -18,6 +18,8 @@ namespace MongoDB\Model; use IteratorIterator; +use Traversable; +use function array_key_exists; /** * IndexInfoIterator for both listIndexes command and legacy query results. @@ -34,6 +36,19 @@ use IteratorIterator; */ class IndexInfoIteratorIterator extends IteratorIterator implements IndexInfoIterator { + /** @var string|null $ns */ + private $ns; + + /** + * @param string|null $ns + */ + public function __construct(Traversable $iterator, $ns = null) + { + parent::__construct($iterator); + + $this->ns = $ns; + } + /** * Return the current element as an IndexInfo instance. * @@ -43,6 +58,12 @@ class IndexInfoIteratorIterator extends IteratorIterator implements IndexInfoIte */ public function current() { - return new IndexInfo(parent::current()); + $info = parent::current(); + + if (! array_key_exists('ns', $info) && $this->ns !== null) { + $info['ns'] = $this->ns; + } + + return new IndexInfo($info); } } diff --git a/cache/stores/mongodb/MongoDB/Model/IndexInput.php b/cache/stores/mongodb/MongoDB/Model/IndexInput.php index ca85ccab943..d7ec50fc283 100644 --- a/cache/stores/mongodb/MongoDB/Model/IndexInput.php +++ b/cache/stores/mongodb/MongoDB/Model/IndexInput.php @@ -62,14 +62,6 @@ class IndexInput implements Serializable } } - if (! isset($index['ns'])) { - throw new InvalidArgumentException('Required "ns" option is missing from index specification'); - } - - if (! is_string($index['ns'])) { - throw InvalidArgumentException::invalidType('"ns" option', $index['ns'], 'string'); - } - if (! isset($index['name'])) { $index['name'] = generate_index_name($index['key']); } diff --git a/cache/stores/mongodb/MongoDB/Operation/Aggregate.php b/cache/stores/mongodb/MongoDB/Operation/Aggregate.php index fc2e9ee40f9..85df84b4438 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Aggregate.php +++ b/cache/stores/mongodb/MongoDB/Operation/Aggregate.php @@ -48,7 +48,7 @@ use function sprintf; * @see \MongoDB\Collection::aggregate() * @see http://docs.mongodb.org/manual/reference/command/aggregate/ */ -class Aggregate implements Executable +class Aggregate implements Executable, Explainable { /** @var integer */ private static $wireVersionForCollation = 5; @@ -285,9 +285,12 @@ class Aggregate implements Executable } $hasExplain = ! empty($this->options['explain']); - $hasWriteStage = is_last_pipeline_operator_write($this->pipeline); + $hasWriteStage = $this->hasWriteStage(); - $command = $this->createCommand($server, $hasWriteStage); + $command = new Command( + $this->createCommandDocument($server, $hasWriteStage), + $this->createCommandOptions() + ); $options = $this->createOptions($hasWriteStage, $hasExplain); $cursor = $hasWriteStage && ! $hasExplain @@ -315,20 +318,17 @@ class Aggregate implements Executable return new ArrayIterator($result->result); } - /** - * Create the aggregate command. - * - * @param Server $server - * @param boolean $hasWriteStage - * @return Command - */ - private function createCommand(Server $server, $hasWriteStage) + public function getCommandDocument(Server $server) + { + return $this->createCommandDocument($server, $this->hasWriteStage()); + } + + private function createCommandDocument(Server $server, bool $hasWriteStage) : array { $cmd = [ - 'aggregate' => isset($this->collectionName) ? $this->collectionName : 1, + 'aggregate' => $this->collectionName ?? 1, 'pipeline' => $this->pipeline, ]; - $cmdOptions = []; $cmd['allowDiskUse'] = $this->options['allowDiskUse']; @@ -352,10 +352,6 @@ class Aggregate implements Executable $cmd['hint'] = is_array($this->options['hint']) ? (object) $this->options['hint'] : $this->options['hint']; } - if (isset($this->options['maxAwaitTimeMS'])) { - $cmdOptions['maxAwaitTimeMS'] = $this->options['maxAwaitTimeMS']; - } - if ($this->options['useCursor']) { /* Ignore batchSize if pipeline includes an $out or $merge stage, as * no documents will be returned and sending a batchSize of zero @@ -365,7 +361,18 @@ class Aggregate implements Executable : new stdClass(); } - return new Command($cmd, $cmdOptions); + return $cmd; + } + + private function createCommandOptions() : array + { + $cmdOptions = []; + + if (isset($this->options['maxAwaitTimeMS'])) { + $cmdOptions['maxAwaitTimeMS'] = $this->options['maxAwaitTimeMS']; + } + + return $cmdOptions; } /** @@ -399,4 +406,9 @@ class Aggregate implements Executable return $options; } + + private function hasWriteStage() : bool + { + return is_last_pipeline_operator_write($this->pipeline); + } } diff --git a/cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php b/cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php index bf4dea3d6b1..ba53d95b7d1 100644 --- a/cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php +++ b/cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php @@ -28,6 +28,7 @@ use MongoDB\Model\IndexInput; use function array_map; use function is_array; use function is_integer; +use function is_string; use function MongoDB\server_supports_feature; use function sprintf; @@ -47,6 +48,9 @@ class CreateIndexes implements Executable /** @var integer */ private static $wireVersionForWriteConcern = 5; + /** @var integer */ + private static $wireVersionForCommitQuorum = 9; + /** @var string */ private $databaseName; @@ -67,6 +71,10 @@ class CreateIndexes implements Executable * * Supported options: * + * * commitQuorum (integer|string): Specifies how many data-bearing members + * of a replica set, including the primary, must complete the index + * builds successfully before the primary marks the indexes as ready. + * * * maxTimeMS (integer): The maximum amount of time to allow the query to * run. * @@ -102,10 +110,6 @@ class CreateIndexes implements Executable throw InvalidArgumentException::invalidType(sprintf('$index[%d]', $i), $index, 'array'); } - if (! isset($index['ns'])) { - $index['ns'] = $databaseName . '.' . $collectionName; - } - if (isset($index['collation'])) { $this->isCollationUsed = true; } @@ -115,6 +119,10 @@ class CreateIndexes implements Executable $expectedIndex += 1; } + if (isset($options['commitQuorum']) && ! is_string($options['commitQuorum']) && ! is_integer($options['commitQuorum'])) { + throw InvalidArgumentException::invalidType('"commitQuorum" option', $options['commitQuorum'], ['integer', 'string']); + } + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); } @@ -202,6 +210,16 @@ class CreateIndexes implements Executable 'indexes' => $this->indexes, ]; + if (isset($this->options['commitQuorum'])) { + /* Drivers MUST manually raise an error if this option is specified + * when creating an index on a pre 4.4 server. */ + if (! server_supports_feature($server, self::$wireVersionForCommitQuorum)) { + throw UnsupportedException::commitQuorumNotSupported(); + } + + $cmd['commitQuorum'] = $this->options['commitQuorum']; + } + if (isset($this->options['maxTimeMS'])) { $cmd['maxTimeMS'] = $this->options['maxTimeMS']; } diff --git a/cache/stores/mongodb/MongoDB/Operation/Delete.php b/cache/stores/mongodb/MongoDB/Operation/Delete.php index 181351b0e97..65cc6f6e933 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Delete.php +++ b/cache/stores/mongodb/MongoDB/Operation/Delete.php @@ -27,6 +27,7 @@ use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnsupportedException; use function is_array; use function is_object; +use function is_string; use function MongoDB\server_supports_feature; /** @@ -43,6 +44,9 @@ class Delete implements Executable, Explainable /** @var integer */ private static $wireVersionForCollation = 5; + /** @var int */ + private static $wireVersionForHintServerSideError = 5; + /** @var string */ private $databaseName; @@ -68,6 +72,13 @@ class Delete implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.4 and will result in an + * exception at execution time if used. + * * * session (MongoDB\Driver\Session): Client session. * * Sessions are not supported for server versions < 3.6. @@ -97,6 +108,10 @@ class Delete implements Executable, Explainable throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object'); } + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) { + throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']); + } + if (isset($options['session']) && ! $options['session'] instanceof Session) { throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class); } @@ -130,6 +145,13 @@ class Delete implements Executable, Explainable throw UnsupportedException::collationNotSupported(); } + /* Server versions >= 3.4.0 raise errors for unknown update + * options. For previous versions, the CRUD spec requires a client-side + * error. */ + if (isset($this->options['hint']) && ! server_supports_feature($server, self::$wireVersionForHintServerSideError)) { + throw UnsupportedException::hintNotSupported(); + } + $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); if ($inTransaction && isset($this->options['writeConcern'])) { throw UnsupportedException::writeConcernNotSupportedInTransaction(); @@ -170,6 +192,10 @@ class Delete implements Executable, Explainable $deleteOptions['collation'] = (object) $this->options['collation']; } + if (isset($this->options['hint'])) { + $deleteOptions['hint'] = $this->options['hint']; + } + return $deleteOptions; } diff --git a/cache/stores/mongodb/MongoDB/Operation/DeleteMany.php b/cache/stores/mongodb/MongoDB/Operation/DeleteMany.php index 96c8031e9cc..ffda516ca8e 100644 --- a/cache/stores/mongodb/MongoDB/Operation/DeleteMany.php +++ b/cache/stores/mongodb/MongoDB/Operation/DeleteMany.php @@ -45,6 +45,13 @@ class DeleteMany implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.4 and will result in an + * exception at execution time if used. + * * * session (MongoDB\Driver\Session): Client session. * * Sessions are not supported for server versions < 3.6. diff --git a/cache/stores/mongodb/MongoDB/Operation/DeleteOne.php b/cache/stores/mongodb/MongoDB/Operation/DeleteOne.php index 3377a66d8a6..742a58f73f9 100644 --- a/cache/stores/mongodb/MongoDB/Operation/DeleteOne.php +++ b/cache/stores/mongodb/MongoDB/Operation/DeleteOne.php @@ -45,6 +45,13 @@ class DeleteOne implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.4 and will result in an + * exception at execution time if used. + * * * session (MongoDB\Driver\Session): Client session. * * Sessions are not supported for server versions < 3.6. diff --git a/cache/stores/mongodb/MongoDB/Operation/DropCollection.php b/cache/stores/mongodb/MongoDB/Operation/DropCollection.php index fb328cba10f..10f16e1edaf 100644 --- a/cache/stores/mongodb/MongoDB/Operation/DropCollection.php +++ b/cache/stores/mongodb/MongoDB/Operation/DropCollection.php @@ -18,7 +18,7 @@ namespace MongoDB\Operation; use MongoDB\Driver\Command; -use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; +use MongoDB\Driver\Exception\CommandException; use MongoDB\Driver\Server; use MongoDB\Driver\Session; use MongoDB\Driver\WriteConcern; @@ -38,6 +38,9 @@ use function MongoDB\server_supports_feature; */ class DropCollection implements Executable { + /** @var integer */ + private static $errorCodeNamespaceNotFound = 26; + /** @var string */ private static $errorMessageNamespaceNotFound = 'ns not found'; @@ -122,13 +125,13 @@ class DropCollection implements Executable try { $cursor = $server->executeWriteCommand($this->databaseName, $command, $this->createOptions()); - } catch (DriverRuntimeException $e) { + } catch (CommandException $e) { /* The server may return an error if the collection does not exist. - * Check for an error message (unfortunately, there isn't a code) - * and NOP instead of throwing. - */ - if ($e->getMessage() === self::$errorMessageNamespaceNotFound) { - return (object) ['ok' => 0, 'errmsg' => self::$errorMessageNamespaceNotFound]; + * Check for an error code (or message for pre-3.2 servers) and + * return the command reply instead of throwing. */ + if ($e->getCode() === self::$errorCodeNamespaceNotFound || + $e->getMessage() === self::$errorMessageNamespaceNotFound) { + return $e->getResultDocument(); } throw $e; diff --git a/cache/stores/mongodb/MongoDB/Operation/Explain.php b/cache/stores/mongodb/MongoDB/Operation/Explain.php index 28a23390748..aa240bb51cf 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Explain.php +++ b/cache/stores/mongodb/MongoDB/Operation/Explain.php @@ -41,6 +41,9 @@ class Explain implements Executable const VERBOSITY_EXEC_STATS = 'executionStats'; const VERBOSITY_QUERY = 'queryPlanner'; + /** @var integer */ + private static $wireVersionForAggregate = 7; + /** @var integer */ private static $wireVersionForDistinct = 4; @@ -108,6 +111,10 @@ class Explain implements Executable throw UnsupportedException::explainNotSupported(); } + if ($this->explainable instanceof Aggregate && ! server_supports_feature($server, self::$wireVersionForAggregate)) { + throw UnsupportedException::explainNotSupported(); + } + $cmd = ['explain' => $this->explainable->getCommandDocument($server)]; if (isset($this->options['verbosity'])) { diff --git a/cache/stores/mongodb/MongoDB/Operation/Explainable.php b/cache/stores/mongodb/MongoDB/Operation/Explainable.php index ed4e99ab4e8..fb531f23997 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Explainable.php +++ b/cache/stores/mongodb/MongoDB/Operation/Explainable.php @@ -20,8 +20,8 @@ namespace MongoDB\Operation; use MongoDB\Driver\Server; /** - * Explainable interface for explainable operations (count, distinct, find, - * findAndModify, delete, and update). + * Explainable interface for explainable operations (aggregate, count, distinct, + * find, findAndModify, delete, and update). * * @internal */ diff --git a/cache/stores/mongodb/MongoDB/Operation/Find.php b/cache/stores/mongodb/MongoDB/Operation/Find.php index f6e11aba86a..4555ebd5d77 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Find.php +++ b/cache/stores/mongodb/MongoDB/Operation/Find.php @@ -55,6 +55,9 @@ class Find implements Executable, Explainable /** @var integer */ private static $wireVersionForReadConcern = 4; + /** @var integer */ + private static $wireVersionForAllowDiskUseServerSideError = 4; + /** @var string */ private $databaseName; @@ -72,6 +75,10 @@ class Find implements Executable, Explainable * * Supported options: * + * * allowDiskUse (boolean): Enables writing to temporary files. When set + * to true, queries can write data to the _tmp sub-directory in the + * dbPath directory. The default is false. + * * * allowPartialResults (boolean): Get partial results from a mongos if * some shards are inaccessible (instead of throwing an error). * @@ -120,7 +127,7 @@ class Find implements Executable, Explainable * Set this option to prevent that. * * * oplogReplay (boolean): Internal replication use only. The driver - * should not set this. + * should not set this. This option is deprecated as of MongoDB 4.4. * * * projection (document): Limits the fields to return for the matching * document. @@ -169,6 +176,10 @@ class Find implements Executable, Explainable throw InvalidArgumentException::invalidType('$filter', $filter, 'array or object'); } + if (isset($options['allowDiskUse']) && ! is_bool($options['allowDiskUse'])) { + throw InvalidArgumentException::invalidType('"allowDiskUse" option', $options['allowDiskUse'], 'boolean'); + } + if (isset($options['allowPartialResults']) && ! is_bool($options['allowPartialResults'])) { throw InvalidArgumentException::invalidType('"allowPartialResults" option', $options['allowPartialResults'], 'boolean'); } @@ -314,6 +325,10 @@ class Find implements Executable, Explainable throw UnsupportedException::readConcernNotSupported(); } + if (isset($this->options['allowDiskUse']) && ! server_supports_feature($server, self::$wireVersionForAllowDiskUseServerSideError)) { + throw UnsupportedException::allowDiskUseNotSupported(); + } + $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); if ($inTransaction && isset($this->options['readConcern'])) { throw UnsupportedException::readConcernNotSupportedInTransaction(); @@ -416,7 +431,7 @@ class Find implements Executable, Explainable } } - foreach (['allowPartialResults', 'batchSize', 'comment', 'hint', 'limit', 'maxAwaitTimeMS', 'maxScan', 'maxTimeMS', 'noCursorTimeout', 'oplogReplay', 'projection', 'readConcern', 'returnKey', 'showRecordId', 'skip', 'snapshot', 'sort'] as $option) { + foreach (['allowDiskUse', 'allowPartialResults', 'batchSize', 'comment', 'hint', 'limit', 'maxAwaitTimeMS', 'maxScan', 'maxTimeMS', 'noCursorTimeout', 'oplogReplay', 'projection', 'readConcern', 'returnKey', 'showRecordId', 'skip', 'snapshot', 'sort'] as $option) { if (isset($this->options[$option])) { $options[$option] = $this->options[$option]; } diff --git a/cache/stores/mongodb/MongoDB/Operation/FindAndModify.php b/cache/stores/mongodb/MongoDB/Operation/FindAndModify.php index acce4366b03..9e1e5949d27 100644 --- a/cache/stores/mongodb/MongoDB/Operation/FindAndModify.php +++ b/cache/stores/mongodb/MongoDB/Operation/FindAndModify.php @@ -30,6 +30,7 @@ use function is_array; use function is_bool; use function is_integer; use function is_object; +use function is_string; use function MongoDB\create_field_path_type_map; use function MongoDB\is_pipeline; use function MongoDB\server_supports_feature; @@ -54,6 +55,12 @@ class FindAndModify implements Executable, Explainable /** @var integer */ private static $wireVersionForDocumentLevelValidation = 4; + /** @var integer */ + private static $wireVersionForHint = 9; + + /** @var integer */ + private static $wireVersionForHintServerSideError = 8; + /** @var integer */ private static $wireVersionForWriteConcern = 4; @@ -91,6 +98,13 @@ class FindAndModify implements Executable, Explainable * * fields (document): Limits the fields to return for the matching * document. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is only supported on server versions >= 4.4. Using this option in + * other contexts will result in an exception at execution time. + * * * maxTimeMS (integer): The maximum amount of time to allow the query to * run. * @@ -153,6 +167,10 @@ class FindAndModify implements Executable, Explainable throw InvalidArgumentException::invalidType('"fields" option', $options['fields'], 'array or object'); } + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) { + throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']); + } + if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); } @@ -226,6 +244,14 @@ class FindAndModify implements Executable, Explainable throw UnsupportedException::collationNotSupported(); } + /* Server versions >= 4.1.10 raise errors for unknown findAndModify + * options (SERVER-40005), but the CRUD spec requires client-side errors + * for server versions < 4.2. For later versions, we'll rely on the + * server to either utilize the option or report its own error. */ + if (isset($this->options['hint']) && ! $this->isHintSupported($server)) { + throw UnsupportedException::hintNotSupported(); + } + if (isset($this->options['writeConcern']) && ! server_supports_feature($server, self::$wireVersionForWriteConcern)) { throw UnsupportedException::writeConcernNotSupported(); } @@ -243,7 +269,7 @@ class FindAndModify implements Executable, Explainable $result = current($cursor->toArray()); - return isset($result->value) ? $result->value : null; + return $result->value ?? null; } public function getCommandDocument(Server $server) @@ -280,12 +306,10 @@ class FindAndModify implements Executable, Explainable : (object) $this->options['update']; } - if (isset($this->options['arrayFilters'])) { - $cmd['arrayFilters'] = $this->options['arrayFilters']; - } - - if (isset($this->options['maxTimeMS'])) { - $cmd['maxTimeMS'] = $this->options['maxTimeMS']; + foreach (['arrayFilters', 'hint', 'maxTimeMS'] as $option) { + if (isset($this->options[$option])) { + $cmd[$option] = $this->options[$option]; + } } if (! empty($this->options['bypassDocumentValidation']) && @@ -317,4 +341,20 @@ class FindAndModify implements Executable, Explainable return $options; } + + private function isAcknowledgedWriteConcern() : bool + { + if (! isset($this->options['writeConcern'])) { + return true; + } + + return $this->options['writeConcern']->getW() > 1 || $this->options['writeConcern']->getJournal(); + } + + private function isHintSupported(Server $server) : bool + { + $requiredWireVersion = $this->isAcknowledgedWriteConcern() ? self::$wireVersionForHintServerSideError : self::$wireVersionForHint; + + return server_supports_feature($server, $requiredWireVersion); + } } diff --git a/cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php b/cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php index 489ca3e95f0..88ed57f7eb6 100644 --- a/cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php +++ b/cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php @@ -46,6 +46,13 @@ class FindOneAndDelete implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.4 and will result in an + * exception at execution time if used. + * * * maxTimeMS (integer): The maximum amount of time to allow the query to * run. * diff --git a/cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php b/cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php index 43721c37257..61256f51844 100644 --- a/cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php +++ b/cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php @@ -57,6 +57,13 @@ class FindOneAndReplace implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.4 and will result in an + * exception at execution time if used. + * * * maxTimeMS (integer): The maximum amount of time to allow the query to * run. * diff --git a/cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php b/cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php index ed38e0c2772..c8121a9b50b 100644 --- a/cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php +++ b/cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php @@ -61,6 +61,13 @@ class FindOneAndUpdate implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.4 and will result in an + * exception at execution time if used. + * * * maxTimeMS (integer): The maximum amount of time to allow the query to * run. * diff --git a/cache/stores/mongodb/MongoDB/Operation/ListCollectionNames.php b/cache/stores/mongodb/MongoDB/Operation/ListCollectionNames.php new file mode 100644 index 00000000000..e36bf5688a4 --- /dev/null +++ b/cache/stores/mongodb/MongoDB/Operation/ListCollectionNames.php @@ -0,0 +1,79 @@ +listCollections = new ListCollectionsCommand($databaseName, ['nameOnly' => true] + $options); + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return Iterator + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function execute(Server $server) : Iterator + { + return new CallbackIterator( + $this->listCollections->execute($server), + function (array $collectionInfo) { + return $collectionInfo['name']; + } + ); + } +} diff --git a/cache/stores/mongodb/MongoDB/Operation/ListCollections.php b/cache/stores/mongodb/MongoDB/Operation/ListCollections.php index 1d1fdf5612b..838202cbba7 100644 --- a/cache/stores/mongodb/MongoDB/Operation/ListCollections.php +++ b/cache/stores/mongodb/MongoDB/Operation/ListCollections.php @@ -17,17 +17,12 @@ namespace MongoDB\Operation; -use MongoDB\Driver\Command; +use MongoDB\Command\ListCollections as ListCollectionsCommand; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Server; -use MongoDB\Driver\Session; use MongoDB\Exception\InvalidArgumentException; -use MongoDB\Model\CachingIterator; use MongoDB\Model\CollectionInfoCommandIterator; use MongoDB\Model\CollectionInfoIterator; -use function is_array; -use function is_integer; -use function is_object; /** * Operation for the listCollections command. @@ -41,8 +36,8 @@ class ListCollections implements Executable /** @var string */ private $databaseName; - /** @var array */ - private $options; + /** @var ListCollectionsCommand */ + private $listCollections; /** * Constructs a listCollections command. @@ -64,20 +59,8 @@ class ListCollections implements Executable */ public function __construct($databaseName, array $options = []) { - if (isset($options['filter']) && ! is_array($options['filter']) && ! is_object($options['filter'])) { - throw InvalidArgumentException::invalidType('"filter" option', $options['filter'], 'array or object'); - } - - if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { - throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); - } - - if (isset($options['session']) && ! $options['session'] instanceof Session) { - throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class); - } - $this->databaseName = (string) $databaseName; - $this->options = $options; + $this->listCollections = new ListCollectionsCommand($databaseName, ['nameOnly' => false] + $options); } /** @@ -90,52 +73,6 @@ class ListCollections implements Executable */ public function execute(Server $server) { - return $this->executeCommand($server); - } - - /** - * Create options for executing the command. - * - * Note: read preference is intentionally omitted, as the spec requires that - * the command be executed on the primary. - * - * @see http://php.net/manual/en/mongodb-driver-server.executecommand.php - * @return array - */ - private function createOptions() - { - $options = []; - - if (isset($this->options['session'])) { - $options['session'] = $this->options['session']; - } - - return $options; - } - - /** - * Returns information for all collections in this database using the - * listCollections command. - * - * @param Server $server - * @return CollectionInfoCommandIterator - * @throws DriverRuntimeException for other driver errors (e.g. connection errors) - */ - private function executeCommand(Server $server) - { - $cmd = ['listCollections' => 1]; - - if (! empty($this->options['filter'])) { - $cmd['filter'] = (object) $this->options['filter']; - } - - if (isset($this->options['maxTimeMS'])) { - $cmd['maxTimeMS'] = $this->options['maxTimeMS']; - } - - $cursor = $server->executeReadCommand($this->databaseName, new Command($cmd), $this->createOptions()); - $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); - - return new CollectionInfoCommandIterator(new CachingIterator($cursor)); + return new CollectionInfoCommandIterator($this->listCollections->execute($server), $this->databaseName); } } diff --git a/cache/stores/mongodb/MongoDB/Operation/ListDatabaseNames.php b/cache/stores/mongodb/MongoDB/Operation/ListDatabaseNames.php new file mode 100644 index 00000000000..a9d5a8a874e --- /dev/null +++ b/cache/stores/mongodb/MongoDB/Operation/ListDatabaseNames.php @@ -0,0 +1,85 @@ +listDatabases = new ListDatabasesCommand(['nameOnly' => true] + $options); + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return Iterator + * @throws UnexpectedValueException if the command response was malformed + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function execute(Server $server) : Iterator + { + $result = $this->listDatabases->execute($server); + + return new ArrayIterator(array_column($result, 'name')); + } +} diff --git a/cache/stores/mongodb/MongoDB/Operation/ListDatabases.php b/cache/stores/mongodb/MongoDB/Operation/ListDatabases.php index c405236917b..66542966f30 100644 --- a/cache/stores/mongodb/MongoDB/Operation/ListDatabases.php +++ b/cache/stores/mongodb/MongoDB/Operation/ListDatabases.php @@ -17,18 +17,13 @@ namespace MongoDB\Operation; -use MongoDB\Driver\Command; +use MongoDB\Command\ListDatabases as ListDatabasesCommand; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Server; -use MongoDB\Driver\Session; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnexpectedValueException; use MongoDB\Model\DatabaseInfoIterator; use MongoDB\Model\DatabaseInfoLegacyIterator; -use function current; -use function is_array; -use function is_integer; -use function is_object; /** * Operation for the ListDatabases command. @@ -39,14 +34,19 @@ use function is_object; */ class ListDatabases implements Executable { - /** @var array */ - private $options; + /** @var ListDatabasesCommand */ + private $listDatabases; /** * Constructs a listDatabases command. * * Supported options: * + * * authorizedDatabases (boolean): Determines which databases are returned + * based on the user privileges. + * + * For servers < 4.0.5, this option is ignored. + * * * filter (document): Query by which to filter databases. * * For servers < 3.6, this option is ignored. @@ -63,19 +63,7 @@ class ListDatabases implements Executable */ public function __construct(array $options = []) { - if (isset($options['filter']) && ! is_array($options['filter']) && ! is_object($options['filter'])) { - throw InvalidArgumentException::invalidType('"filter" option', $options['filter'], 'array or object'); - } - - if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { - throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); - } - - if (isset($options['session']) && ! $options['session'] instanceof Session) { - throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class); - } - - $this->options = $options; + $this->listDatabases = new ListDatabasesCommand(['nameOnly' => false] + $options); } /** @@ -89,50 +77,6 @@ class ListDatabases implements Executable */ public function execute(Server $server) { - $cmd = ['listDatabases' => 1]; - - if (! empty($this->options['filter'])) { - $cmd['filter'] = (object) $this->options['filter']; - } - - if (isset($this->options['maxTimeMS'])) { - $cmd['maxTimeMS'] = $this->options['maxTimeMS']; - } - - $cursor = $server->executeReadCommand('admin', new Command($cmd), $this->createOptions()); - $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); - $result = current($cursor->toArray()); - - if (! isset($result['databases']) || ! is_array($result['databases'])) { - throw new UnexpectedValueException('listDatabases command did not return a "databases" array'); - } - - /* Return an Iterator instead of an array in case listDatabases is - * eventually changed to return a command cursor, like the collection - * and index enumeration commands. This makes the "totalSize" command - * field inaccessible, but users can manually invoke the command if they - * need that value. - */ - return new DatabaseInfoLegacyIterator($result['databases']); - } - - /** - * Create options for executing the command. - * - * Note: read preference is intentionally omitted, as the spec requires that - * the command be executed on the primary. - * - * @see http://php.net/manual/en/mongodb-driver-server.executecommand.php - * @return array - */ - private function createOptions() - { - $options = []; - - if (isset($this->options['session'])) { - $options['session'] = $this->options['session']; - } - - return $options; + return new DatabaseInfoLegacyIterator($this->listDatabases->execute($server)); } } diff --git a/cache/stores/mongodb/MongoDB/Operation/ListIndexes.php b/cache/stores/mongodb/MongoDB/Operation/ListIndexes.php index 96aa5b95d11..9848a8847da 100644 --- a/cache/stores/mongodb/MongoDB/Operation/ListIndexes.php +++ b/cache/stores/mongodb/MongoDB/Operation/ListIndexes.php @@ -149,6 +149,6 @@ class ListIndexes implements Executable $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); - return new IndexInfoIteratorIterator(new CachingIterator($cursor)); + return new IndexInfoIteratorIterator(new CachingIterator($cursor), $this->databaseName . '.' . $this->collectionName); } } diff --git a/cache/stores/mongodb/MongoDB/Operation/MapReduce.php b/cache/stores/mongodb/MongoDB/Operation/MapReduce.php index 22c357dc6ea..c1bf6926aa6 100644 --- a/cache/stores/mongodb/MongoDB/Operation/MapReduce.php +++ b/cache/stores/mongodb/MongoDB/Operation/MapReduce.php @@ -40,6 +40,8 @@ use function is_string; use function MongoDB\create_field_path_type_map; use function MongoDB\is_mapreduce_output_inline; use function MongoDB\server_supports_feature; +use function trigger_error; +use const E_USER_DEPRECATED; /** * Operation for the mapReduce command. @@ -88,9 +90,15 @@ class MapReduce implements Executable * * map (MongoDB\BSON\Javascript): A JavaScript function that associates * or "maps" a value with a key and emits the key and value pair. * + * Passing a Javascript instance with a scope is deprecated. Put all + * scope variables in the "scope" option of the MapReduce operation. + * * * reduce (MongoDB\BSON\Javascript): A JavaScript function that "reduces" * to a single object all the values associated with a particular key. * + * Passing a Javascript instance with a scope is deprecated. Put all + * scope variables in the "scope" option of the MapReduce operation. + * * * out (string|document): Specifies where to output the result of the * map-reduce operation. You can either output to a collection or return * the result inline. On a primary member of a replica set you can output @@ -114,6 +122,9 @@ class MapReduce implements Executable * * finalize (MongoDB\BSON\JavascriptInterface): Follows the reduce method * and modifies the output. * + * Passing a Javascript instance with a scope is deprecated. Put all + * scope variables in the "scope" option of the MapReduce operation. + * * * jsMode (boolean): Specifies whether to convert intermediate data into * BSON format between the execution of the map and reduce functions. * @@ -242,6 +253,21 @@ class MapReduce implements Executable unset($options['writeConcern']); } + // Handle deprecation of CodeWScope + if ($map->getScope() !== null) { + @trigger_error('Use of Javascript with scope in "$map" argument for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); + } + + if ($reduce->getScope() !== null) { + @trigger_error('Use of Javascript with scope in "$reduce" argument for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); + } + + if (isset($options['finalize']) && $options['finalize']->getScope() !== null) { + @trigger_error('Use of Javascript with scope in "finalize" option for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); + } + + $this->checkOutDeprecations($out); + $this->databaseName = (string) $databaseName; $this->collectionName = (string) $collectionName; $this->map = $map; @@ -310,6 +336,27 @@ class MapReduce implements Executable return new MapReduceResult($getIterator, $result); } + /** + * @param string|array|object $out + * @return void + */ + private function checkOutDeprecations($out) + { + if (is_string($out)) { + return; + } + + $out = (array) $out; + + if (isset($out['nonAtomic']) && ! $out['nonAtomic']) { + @trigger_error('Specifying false for "out.nonAtomic" is deprecated.', E_USER_DEPRECATED); + } + + if (isset($out['sharded']) && ! $out['sharded']) { + @trigger_error('Specifying false for "out.sharded" is deprecated.', E_USER_DEPRECATED); + } + } + /** * Create the mapReduce command. * diff --git a/cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php b/cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php index 0dc01115427..3b5177fd5db 100644 --- a/cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php +++ b/cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php @@ -126,7 +126,7 @@ class ModifyCollection implements Executable /** * Create options for executing the command. * - * @see http://php.net/manual/en/mongodb-driver-server.executereadwritecommand.php + * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php * @return array */ private function createOptions() diff --git a/cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php b/cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php index cbbda7ff289..e55bf74afb7 100644 --- a/cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php +++ b/cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php @@ -55,6 +55,13 @@ class ReplaceOne implements Executable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.2 and will result in an + * exception at execution time if used. + * * * session (MongoDB\Driver\Session): Client session. * * Sessions are not supported for server versions < 3.6. diff --git a/cache/stores/mongodb/MongoDB/Operation/Update.php b/cache/stores/mongodb/MongoDB/Operation/Update.php index e4ee30dbc0b..b3b86e0c673 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Update.php +++ b/cache/stores/mongodb/MongoDB/Operation/Update.php @@ -28,6 +28,7 @@ use MongoDB\UpdateResult; use function is_array; use function is_bool; use function is_object; +use function is_string; use function MongoDB\is_first_key_operator; use function MongoDB\is_pipeline; use function MongoDB\server_supports_feature; @@ -52,6 +53,9 @@ class Update implements Executable, Explainable /** @var integer */ private static $wireVersionForDocumentLevelValidation = 4; + /** @var integer */ + private static $wireVersionForHintServerSideError = 5; + /** @var string */ private $databaseName; @@ -89,6 +93,13 @@ class Update implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.2 and will result in an + * exception at execution time if used. + * * * multi (boolean): When true, updates all documents matching the query. * This option cannot be true if the $update argument is a replacement * document (i.e. contains no update operators). The default is false. @@ -137,6 +148,10 @@ class Update implements Executable, Explainable throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object'); } + if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) { + throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']); + } + if (! is_bool($options['multi'])) { throw InvalidArgumentException::invalidType('"multi" option', $options['multi'], 'boolean'); } @@ -187,6 +202,13 @@ class Update implements Executable, Explainable throw UnsupportedException::collationNotSupported(); } + /* Server versions >= 3.4.0 raise errors for unknown update + * options. For previous versions, the CRUD spec requires a client-side + * error. */ + if (isset($this->options['hint']) && ! server_supports_feature($server, self::$wireVersionForHintServerSideError)) { + throw UnsupportedException::hintNotSupported(); + } + $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); if ($inTransaction && isset($this->options['writeConcern'])) { throw UnsupportedException::writeConcernNotSupportedInTransaction(); @@ -261,8 +283,10 @@ class Update implements Executable, Explainable 'upsert' => $this->options['upsert'], ]; - if (isset($this->options['arrayFilters'])) { - $updateOptions['arrayFilters'] = $this->options['arrayFilters']; + foreach (['arrayFilters', 'hint'] as $option) { + if (isset($this->options[$option])) { + $updateOptions[$option] = $this->options[$option]; + } } if (isset($this->options['collation'])) { diff --git a/cache/stores/mongodb/MongoDB/Operation/UpdateMany.php b/cache/stores/mongodb/MongoDB/Operation/UpdateMany.php index 9138fe6811d..eb1bd6cd506 100644 --- a/cache/stores/mongodb/MongoDB/Operation/UpdateMany.php +++ b/cache/stores/mongodb/MongoDB/Operation/UpdateMany.php @@ -61,6 +61,13 @@ class UpdateMany implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.2 and will result in an + * exception at execution time if used. + * * * session (MongoDB\Driver\Session): Client session. * * Sessions are not supported for server versions < 3.6. diff --git a/cache/stores/mongodb/MongoDB/Operation/UpdateOne.php b/cache/stores/mongodb/MongoDB/Operation/UpdateOne.php index 0c8d73a69c9..e8e7ef84282 100644 --- a/cache/stores/mongodb/MongoDB/Operation/UpdateOne.php +++ b/cache/stores/mongodb/MongoDB/Operation/UpdateOne.php @@ -61,6 +61,13 @@ class UpdateOne implements Executable, Explainable * This is not supported for server versions < 3.4 and will result in an * exception at execution time if used. * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 4.2 and will result in an + * exception at execution time if used. + * * * session (MongoDB\Driver\Session): Client session. * * Sessions are not supported for server versions < 3.6. diff --git a/cache/stores/mongodb/MongoDB/Operation/Watch.php b/cache/stores/mongodb/MongoDB/Operation/Watch.php index ef301603b20..1c88b87beb4 100644 --- a/cache/stores/mongodb/MongoDB/Operation/Watch.php +++ b/cache/stores/mongodb/MongoDB/Operation/Watch.php @@ -178,10 +178,14 @@ class Watch implements Executable, /* @internal */ CommandSubscriber 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY), ]; - if (isset($options['fullDocument']) && ! is_string($options['fullDocument'])) { + if (! is_string($options['fullDocument'])) { throw InvalidArgumentException::invalidType('"fullDocument" option', $options['fullDocument'], 'string'); } + if (! $options['readPreference'] instanceof ReadPreference) { + throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class); + } + if (isset($options['resumeAfter']) && ! is_array($options['resumeAfter']) && ! is_object($options['resumeAfter'])) { throw InvalidArgumentException::invalidType('"resumeAfter" option', $options['resumeAfter'], 'array or object'); } diff --git a/cache/stores/mongodb/MongoDB/Operation/WithTransaction.php b/cache/stores/mongodb/MongoDB/Operation/WithTransaction.php index 249616e6c83..3d0474e7ead 100644 --- a/cache/stores/mongodb/MongoDB/Operation/WithTransaction.php +++ b/cache/stores/mongodb/MongoDB/Operation/WithTransaction.php @@ -5,6 +5,7 @@ namespace MongoDB\Operation; use Exception; use MongoDB\Driver\Exception\RuntimeException; use MongoDB\Driver\Session; +use Throwable; use function call_user_func; use function time; @@ -63,7 +64,7 @@ class WithTransaction try { call_user_func($this->callback, $session); - } catch (Exception $e) { + } catch (Throwable $e) { if ($session->isInTransaction()) { $session->abortTransaction(); }