Merge branch 'MDL-70309-311' of git://github.com/ferranrecio/moodle into MOODLE_311_STABLE

This commit is contained in:
Sara Arjona 2021-02-02 10:44:54 +01:00
commit b6ea9f0d8c
51 changed files with 1290 additions and 277 deletions

View file

@ -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);
}
/**

View file

@ -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;
}
}

View file

@ -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);

View file

@ -0,0 +1,139 @@
<?php
/*
* Copyright 2020-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Command;
use MongoDB\Driver\Command;
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\Operation\Executable;
use function is_array;
use function is_bool;
use function is_integer;
use function is_object;
/**
* Wrapper for the listCollections command.
*
* @internal
* @see http://docs.mongodb.org/manual/reference/command/listCollections/
*/
class ListCollections implements Executable
{
/** @var string */
private $databaseName;
/** @var array */
private $options;
/**
* Constructs a listCollections command.
*
* Supported options:
*
* * filter (document): Query by which to filter collections.
*
* * maxTimeMS (integer): The maximum amount of time to allow the query to
* run.
*
* * nameOnly (boolean): A flag to indicate whether the command should
* return just the collection/view names and type or return both the name
* and other information.
*
* * session (MongoDB\Driver\Session): Client session.
*
* Sessions are not supported for server versions < 3.6.
*
* @param string $databaseName Database name
* @param array $options Command options
* @throws InvalidArgumentException for parameter/option parsing errors
*/
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['nameOnly']) && ! is_bool($options['nameOnly'])) {
throw InvalidArgumentException::invalidType('"nameOnly" option', $options['nameOnly'], 'boolean');
}
if (isset($options['session']) && ! $options['session'] instanceof Session) {
throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
}
$this->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;
}
}

View file

@ -0,0 +1,156 @@
<?php
/*
* Copyright 2020-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Command;
use MongoDB\Driver\Command;
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\Operation\Executable;
use function current;
use function is_array;
use function is_bool;
use function is_integer;
use function is_object;
/**
* Wrapper for the ListDatabases command.
*
* @internal
* @see http://docs.mongodb.org/manual/reference/command/listDatabases/
*/
class ListDatabases implements Executable
{
/** @var array */
private $options;
/**
* 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.
*
* * maxTimeMS (integer): The maximum amount of time to allow the query to
* run.
*
* * nameOnly (boolean): A flag to indicate whether the command should
* return just the database names, or return both database names and size
* information.
*
* * session (MongoDB\Driver\Session): Client session.
*
* Sessions are not supported for server versions < 3.6.
*
* @param array $options Command options
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function __construct(array $options = [])
{
if (isset($options['authorizedDatabases']) && ! is_bool($options['authorizedDatabases'])) {
throw InvalidArgumentException::invalidType('"authorizedDatabases" option', $options['authorizedDatabases'], 'boolean');
}
if (isset($options['filter']) && ! is_array($options['filter']) && ! is_object($options['filter'])) {
throw InvalidArgumentException::invalidType('"filter" option', $options['filter'], ['array', 'object']);
}
if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
}
if (isset($options['nameOnly']) && ! is_bool($options['nameOnly'])) {
throw InvalidArgumentException::invalidType('"nameOnly" option', $options['nameOnly'], 'boolean');
}
if (isset($options['session']) && ! $options['session'] instanceof Session) {
throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
}
$this->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;
}
}

View file

@ -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.
*

View file

@ -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)));
}
}

View file

@ -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)));
}
/**

View file

@ -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.
*

View file

@ -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();

View file

@ -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.
*

View file

@ -0,0 +1,46 @@
<?php
namespace MongoDB\GridFS\Exception;
use MongoDB\Exception\RuntimeException;
use function MongoDB\BSON\fromPHP;
use function MongoDB\BSON\toJSON;
use function sprintf;
use function stream_get_meta_data;
class StreamException extends RuntimeException
{
/**
* @param resource $source
* @param resource $destination
*/
public static function downloadFromFilenameFailed(string $filename, $source, $destination) : self
{
$sourceMetadata = stream_get_meta_data($source);
$destinationMetadata = stream_get_meta_data($destination);
return new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $filename));
}
/**
* @param mixed $id
* @param resource $source
* @param resource $destination
*/
public static function downloadFromIdFailed($id, $source, $destination) : self
{
$idString = toJSON(fromPHP(['_id' => $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));
}
}

View file

@ -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;

View file

@ -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');
}

View file

@ -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 : [];
}

View file

@ -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;
}
}

View file

@ -0,0 +1,88 @@
<?php
/*
* Copyright 2017 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Model;
use Closure;
use Iterator;
use IteratorIterator;
use Traversable;
/**
* Iterator to apply a callback before returning an element
*
* @internal
*/
class CallbackIterator implements Iterator
{
/** @var Closure */
private $callback;
/** @var IteratorIterator */
private $iterator;
public function __construct(Traversable $traversable, Closure $callback)
{
$this->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();
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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']);
}

View file

@ -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);
}
}

View file

@ -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'];
}

View file

@ -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;
}

View file

@ -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.

View file

@ -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.

View file

@ -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;

View file

@ -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'])) {

View file

@ -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
*/

View file

@ -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];
}

View file

@ -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);
}
}

View file

@ -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.
*

View file

@ -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.
*

View file

@ -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.
*

View file

@ -0,0 +1,79 @@
<?php
/*
* Copyright 2020-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Operation;
use Iterator;
use MongoDB\Command\ListCollections as ListCollectionsCommand;
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Server;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Model\CallbackIterator;
/**
* Operation for the listCollectionNames helper.
*
* @api
* @see \MongoDB\Database::listCollectionNames()
* @see http://docs.mongodb.org/manual/reference/command/listCollections/
*/
class ListCollectionNames implements Executable
{
/** @var ListCollectionsCommand */
private $listCollections;
/**
* Constructs a listCollections command.
*
* Supported options:
*
* * filter (document): Query by which to filter collections.
*
* * maxTimeMS (integer): The maximum amount of time to allow the query to
* run.
*
* * session (MongoDB\Driver\Session): Client session.
*
* Sessions are not supported for server versions < 3.6.
*
* @param string $databaseName Database name
* @param array $options Command options
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function __construct($databaseName, array $options = [])
{
$this->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'];
}
);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,85 @@
<?php
/*
* Copyright 2020-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Operation;
use ArrayIterator;
use Iterator;
use MongoDB\Command\ListDatabases as ListDatabasesCommand;
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Server;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnexpectedValueException;
use function array_column;
/**
* Operation for the ListDatabases command, returning only database names.
*
* @api
* @see \MongoDB\Client::listDatabaseNames()
* @see http://docs.mongodb.org/manual/reference/command/ListDatabases/
*/
class ListDatabaseNames implements Executable
{
/** @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.
*
* * maxTimeMS (integer): The maximum amount of time to allow the query to
* run.
*
* * session (MongoDB\Driver\Session): Client session.
*
* Sessions are not supported for server versions < 3.6.
*
* @param array $options Command options
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function __construct(array $options = [])
{
$this->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'));
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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.
*

View file

@ -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()

View file

@ -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.

View file

@ -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'])) {

View file

@ -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.

View file

@ -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.

View file

@ -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');
}

View file

@ -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();
}

View file

@ -158,7 +158,7 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
* @return bool
*/
public static function are_requirements_met() {
return version_compare(phpversion('mongodb'), '1.5', 'ge');
return version_compare(phpversion('mongodb'), '1.8', 'ge');
}
/**

View file

@ -7,5 +7,6 @@ Import procedure:
- Copy all the files and folders from the folder mongodb/src in the cache/stores/mongodb/MongoDB directory.
- Copy the license file from the project root.
- Update thirdpartylibs.xml with the latest version.
- Check the minim php driver version in https://docs.mongodb.com/drivers/php#compatibility and change the value in the "are_requirements_met" method if necessary.
This version (1.5.1) requires PHP mongodb extension >= 1.6.0
This version (1.8.0) requires PHP mongodb extension >= 1.8.0

View file

@ -4,7 +4,7 @@
<location>MongoDB</location>
<name>MongoDB PHP Library</name>
<license>Apache</license>
<version>1.5.1</version>
<version>1.8.0</version>
<licenseversion>2.0</licenseversion>
</library>
</libraries>