--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a bulk write operation.
+ */
+class BulkWriteResult
+{
+ private $writeResult;
+ private $insertedIds;
+ private $isAcknowledged;
+
+ /**
+ * Constructor.
+ *
+ * @param WriteResult $writeResult
+ * @param mixed[] $insertedIds
+ */
+ public function __construct(WriteResult $writeResult, array $insertedIds)
+ {
+ $this->writeResult = $writeResult;
+ $this->insertedIds = $insertedIds;
+ $this->isAcknowledged = $writeResult->isAcknowledged();
+ }
+
+ /**
+ * Return the number of documents that were deleted.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see BulkWriteResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getDeletedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getDeletedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return the number of documents that were inserted.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see BulkWriteResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getInsertedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getInsertedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return a map of the inserted documents' IDs.
+ *
+ * The index of each ID in the map corresponds to each document's position
+ * in the bulk operation. If a document had an ID prior to inserting (i.e.
+ * the driver did not generate an ID), the index will contain its "_id"
+ * field value. Any driver-generated ID will be a MongoDB\BSON\ObjectId
+ * instance.
+ *
+ * @return mixed[]
+ */
+ public function getInsertedIds()
+ {
+ return $this->insertedIds;
+ }
+
+ /**
+ * Return the number of documents that were matched by the filter.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see BulkWriteResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getMatchedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getMatchedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return the number of documents that were modified.
+ *
+ * This value is undefined (i.e. null) if the write executed as a legacy
+ * operation instead of command.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see BulkWriteResult::isAcknowledged()
+ * @return integer|null
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getModifiedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getModifiedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return the number of documents that were upserted.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see BulkWriteResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getUpsertedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getUpsertedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return a map of the upserted documents' IDs.
+ *
+ * The index of each ID in the map corresponds to each document's position
+ * in bulk operation. If a document had an ID prior to upserting (i.e. the
+ * server did not need to generate an ID), this will contain its "_id". Any
+ * server-generated ID will be a MongoDB\BSON\ObjectId instance.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see BulkWriteResult::isAcknowledged()
+ * @return mixed[]
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getUpsertedIds()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getUpsertedIds();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return whether this update was acknowledged by the server.
+ *
+ * If the update was not acknowledged, other fields from the WriteResult
+ * (e.g. matchedCount) will be undefined.
+ *
+ * @return boolean
+ */
+ public function isAcknowledged()
+ {
+ return $this->isAcknowledged;
+ }
+}
--- /dev/null
+<?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;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Exception\ConnectionException;
+use MongoDB\Driver\Exception\RuntimeException;
+use MongoDB\Driver\Exception\ServerException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\ResumeTokenException;
+use IteratorIterator;
+use Iterator;
+
+/**
+ * Iterator for a change stream.
+ *
+ * @api
+ * @see \MongoDB\Collection::watch()
+ * @see http://docs.mongodb.org/manual/reference/command/changeStream/
+ */
+class ChangeStream implements Iterator
+{
+ /**
+ * @deprecated 1.4
+ * @todo Remove this in 2.0 (see: PHPLIB-360)
+ */
+ const CURSOR_NOT_FOUND = 43;
+
+ private static $errorCodeCappedPositionLost = 136;
+ private static $errorCodeInterrupted = 11601;
+ private static $errorCodeCursorKilled = 237;
+
+ private $resumeToken;
+ private $resumeCallable;
+ private $csIt;
+ private $key = 0;
+ private $hasAdvanced = false;
+
+ /**
+ * Constructor.
+ *
+ * @internal
+ * @param Cursor $cursor
+ * @param callable $resumeCallable
+ */
+ public function __construct(Cursor $cursor, callable $resumeCallable)
+ {
+ $this->resumeCallable = $resumeCallable;
+ $this->csIt = new IteratorIterator($cursor);
+ }
+
+ /**
+ * @see http://php.net/iterator.current
+ * @return mixed
+ */
+ public function current()
+ {
+ return $this->csIt->current();
+ }
+
+ /**
+ * @return \MongoDB\Driver\CursorId
+ */
+ public function getCursorId()
+ {
+ return $this->csIt->getInnerIterator()->getId();
+ }
+
+ /**
+ * @see http://php.net/iterator.key
+ * @return mixed
+ */
+ public function key()
+ {
+ if ($this->valid()) {
+ return $this->key;
+ }
+ return null;
+ }
+
+ /**
+ * @see http://php.net/iterator.next
+ * @return void
+ */
+ public function next()
+ {
+ try {
+ $this->csIt->next();
+ if ($this->valid()) {
+ if ($this->hasAdvanced) {
+ $this->key++;
+ }
+ $this->hasAdvanced = true;
+ $this->resumeToken = $this->extractResumeToken($this->csIt->current());
+ }
+ /* If the cursorId is 0, the server has invalidated the cursor so we
+ * will never perform another getMore. This means that we cannot
+ * resume and we can therefore unset the resumeCallable, which will
+ * free any reference to Watch. This will also free the only
+ * reference to an implicit session, since any such reference
+ * belongs to Watch. */
+ if ((string) $this->getCursorId() === '0') {
+ $this->resumeCallable = null;
+ }
+ } catch (RuntimeException $e) {
+ if ($this->isResumableError($e)) {
+ $this->resume();
+ }
+ }
+ }
+
+ /**
+ * @see http://php.net/iterator.rewind
+ * @return void
+ */
+ public function rewind()
+ {
+ try {
+ $this->csIt->rewind();
+ if ($this->valid()) {
+ $this->hasAdvanced = true;
+ $this->resumeToken = $this->extractResumeToken($this->csIt->current());
+ }
+ // As with next(), free the callable once we know it will never be used.
+ if ((string) $this->getCursorId() === '0') {
+ $this->resumeCallable = null;
+ }
+ } catch (RuntimeException $e) {
+ if ($this->isResumableError($e)) {
+ $this->resume();
+ }
+ }
+ }
+
+ /**
+ * @see http://php.net/iterator.valid
+ * @return boolean
+ */
+ public function valid()
+ {
+ return $this->csIt->valid();
+ }
+
+ /**
+ * Extracts the resume token (i.e. "_id" field) from the change document.
+ *
+ * @param array|document $document Change document
+ * @return mixed
+ * @throws InvalidArgumentException
+ * @throws ResumeTokenException if the resume token is not found or invalid
+ */
+ private function extractResumeToken($document)
+ {
+ if ( ! is_array($document) && ! is_object($document)) {
+ throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
+ }
+
+ if ($document instanceof Serializable) {
+ return $this->extractResumeToken($document->bsonSerialize());
+ }
+
+ $resumeToken = is_array($document)
+ ? (isset($document['_id']) ? $document['_id'] : null)
+ : (isset($document->_id) ? $document->_id : null);
+
+ if ( ! isset($resumeToken)) {
+ throw ResumeTokenException::notFound();
+ }
+
+ if ( ! is_array($resumeToken) && ! is_object($resumeToken)) {
+ throw ResumeTokenException::invalidType($resumeToken);
+ }
+
+ return $resumeToken;
+ }
+
+ /**
+ * Determines if an exception is a resumable error.
+ *
+ * @see https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#resumable-error
+ * @param RuntimeException $exception
+ * @return boolean
+ */
+ private function isResumableError(RuntimeException $exception)
+ {
+ if ($exception instanceof ConnectionException) {
+ return true;
+ }
+
+ if ( ! $exception instanceof ServerException) {
+ return false;
+ }
+
+ if (in_array($exception->getCode(), [self::$errorCodeCappedPositionLost, self::$errorCodeCursorKilled, self::$errorCodeInterrupted])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a new changeStream after a resumable server error.
+ *
+ * @return void
+ */
+ private function resume()
+ {
+ $newChangeStream = call_user_func($this->resumeCallable, $this->resumeToken);
+ $this->csIt = $newChangeStream->csIt;
+ $this->csIt->rewind();
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\DatabaseInfoIterator;
+use MongoDB\Operation\DropDatabase;
+use MongoDB\Operation\ListDatabases;
+use MongoDB\Operation\Watch;
+
+class Client
+{
+ private static $defaultTypeMap = [
+ 'array' => 'MongoDB\Model\BSONArray',
+ 'document' => 'MongoDB\Model\BSONDocument',
+ 'root' => 'MongoDB\Model\BSONDocument',
+ ];
+ private static $wireVersionForReadConcern = 4;
+ private static $wireVersionForWritableCommandWriteConcern = 5;
+
+ private $manager;
+ private $readConcern;
+ private $readPreference;
+ private $uri;
+ private $typeMap;
+ private $writeConcern;
+
+ /**
+ * Constructs a new Client instance.
+ *
+ * This is the preferred class for connecting to a MongoDB server or
+ * cluster of servers. It serves as a gateway for accessing individual
+ * databases and collections.
+ *
+ * Supported driver-specific options:
+ *
+ * * typeMap (array): Default type map for cursors and BSON documents.
+ *
+ * Other options are documented in MongoDB\Driver\Manager::__construct().
+ *
+ * @see http://docs.mongodb.org/manual/reference/connection-string/
+ * @see http://php.net/manual/en/mongodb-driver-manager.construct.php
+ * @see http://php.net/manual/en/mongodb.persistence.php#mongodb.persistence.typemaps
+ * @param string $uri MongoDB connection string
+ * @param array $uriOptions Additional connection string options
+ * @param array $driverOptions Driver-specific options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverInvalidArgumentException for parameter/option parsing errors in the driver
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function __construct($uri = 'mongodb://127.0.0.1/', array $uriOptions = [], array $driverOptions = [])
+ {
+ $driverOptions += ['typeMap' => self::$defaultTypeMap];
+
+ if (isset($driverOptions['typeMap']) && ! is_array($driverOptions['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" driver option', $driverOptions['typeMap'], 'array');
+ }
+
+ $this->uri = (string) $uri;
+ $this->typeMap = isset($driverOptions['typeMap']) ? $driverOptions['typeMap'] : null;
+
+ unset($driverOptions['typeMap']);
+
+ $this->manager = new Manager($uri, $uriOptions, $driverOptions);
+ $this->readConcern = $this->manager->getReadConcern();
+ $this->readPreference = $this->manager->getReadPreference();
+ $this->writeConcern = $this->manager->getWriteConcern();
+ }
+
+ /**
+ * Return internal properties for debugging purposes.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'manager' => $this->manager,
+ 'uri' => $this->uri,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+ }
+
+ /**
+ * Select a database.
+ *
+ * Note: databases whose names contain special characters (e.g. "-") may
+ * be selected with complex syntax (e.g. $client->{"that-database"}) or
+ * {@link selectDatabase()}.
+ *
+ * @see http://php.net/oop5.overloading#object.get
+ * @see http://php.net/types.string#language.types.string.parsing.complex
+ * @param string $databaseName Name of the database to select
+ * @return Database
+ */
+ public function __get($databaseName)
+ {
+ return $this->selectDatabase($databaseName);
+ }
+
+ /**
+ * Return the connection string (i.e. URI).
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->uri;
+ }
+
+ /**
+ * Drop a database.
+ *
+ * @see DropDatabase::__construct() for supported options
+ * @param string $databaseName Database name
+ * @param array $options Additional options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are unsupported on the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function dropDatabase($databaseName, array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DropDatabase($databaseName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Return the Manager.
+ *
+ * @return Manager
+ */
+ public function getManager()
+ {
+ return $this->manager;
+ }
+
+ /**
+ * Return the read concern for this client.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+ * @return ReadConcern
+ */
+ public function getReadConcern()
+ {
+ return $this->readConcern;
+ }
+
+ /**
+ * Return the read preference for this client.
+ *
+ * @return ReadPreference
+ */
+ public function getReadPreference()
+ {
+ return $this->readPreference;
+ }
+
+ /**
+ * Return the type map for this client.
+ *
+ * @return array
+ */
+ public function getTypeMap()
+ {
+ return $this->typeMap;
+ }
+
+ /**
+ * Return the write concern for this client.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+ * @return WriteConcern
+ */
+ public function getWriteConcern()
+ {
+ return $this->writeConcern;
+ }
+
+ /**
+ * List databases.
+ *
+ * @see ListDatabases::__construct() for supported options
+ * @return DatabaseInfoIterator
+ * @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 listDatabases(array $options = [])
+ {
+ $operation = new ListDatabases($options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Select a collection.
+ *
+ * @see Collection::__construct() for supported options
+ * @param string $databaseName Name of the database containing the collection
+ * @param string $collectionName Name of the collection to select
+ * @param array $options Collection constructor options
+ * @return Collection
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function selectCollection($databaseName, $collectionName, array $options = [])
+ {
+ $options += ['typeMap' => $this->typeMap];
+
+ return new Collection($this->manager, $databaseName, $collectionName, $options);
+ }
+
+ /**
+ * Select a database.
+ *
+ * @see Database::__construct() for supported options
+ * @param string $databaseName Name of the database to select
+ * @param array $options Database constructor options
+ * @return Database
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function selectDatabase($databaseName, array $options = [])
+ {
+ $options += ['typeMap' => $this->typeMap];
+
+ return new Database($this->manager, $databaseName, $options);
+ }
+
+ /**
+ * Start a new client session.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-manager.startsession.php
+ * @param array $options Session options
+ * @return MongoDB\Driver\Session
+ */
+ public function startSession(array $options = [])
+ {
+ return $this->manager->startSession($options);
+ }
+
+ /**
+ * Create a change stream for watching changes to the cluster.
+ *
+ * @see Watch::__construct() for supported options
+ * @param array $pipeline List of pipeline operations
+ * @param array $options Command options
+ * @return ChangeStream
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function watch(array $pipeline = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new Watch($this->manager, null, null, $pipeline, $options);
+
+ return $operation->execute($server);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\BSON\JavascriptInterface;
+use MongoDB\BSON\Serializable;
+use MongoDB\ChangeStream;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\IndexInfo;
+use MongoDB\Model\IndexInfoIterator;
+use MongoDB\Operation\Aggregate;
+use MongoDB\Operation\BulkWrite;
+use MongoDB\Operation\CreateIndexes;
+use MongoDB\Operation\Count;
+use MongoDB\Operation\CountDocuments;
+use MongoDB\Operation\DeleteMany;
+use MongoDB\Operation\DeleteOne;
+use MongoDB\Operation\Distinct;
+use MongoDB\Operation\DropCollection;
+use MongoDB\Operation\DropIndexes;
+use MongoDB\Operation\EstimatedDocumentCount;
+use MongoDB\Operation\Explain;
+use MongoDB\Operation\Explainable;
+use MongoDB\Operation\Find;
+use MongoDB\Operation\FindOne;
+use MongoDB\Operation\FindOneAndDelete;
+use MongoDB\Operation\FindOneAndReplace;
+use MongoDB\Operation\FindOneAndUpdate;
+use MongoDB\Operation\InsertMany;
+use MongoDB\Operation\InsertOne;
+use MongoDB\Operation\ListIndexes;
+use MongoDB\Operation\MapReduce;
+use MongoDB\Operation\ReplaceOne;
+use MongoDB\Operation\UpdateMany;
+use MongoDB\Operation\UpdateOne;
+use MongoDB\Operation\Watch;
+use Traversable;
+
+class Collection
+{
+ private static $defaultTypeMap = [
+ 'array' => 'MongoDB\Model\BSONArray',
+ 'document' => 'MongoDB\Model\BSONDocument',
+ 'root' => 'MongoDB\Model\BSONDocument',
+ ];
+ private static $wireVersionForFindAndModifyWriteConcern = 4;
+ private static $wireVersionForReadConcern = 4;
+ private static $wireVersionForWritableCommandWriteConcern = 5;
+
+ private $collectionName;
+ private $databaseName;
+ private $manager;
+ private $readConcern;
+ private $readPreference;
+ private $typeMap;
+ private $writeConcern;
+
+ /**
+ * Constructs new Collection instance.
+ *
+ * This class provides methods for collection-specific operations, such as
+ * CRUD (i.e. create, read, update, and delete) and index management.
+ *
+ * Supported options:
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): The default read concern to
+ * use for collection operations. Defaults to the Manager's read concern.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): The default read
+ * preference to use for collection operations. Defaults to the Manager's
+ * read preference.
+ *
+ * * typeMap (array): Default type map for cursors and BSON documents.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): The default write concern
+ * to use for collection operations. Defaults to the Manager's write
+ * concern.
+ *
+ * @param Manager $manager Manager instance from the driver
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array $options Collection options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct(Manager $manager, $databaseName, $collectionName, array $options = [])
+ {
+ if (strlen($databaseName) < 1) {
+ throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName);
+ }
+
+ if (strlen($collectionName) < 1) {
+ throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName);
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ $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();
+ }
+
+ /**
+ * Return internal properties for debugging purposes.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'collectionName' => $this->collectionName,
+ 'databaseName' => $this->databaseName,
+ 'manager' => $this->manager,
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+ }
+
+ /**
+ * Return the collection namespace (e.g. "db.collection").
+ *
+ * @see https://docs.mongodb.org/manual/faq/developers/#faq-dev-namespace
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->databaseName . '.' . $this->collectionName;
+ }
+
+ /**
+ * Executes an aggregation framework pipeline on the collection.
+ *
+ * Note: this method's return value depends on the MongoDB server version
+ * and the "useCursor" option. If "useCursor" is true, a Cursor will be
+ * returned; otherwise, an ArrayIterator is returned, which wraps the
+ * "result" array from the command response document.
+ *
+ * @see Aggregate::__construct() for supported options
+ * @param array $pipeline List of pipeline operations
+ * @param array $options Command options
+ * @return Traversable
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function aggregate(array $pipeline, array $options = [])
+ {
+ $hasOutStage = \MongoDB\is_last_pipeline_operator_out($pipeline);
+
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ if ($hasOutStage) {
+ $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ /* A "majority" read concern is not compatible with the $out stage, so
+ * avoid providing the Collection's read concern if it would conflict.
+ */
+ if ( ! isset($options['readConcern']) &&
+ ! ($hasOutStage && $this->readConcern->getLevel() === ReadConcern::MAJORITY) &&
+ \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ if ($hasOutStage && ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new Aggregate($this->databaseName, $this->collectionName, $pipeline, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Executes multiple write operations.
+ *
+ * @see BulkWrite::__construct() for supported options
+ * @param array[] $operations List of write operations
+ * @param array $options Command options
+ * @return BulkWriteResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function bulkWrite(array $operations, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new BulkWrite($this->databaseName, $this->collectionName, $operations, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Gets the number of documents matching the filter.
+ *
+ * @see Count::__construct() for supported options
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @return integer
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ *
+ * @deprecated 1.4
+ */
+ public function count($filter = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ $operation = new Count($this->databaseName, $this->collectionName, $filter, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Gets the number of documents matching the filter.
+ *
+ * @see CountDocuments::__construct() for supported options
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @return integer
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function countDocuments($filter = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ $operation = new CountDocuments($this->databaseName, $this->collectionName, $filter, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Create a single index for the collection.
+ *
+ * @see Collection::createIndexes()
+ * @see CreateIndexes::__construct() for supported command options
+ * @param array|object $key Document containing fields mapped to values,
+ * which denote order or an index type
+ * @param array $options Index and command options
+ * @return string The name of the created index
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function createIndex($key, array $options = [])
+ {
+ $commandOptionKeys = ['maxTimeMS' => 1, 'session' => 1, 'writeConcern' => 1];
+ $indexOptions = array_diff_key($options, $commandOptionKeys);
+ $commandOptions = array_intersect_key($options, $commandOptionKeys);
+
+ return current($this->createIndexes([['key' => $key] + $indexOptions], $commandOptions));
+ }
+
+ /**
+ * Create one or more indexes for the collection.
+ *
+ * Each element in the $indexes array must have a "key" document, which
+ * contains fields mapped to an order or type. Other options may follow.
+ * For example:
+ *
+ * $indexes = [
+ * // Create a unique index on the "username" field
+ * [ 'key' => [ 'username' => 1 ], 'unique' => true ],
+ * // Create a 2dsphere index on the "loc" field with a custom name
+ * [ 'key' => [ 'loc' => '2dsphere' ], 'name' => 'geo' ],
+ * ];
+ *
+ * If the "name" option is unspecified, a name will be generated from the
+ * "key" document.
+ *
+ * @see http://docs.mongodb.org/manual/reference/command/createIndexes/
+ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
+ * @see CreateIndexes::__construct() for supported command options
+ * @param array[] $indexes List of index specifications
+ * @param array $options Command options
+ * @return string[] The names of the created indexes
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function createIndexes(array $indexes, array $options = [])
+ {
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new CreateIndexes($this->databaseName, $this->collectionName, $indexes, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Deletes all documents matching the filter.
+ *
+ * @see DeleteMany::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/delete/
+ * @param array|object $filter Query by which to delete documents
+ * @param array $options Command options
+ * @return DeleteResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function deleteMany($filter, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Deletes at most one document matching the filter.
+ *
+ * @see DeleteOne::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/delete/
+ * @param array|object $filter Query by which to delete documents
+ * @param array $options Command options
+ * @return DeleteResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function deleteOne($filter, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Finds the distinct values for a specified field across the collection.
+ *
+ * @see Distinct::__construct() for supported options
+ * @param string $fieldName Field for which to return distinct values
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @return mixed[]
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function distinct($fieldName, $filter = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ $operation = new Distinct($this->databaseName, $this->collectionName, $fieldName, $filter, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Drop this collection.
+ *
+ * @see DropCollection::__construct() for supported options
+ * @param array $options Additional options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function drop(array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DropCollection($this->databaseName, $this->collectionName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Drop a single index in the collection.
+ *
+ * @see DropIndexes::__construct() for supported options
+ * @param string|IndexInfo $indexName Index name or model object
+ * @param array $options Additional options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function dropIndex($indexName, array $options = [])
+ {
+ $indexName = (string) $indexName;
+
+ if ($indexName === '*') {
+ throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes');
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DropIndexes($this->databaseName, $this->collectionName, $indexName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Drop all indexes in the collection.
+ *
+ * @see DropIndexes::__construct() for supported options
+ * @param array $options Additional options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function dropIndexes(array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DropIndexes($this->databaseName, $this->collectionName, '*', $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Gets an estimated number of documents in the collection using the collection metadata.
+ *
+ * @see EstimatedDocumentCount::__construct() for supported options
+ * @param array $options Command options
+ * @return integer
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function EstimatedDocumentCount(array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ $operation = new EstimatedDocumentCount($this->databaseName, $this->collectionName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Explains explainable commands.
+ *
+ * @see Explain::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/explain/
+ * @param Explainable $explainable Command on which to run explain
+ * @param array $options Additional options
+ * @return array|object
+ * @throws UnsupportedException if explainable or options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function explain(Explainable $explainable, array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ $operation = new Explain($this->databaseName, $explainable, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Finds documents matching the query.
+ *
+ * @see Find::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Additional options
+ * @return Cursor
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function find($filter = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new Find($this->databaseName, $this->collectionName, $filter, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Finds a single document matching the query.
+ *
+ * @see FindOne::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Additional options
+ * @return array|object|null
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function findOne($filter = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new FindOne($this->databaseName, $this->collectionName, $filter, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Finds a single document and deletes it, returning the original.
+ *
+ * The document to return may be null if no document matched the filter.
+ *
+ * @see FindOneAndDelete::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @return array|object|null
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function findOneAndDelete($filter, array $options = [])
+ {
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new FindOneAndDelete($this->databaseName, $this->collectionName, $filter, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Finds a single document and replaces it, returning either the original or
+ * the replaced document.
+ *
+ * The document to return may be null if no document matched the filter. By
+ * default, the original document is returned. Specify
+ * FindOneAndReplace::RETURN_DOCUMENT_AFTER for the "returnDocument" option
+ * to return the updated document.
+ *
+ * @see FindOneAndReplace::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
+ * @param array|object $filter Query by which to filter documents
+ * @param array|object $replacement Replacement document
+ * @param array $options Command options
+ * @return array|object|null
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function findOneAndReplace($filter, $replacement, array $options = [])
+ {
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new FindOneAndReplace($this->databaseName, $this->collectionName, $filter, $replacement, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Finds a single document and updates it, returning either the original or
+ * the updated document.
+ *
+ * The document to return may be null if no document matched the filter. By
+ * default, the original document is returned. Specify
+ * FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the "returnDocument" option
+ * to return the updated document.
+ *
+ * @see FindOneAndReplace::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
+ * @param array|object $filter Query by which to filter documents
+ * @param array|object $update Update to apply to the matched document
+ * @param array $options Command options
+ * @return array|object|null
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function findOneAndUpdate($filter, $update, array $options = [])
+ {
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new FindOneAndUpdate($this->databaseName, $this->collectionName, $filter, $update, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Return the collection name.
+ *
+ * @return string
+ */
+ public function getCollectionName()
+ {
+ return $this->collectionName;
+ }
+
+ /**
+ * Return the database name.
+ *
+ * @return string
+ */
+ public function getDatabaseName()
+ {
+ return $this->databaseName;
+ }
+
+ /**
+ * Return the Manager.
+ *
+ * @return Manager
+ */
+ public function getManager()
+ {
+ return $this->manager;
+ }
+
+ /**
+ * Return the collection namespace.
+ *
+ * @see https://docs.mongodb.org/manual/reference/glossary/#term-namespace
+ * @return string
+ */
+ public function getNamespace()
+ {
+ return $this->databaseName . '.' . $this->collectionName;
+ }
+
+ /**
+ * Return the read concern for this collection.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+ * @return ReadConcern
+ */
+ public function getReadConcern()
+ {
+ return $this->readConcern;
+ }
+
+ /**
+ * Return the read preference for this collection.
+ *
+ * @return ReadPreference
+ */
+ public function getReadPreference()
+ {
+ return $this->readPreference;
+ }
+
+ /**
+ * Return the type map for this collection.
+ *
+ * @return array
+ */
+ public function getTypeMap()
+ {
+ return $this->typeMap;
+ }
+
+ /**
+ * Return the write concern for this collection.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+ * @return WriteConcern
+ */
+ public function getWriteConcern()
+ {
+ return $this->writeConcern;
+ }
+
+ /**
+ * Inserts multiple documents.
+ *
+ * @see InsertMany::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/insert/
+ * @param array[]|object[] $documents The documents to insert
+ * @param array $options Command options
+ * @return InsertManyResult
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function insertMany(array $documents, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new InsertMany($this->databaseName, $this->collectionName, $documents, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Inserts one document.
+ *
+ * @see InsertOne::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/insert/
+ * @param array|object $document The document to insert
+ * @param array $options Command options
+ * @return InsertOneResult
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function insertOne($document, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new InsertOne($this->databaseName, $this->collectionName, $document, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Returns information for all indexes for the collection.
+ *
+ * @see ListIndexes::__construct() for supported options
+ * @return IndexInfoIterator
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function listIndexes(array $options = [])
+ {
+ $operation = new ListIndexes($this->databaseName, $this->collectionName, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Executes a map-reduce aggregation on the collection.
+ *
+ * @see MapReduce::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/mapReduce/
+ * @param JavascriptInterface $map Map function
+ * @param JavascriptInterface $reduce Reduce function
+ * @param string|array|object $out Output specification
+ * @param array $options Command options
+ * @return MapReduceResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ * @throws UnexpectedValueException if the command response was malformed
+ */
+ public function mapReduce(JavascriptInterface $map, JavascriptInterface $reduce, $out, array $options = [])
+ {
+ $hasOutputCollection = ! \MongoDB\is_mapreduce_output_inline($out);
+
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ // Check if the out option is inline because we will want to coerce a primary read preference if not
+ if ($hasOutputCollection) {
+ $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ /* A "majority" read concern is not compatible with inline output, so
+ * avoid providing the Collection's read concern if it would conflict.
+ */
+ if ( ! isset($options['readConcern']) && ! ($hasOutputCollection && $this->readConcern->getLevel() === ReadConcern::MAJORITY) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new MapReduce($this->databaseName, $this->collectionName, $map, $reduce, $out, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Replaces at most one document matching the filter.
+ *
+ * @see ReplaceOne::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/update/
+ * @param array|object $filter Query by which to filter documents
+ * @param array|object $replacement Replacement document
+ * @param array $options Command options
+ * @return UpdateResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function replaceOne($filter, $replacement, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new ReplaceOne($this->databaseName, $this->collectionName, $filter, $replacement, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Updates all documents matching the filter.
+ *
+ * @see UpdateMany::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/update/
+ * @param array|object $filter Query by which to filter documents
+ * @param array|object $update Update to apply to the matched documents
+ * @param array $options Command options
+ * @return UpdateResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function updateMany($filter, $update, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new UpdateMany($this->databaseName, $this->collectionName, $filter, $update, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Updates at most one document matching the filter.
+ *
+ * @see UpdateOne::__construct() for supported options
+ * @see http://docs.mongodb.org/manual/reference/command/update/
+ * @param array|object $filter Query by which to filter documents
+ * @param array|object $update Update to apply to the matched document
+ * @param array $options Command options
+ * @return UpdateResult
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function updateOne($filter, $update, array $options = [])
+ {
+ if ( ! isset($options['writeConcern'])) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new UpdateOne($this->databaseName, $this->collectionName, $filter, $update, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Create a change stream for watching changes to the collection.
+ *
+ * @see Watch::__construct() for supported options
+ * @param array $pipeline List of pipeline operations
+ * @param array $options Command options
+ * @return ChangeStream
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function watch(array $pipeline = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ /* Although change streams require a newer version of the server than
+ * read concerns, perform the usual wire version check before inheriting
+ * the collection's read concern. In the event that the server is too
+ * old, this makes it more likely that users will encounter an error
+ * related to change streams being unsupported instead of an
+ * UnsupportedException regarding use of the "readConcern" option from
+ * the Aggregate operation class. */
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new Watch($this->manager, $this->databaseName, $this->collectionName, $pipeline, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Get a clone of this collection with different options.
+ *
+ * @see Collection::__construct() for supported options
+ * @param array $options Collection constructor options
+ * @return Collection
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function withOptions(array $options = [])
+ {
+ $options += [
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+
+ return new Collection($this->manager, $this->databaseName, $this->collectionName, $options);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\Collection;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\GridFS\Bucket;
+use MongoDB\Model\CollectionInfoIterator;
+use MongoDB\Operation\CreateCollection;
+use MongoDB\Operation\DatabaseCommand;
+use MongoDB\Operation\DropCollection;
+use MongoDB\Operation\DropDatabase;
+use MongoDB\Operation\ListCollections;
+use MongoDB\Operation\ModifyCollection;
+use MongoDB\Operation\Watch;
+
+class Database
+{
+ private static $defaultTypeMap = [
+ 'array' => 'MongoDB\Model\BSONArray',
+ 'document' => 'MongoDB\Model\BSONDocument',
+ 'root' => 'MongoDB\Model\BSONDocument',
+ ];
+ private static $wireVersionForReadConcern = 4;
+ private static $wireVersionForWritableCommandWriteConcern = 5;
+
+ private $databaseName;
+ private $manager;
+ private $readConcern;
+ private $readPreference;
+ private $typeMap;
+ private $writeConcern;
+
+ /**
+ * Constructs new Database instance.
+ *
+ * This class provides methods for database-specific operations and serves
+ * as a gateway for accessing collections.
+ *
+ * Supported options:
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): The default read concern to
+ * use for database operations and selected collections. Defaults to the
+ * Manager's read concern.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): The default read
+ * preference to use for database operations and selected collections.
+ * Defaults to the Manager's read preference.
+ *
+ * * typeMap (array): Default type map for cursors and BSON documents.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): The default write concern
+ * to use for database operations and selected collections. Defaults to
+ * the Manager's write concern.
+ *
+ * @param Manager $manager Manager instance from the driver
+ * @param string $databaseName Database name
+ * @param array $options Database options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct(Manager $manager, $databaseName, array $options = [])
+ {
+ if (strlen($databaseName) < 1) {
+ throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName);
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ $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();
+ }
+
+ /**
+ * Return internal properties for debugging purposes.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'databaseName' => $this->databaseName,
+ 'manager' => $this->manager,
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+ }
+
+ /**
+ * Select a collection within this database.
+ *
+ * Note: collections whose names contain special characters (e.g. ".") may
+ * be selected with complex syntax (e.g. $database->{"system.profile"}) or
+ * {@link selectCollection()}.
+ *
+ * @see http://php.net/oop5.overloading#object.get
+ * @see http://php.net/types.string#language.types.string.parsing.complex
+ * @param string $collectionName Name of the collection to select
+ * @return Collection
+ */
+ public function __get($collectionName)
+ {
+ return $this->selectCollection($collectionName);
+ }
+
+ /**
+ * Return the database name.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->databaseName;
+ }
+
+ /**
+ * Execute a command on this database.
+ *
+ * @see DatabaseCommand::__construct() for supported options
+ * @param array|object $command Command document
+ * @param array $options Options for command execution
+ * @return Cursor
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function command($command, array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new DatabaseCommand($this->databaseName, $command, $options);
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Create a new collection explicitly.
+ *
+ * @see CreateCollection::__construct() for supported options
+ * @param string $collectionName
+ * @param array $options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function createCollection($collectionName, array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new CreateCollection($this->databaseName, $collectionName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Drop this database.
+ *
+ * @see DropDatabase::__construct() for supported options
+ * @param array $options Additional options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are unsupported on the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function drop(array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DropDatabase($this->databaseName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Drop a collection within this database.
+ *
+ * @see DropCollection::__construct() for supported options
+ * @param string $collectionName Collection name
+ * @param array $options Additional options
+ * @return array|object Command result document
+ * @throws UnsupportedException if options are unsupported on the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function dropCollection($collectionName, array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new DropCollection($this->databaseName, $collectionName, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Returns the database name.
+ *
+ * @return string
+ */
+ public function getDatabaseName()
+ {
+ return $this->databaseName;
+ }
+
+ /**
+ * Return the Manager.
+ *
+ * @return Manager
+ */
+ public function getManager()
+ {
+ return $this->manager;
+ }
+
+ /**
+ * Return the read concern for this database.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+ * @return ReadConcern
+ */
+ public function getReadConcern()
+ {
+ return $this->readConcern;
+ }
+
+ /**
+ * Return the read preference for this database.
+ *
+ * @return ReadPreference
+ */
+ public function getReadPreference()
+ {
+ return $this->readPreference;
+ }
+
+ /**
+ * Return the type map for this database.
+ *
+ * @return array
+ */
+ public function getTypeMap()
+ {
+ return $this->typeMap;
+ }
+
+ /**
+ * Return the write concern for this database.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+ * @return WriteConcern
+ */
+ public function getWriteConcern()
+ {
+ return $this->writeConcern;
+ }
+
+ /**
+ * Returns information for all collections in this database.
+ *
+ * @see ListCollections::__construct() for supported options
+ * @param array $options
+ * @return CollectionInfoIterator
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function listCollections(array $options = [])
+ {
+ $operation = new ListCollections($this->databaseName, $options);
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Modifies a collection or view.
+ *
+ * @see ModifyCollection::__construct() for supported options
+ * @param string $collectionName Collection or view to modify
+ * @param array $collectionOptions Collection or view options to assign
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function modifyCollection($collectionName, array $collectionOptions, array $options = [])
+ {
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+ if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ $options['writeConcern'] = $this->writeConcern;
+ }
+
+ $operation = new ModifyCollection($this->databaseName, $collectionName, $collectionOptions, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Select a collection within this database.
+ *
+ * @see Collection::__construct() for supported options
+ * @param string $collectionName Name of the collection to select
+ * @param array $options Collection constructor options
+ * @return Collection
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function selectCollection($collectionName, array $options = [])
+ {
+ $options += [
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+
+ return new Collection($this->manager, $this->databaseName, $collectionName, $options);
+ }
+
+ /**
+ * Select a GridFS bucket within this database.
+ *
+ * @see Bucket::__construct() for supported options
+ * @param array $options Bucket constructor options
+ * @return Bucket
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function selectGridFSBucket(array $options = [])
+ {
+ $options += [
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+
+ return new Bucket($this->manager, $this->databaseName, $options);
+ }
+
+ /**
+ * Create a change stream for watching changes to the database.
+ *
+ * @see Watch::__construct() for supported options
+ * @param array $pipeline List of pipeline operations
+ * @param array $options Command options
+ * @return ChangeStream
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function watch(array $pipeline = [], array $options = [])
+ {
+ if ( ! isset($options['readPreference'])) {
+ $options['readPreference'] = $this->readPreference;
+ }
+
+ $server = $this->manager->selectServer($options['readPreference']);
+
+ if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ $options['readConcern'] = $this->readConcern;
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $operation = new Watch($this->manager, $this->databaseName, null, $pipeline, $options);
+
+ return $operation->execute($server);
+ }
+
+ /**
+ * Get a clone of this database with different options.
+ *
+ * @see Database::__construct() for supported options
+ * @param array $options Database constructor options
+ * @return Database
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function withOptions(array $options = [])
+ {
+ $options += [
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+
+ return new Database($this->manager, $this->databaseName, $options);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a delete operation.
+ */
+class DeleteResult
+{
+ private $writeResult;
+ private $isAcknowledged;
+
+ /**
+ * Constructor.
+ *
+ * @param WriteResult $writeResult
+ */
+ public function __construct(WriteResult $writeResult)
+ {
+ $this->writeResult = $writeResult;
+ $this->isAcknowledged = $writeResult->isAcknowledged();
+ }
+
+ /**
+ * Return the number of documents that were deleted.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see DeleteResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getDeletedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getDeletedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return whether this delete was acknowledged by the server.
+ *
+ * If the delete was not acknowledged, other fields from the WriteResult
+ * (e.g. deletedCount) will be undefined.
+ *
+ * @return boolean
+ */
+ public function isAcknowledged()
+ {
+ return $this->isAcknowledged;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+class BadMethodCallException extends \BadMethodCallException implements Exception
+{
+ /**
+ * Thrown when a mutable method is invoked on an immutable object.
+ *
+ * @param string $class Class name
+ * @return self
+ */
+ public static function classIsImmutable($class)
+ {
+ return new static(sprintf('%s is immutable', $class));
+ }
+
+ /**
+ * Thrown when accessing a result field on an unacknowledged write result.
+ *
+ * @param string $method Method name
+ * @return self
+ */
+ public static function unacknowledgedWriteResultAccess($method)
+ {
+ return new static(sprintf('%s should not be called for an unacknowledged write result', $method));
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+interface Exception extends \MongoDB\Driver\Exception\Exception
+{
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+class InvalidArgumentException extends \MongoDB\Driver\Exception\InvalidArgumentException implements Exception
+{
+ /**
+ * 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
+ * @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)));
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+class ResumeTokenException extends \Exception
+{
+ /**
+ * Thrown when a resume token has an invalid type.
+ *
+ * @param mixed $value Actual value (used to derive the type)
+ * @return self
+ */
+ public static function invalidType($value)
+ {
+ return new static(sprintf('Expected resume token to have type "array or object" but found "%s"', gettype($value)));
+ }
+
+ /**
+ * Thrown when a resume token is not found in a change document.
+ *
+ * @return self
+ */
+ public static function notFound()
+ {
+ return new static('Resume token not found in change document');
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+class RuntimeException extends \MongoDB\Driver\Exception\RuntimeException implements Exception
+{
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+class UnexpectedValueException extends \MongoDB\Driver\Exception\UnexpectedValueException implements Exception
+{
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Exception;
+
+class UnsupportedException extends RuntimeException
+{
+ /**
+ * Thrown when array filters are not supported by a server.
+ *
+ * @return self
+ */
+ public static function arrayFiltersNotSupported()
+ {
+ return new static('Array filters are not supported by the server executing this operation');
+ }
+
+ /**
+ * Thrown when collations are not supported by a server.
+ *
+ * @return self
+ */
+ public static function collationNotSupported()
+ {
+ return new static('Collations are not supported by the server executing this operation');
+ }
+
+ /**
+ * Thrown when explain is not supported by a server.
+ *
+ * @return self
+ */
+ public static function explainNotSupported()
+ {
+ return new static('Explain is not supported by the server executing this operation');
+ }
+
+ /**
+ * Thrown when a command's readConcern option is not supported by a server.
+ *
+ * @return self
+ */
+ public static function readConcernNotSupported()
+ {
+ return new static('Read concern is not supported by the server executing this command');
+ }
+
+ /**
+ * Thrown when a command's writeConcern option is not supported by a server.
+ *
+ * @return self
+ */
+ public static function writeConcernNotSupported()
+ {
+ return new static('Write concern is not supported by the server executing this command');
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS;
+
+use MongoDB\Collection;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\GridFS\Exception\CorruptFileException;
+use MongoDB\GridFS\Exception\FileNotFoundException;
+use MongoDB\Operation\Find;
+use stdClass;
+
+/**
+ * Bucket provides a public API for interacting with the GridFS files and chunks
+ * collections.
+ *
+ * @api
+ */
+class Bucket
+{
+ private static $defaultBucketName = 'fs';
+ private static $defaultChunkSizeBytes = 261120;
+ private static $defaultTypeMap = [
+ 'array' => 'MongoDB\Model\BSONArray',
+ 'document' => 'MongoDB\Model\BSONDocument',
+ 'root' => 'MongoDB\Model\BSONDocument',
+ ];
+ private static $streamWrapperProtocol = 'gridfs';
+
+ private $collectionWrapper;
+ private $databaseName;
+ private $manager;
+ private $bucketName;
+ private $disableMD5;
+ private $chunkSizeBytes;
+ private $readConcern;
+ private $readPreference;
+ private $typeMap;
+ private $writeConcern;
+
+ /**
+ * Constructs a GridFS bucket.
+ *
+ * Supported options:
+ *
+ * * bucketName (string): The bucket name, which will be used as a prefix
+ * for the files and chunks collections. Defaults to "fs".
+ *
+ * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
+ * 261120 (i.e. 255 KiB).
+ *
+ * * disableMD5 (boolean): When true, no MD5 sum will be generated for
+ * each stored file. Defaults to "false".
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): Read concern.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+ *
+ * * typeMap (array): Default type map for cursors and BSON documents.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * @param Manager $manager Manager instance from the driver
+ * @param string $databaseName Database name
+ * @param array $options Bucket options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct(Manager $manager, $databaseName, array $options = [])
+ {
+ $options += [
+ 'bucketName' => self::$defaultBucketName,
+ 'chunkSizeBytes' => self::$defaultChunkSizeBytes,
+ 'disableMD5' => false,
+ ];
+
+ if (isset($options['bucketName']) && ! is_string($options['bucketName'])) {
+ throw InvalidArgumentException::invalidType('"bucketName" option', $options['bucketName'], 'string');
+ }
+
+ if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
+ throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
+ }
+
+ if (isset($options['chunkSizeBytes']) && $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'])) {
+ throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ $this->manager = $manager;
+ $this->databaseName = (string) $databaseName;
+ $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();
+
+ $collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'typeMap' => 1, 'writeConcern' => 1]);
+
+ $this->collectionWrapper = new CollectionWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions);
+ $this->registerStreamWrapper();
+ }
+
+ /**
+ * Return internal properties for debugging purposes.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'bucketName' => $this->bucketName,
+ 'databaseName' => $this->databaseName,
+ 'manager' => $this->manager,
+ 'chunkSizeBytes' => $this->chunkSizeBytes,
+ 'readConcern' => $this->readConcern,
+ 'readPreference' => $this->readPreference,
+ 'typeMap' => $this->typeMap,
+ 'writeConcern' => $this->writeConcern,
+ ];
+ }
+
+ /**
+ * Delete a file from the GridFS bucket.
+ *
+ * If the files collection document is not found, this method will still
+ * attempt to delete orphaned chunks.
+ *
+ * @param mixed $id File ID
+ * @throws FileNotFoundException if no file could be selected
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function delete($id)
+ {
+ $file = $this->collectionWrapper->findFileById($id);
+ $this->collectionWrapper->deleteFileAndChunksById($id);
+
+ if ($file === null) {
+ throw FileNotFoundException::byId($id, $this->getFilesNamespace());
+ }
+ }
+
+ /**
+ * Writes the contents of a GridFS file to a writable stream.
+ *
+ * @param mixed $id File ID
+ * @param resource $destination Writable Stream
+ * @throws FileNotFoundException if no file could be selected
+ * @throws InvalidArgumentException if $destination is not a stream
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function downloadToStream($id, $destination)
+ {
+ if ( ! is_resource($destination) || get_resource_type($destination) != "stream") {
+ throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
+ }
+
+ stream_copy_to_stream($this->openDownloadStream($id), $destination);
+ }
+
+ /**
+ * Writes the contents of a GridFS file, which is selected by name and
+ * revision, to a writable stream.
+ *
+ * Supported options:
+ *
+ * * revision (integer): Which revision (i.e. documents with the same
+ * filename and different uploadDate) of the file to retrieve. Defaults
+ * to -1 (i.e. the most recent revision).
+ *
+ * Revision numbers are defined as follows:
+ *
+ * * 0 = the original stored file
+ * * 1 = the first revision
+ * * 2 = the second revision
+ * * etc…
+ * * -2 = the second most recent revision
+ * * -1 = the most recent revision
+ *
+ * @param string $filename Filename
+ * @param resource $destination Writable Stream
+ * @param array $options Download options
+ * @throws FileNotFoundException if no file could be selected
+ * @throws InvalidArgumentException if $destination is not a stream
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function downloadToStreamByName($filename, $destination, array $options = [])
+ {
+ if ( ! is_resource($destination) || get_resource_type($destination) != "stream") {
+ throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
+ }
+
+ stream_copy_to_stream($this->openDownloadStreamByName($filename, $options), $destination);
+ }
+
+ /**
+ * Drops the files and chunks collections associated with this GridFS
+ * bucket.
+ *
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function drop()
+ {
+ $this->collectionWrapper->dropCollections();
+ }
+
+ /**
+ * Finds documents from the GridFS bucket's files collection matching the
+ * query.
+ *
+ * @see Find::__construct() for supported options
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Additional options
+ * @return Cursor
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function find($filter = [], array $options = [])
+ {
+ return $this->collectionWrapper->findFiles($filter, $options);
+ }
+
+ /**
+ * Finds a single document from the GridFS bucket's files collection
+ * matching the query.
+ *
+ * @see FindOne::__construct() for supported options
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Additional options
+ * @return array|object|null
+ * @throws UnsupportedException if options are not supported by the selected server
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function findOne($filter = [], array $options = [])
+ {
+ return $this->collectionWrapper->findOneFile($filter, $options);
+ }
+
+ /**
+ * Return the bucket name.
+ *
+ * @return string
+ */
+ public function getBucketName()
+ {
+ return $this->bucketName;
+ }
+
+ /**
+ * Return the chunks collection.
+ *
+ * @return Collection
+ */
+ public function getChunksCollection()
+ {
+ return $this->collectionWrapper->getChunksCollection();
+ }
+
+ /**
+ * Return the chunk size in bytes.
+ *
+ * @return integer
+ */
+ public function getChunkSizeBytes()
+ {
+ return $this->chunkSizeBytes;
+ }
+
+ /**
+ * Return the database name.
+ *
+ * @return string
+ */
+ public function getDatabaseName()
+ {
+ return $this->databaseName;
+ }
+
+ /**
+ * Gets the file document of the GridFS file associated with a stream.
+ *
+ * @param resource $stream GridFS stream
+ * @return array|object
+ * @throws InvalidArgumentException if $stream is not a GridFS stream
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function getFileDocumentForStream($stream)
+ {
+ $file = $this->getRawFileDocumentForStream($stream);
+
+ // Filter the raw document through the specified type map
+ return \MongoDB\apply_type_map_to_document($file, $this->typeMap);
+ }
+
+ /**
+ * Gets the file document's ID of the GridFS file associated with a stream.
+ *
+ * @param resource $stream GridFS stream
+ * @return mixed
+ * @throws CorruptFileException if the file "_id" field does not exist
+ * @throws InvalidArgumentException if $stream is not a GridFS stream
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function getFileIdForStream($stream)
+ {
+ $file = $this->getRawFileDocumentForStream($stream);
+
+ /* Filter the raw document through the specified type map, but override
+ * the root type so we can reliably access the ID.
+ */
+ $typeMap = ['root' => 'stdClass'] + $this->typeMap;
+ $file = \MongoDB\apply_type_map_to_document($file, $typeMap);
+
+ if ( ! isset($file->_id) && ! property_exists($file, '_id')) {
+ throw new CorruptFileException('file._id does not exist');
+ }
+
+ return $file->_id;
+ }
+
+ /**
+ * Return the files collection.
+ *
+ * @return Collection
+ */
+ public function getFilesCollection()
+ {
+ return $this->collectionWrapper->getFilesCollection();
+ }
+
+ /**
+ * Return the read concern for this GridFS bucket.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+ * @return ReadConcern
+ */
+ public function getReadConcern()
+ {
+ return $this->readConcern;
+ }
+
+ /**
+ * Return the read preference for this GridFS bucket.
+ *
+ * @return ReadPreference
+ */
+ public function getReadPreference()
+ {
+ return $this->readPreference;
+ }
+
+ /**
+ * Return the type map for this GridFS bucket.
+ *
+ * @return array
+ */
+ public function getTypeMap()
+ {
+ return $this->typeMap;
+ }
+
+ /**
+ * Return the write concern for this GridFS bucket.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+ * @return WriteConcern
+ */
+ public function getWriteConcern()
+ {
+ return $this->writeConcern;
+ }
+
+ /**
+ * Opens a readable stream for reading a GridFS file.
+ *
+ * @param mixed $id File ID
+ * @return resource
+ * @throws FileNotFoundException if no file could be selected
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function openDownloadStream($id)
+ {
+ $file = $this->collectionWrapper->findFileById($id);
+
+ if ($file === null) {
+ throw FileNotFoundException::byId($id, $this->getFilesNamespace());
+ }
+
+ return $this->openDownloadStreamByFile($file);
+ }
+
+ /**
+ * Opens a readable stream stream to read a GridFS file, which is selected
+ * by name and revision.
+ *
+ * Supported options:
+ *
+ * * revision (integer): Which revision (i.e. documents with the same
+ * filename and different uploadDate) of the file to retrieve. Defaults
+ * to -1 (i.e. the most recent revision).
+ *
+ * Revision numbers are defined as follows:
+ *
+ * * 0 = the original stored file
+ * * 1 = the first revision
+ * * 2 = the second revision
+ * * etc…
+ * * -2 = the second most recent revision
+ * * -1 = the most recent revision
+ *
+ * @param string $filename Filename
+ * @param array $options Download options
+ * @return resource
+ * @throws FileNotFoundException if no file could be selected
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function openDownloadStreamByName($filename, array $options = [])
+ {
+ $options += ['revision' => -1];
+
+ $file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $options['revision']);
+
+ if ($file === null) {
+ throw FileNotFoundException::byFilenameAndRevision($filename, $options['revision'], $this->getFilesNamespace());
+ }
+
+ return $this->openDownloadStreamByFile($file);
+ }
+
+ /**
+ * Opens a writable stream for writing a GridFS file.
+ *
+ * Supported options:
+ *
+ * * _id (mixed): File document identifier. Defaults to a new ObjectId.
+ *
+ * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
+ * bucket's chunk size.
+ *
+ * * disableMD5 (boolean): When true, no MD5 sum will be generated for
+ * the stored file. Defaults to "false".
+ *
+ * * metadata (document): User data for the "metadata" field of the files
+ * collection document.
+ *
+ * @param string $filename Filename
+ * @param array $options Upload options
+ * @return resource
+ */
+ public function openUploadStream($filename, array $options = [])
+ {
+ $options += ['chunkSizeBytes' => $this->chunkSizeBytes];
+
+ $path = $this->createPathForUpload();
+ $context = stream_context_create([
+ self::$streamWrapperProtocol => [
+ 'collectionWrapper' => $this->collectionWrapper,
+ 'filename' => $filename,
+ 'options' => $options,
+ ],
+ ]);
+
+ return fopen($path, 'w', false, $context);
+ }
+
+ /**
+ * Renames the GridFS file with the specified ID.
+ *
+ * @param mixed $id File ID
+ * @param string $newFilename New filename
+ * @throws FileNotFoundException if no file could be selected
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function rename($id, $newFilename)
+ {
+ $updateResult = $this->collectionWrapper->updateFilenameForId($id, $newFilename);
+
+ if ($updateResult->getModifiedCount() === 1) {
+ return;
+ }
+
+ /* If the update resulted in no modification, it's possible that the
+ * file did not exist, in which case we must raise an error. Checking
+ * the write result's matched count will be most efficient, but fall
+ * back to a findOne operation if necessary (i.e. legacy writes).
+ */
+ $found = $updateResult->getMatchedCount() !== null
+ ? $updateResult->getMatchedCount() === 1
+ : $this->collectionWrapper->findFileById($id) !== null;
+
+ if ( ! $found) {
+ throw FileNotFoundException::byId($id, $this->getFilesNamespace());
+ }
+ }
+
+ /**
+ * Writes the contents of a readable stream to a GridFS file.
+ *
+ * Supported options:
+ *
+ * * _id (mixed): File document identifier. Defaults to a new ObjectId.
+ *
+ * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
+ * bucket's chunk size.
+ *
+ * * disableMD5 (boolean): When true, no MD5 sum will be generated for
+ * the stored file. Defaults to "false".
+ *
+ * * metadata (document): User data for the "metadata" field of the files
+ * collection document.
+ *
+ * @param string $filename Filename
+ * @param resource $source Readable stream
+ * @param array $options Stream options
+ * @return mixed ID of the newly created GridFS file
+ * @throws InvalidArgumentException if $source is not a GridFS stream
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function uploadFromStream($filename, $source, array $options = [])
+ {
+ if ( ! is_resource($source) || get_resource_type($source) != "stream") {
+ throw InvalidArgumentException::invalidType('$source', $source, 'resource');
+ }
+
+ $destination = $this->openUploadStream($filename, $options);
+ stream_copy_to_stream($source, $destination);
+
+ return $this->getFileIdForStream($destination);
+ }
+
+ /**
+ * Creates a path for an existing GridFS file.
+ *
+ * @param stdClass $file GridFS file document
+ * @return string
+ */
+ private function createPathForFile(stdClass $file)
+ {
+ if ( ! is_object($file->_id) || method_exists($file->_id, '__toString')) {
+ $id = (string) $file->_id;
+ } else {
+ $id = \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP(['_id' => $file->_id]));
+ }
+
+ return sprintf(
+ '%s://%s/%s.files/%s',
+ self::$streamWrapperProtocol,
+ urlencode($this->databaseName),
+ urlencode($this->bucketName),
+ urlencode($id)
+ );
+ }
+
+ /**
+ * Creates a path for a new GridFS file, which does not yet have an ID.
+ *
+ * @return string
+ */
+ private function createPathForUpload()
+ {
+ return sprintf(
+ '%s://%s/%s.files',
+ self::$streamWrapperProtocol,
+ urlencode($this->databaseName),
+ urlencode($this->bucketName)
+ );
+ }
+
+ /**
+ * Returns the names of the files collection.
+ *
+ * @return string
+ */
+ private function getFilesNamespace()
+ {
+ return sprintf('%s.%s.files', $this->databaseName, $this->bucketName);
+ }
+
+ /**
+ * Gets the file document of the GridFS file associated with a stream.
+ *
+ * This returns the raw document from the StreamWrapper, which does not
+ * respect the Bucket's type map.
+ *
+ * @param resource $stream GridFS stream
+ * @return stdClass
+ * @throws InvalidArgumentException
+ */
+ private function getRawFileDocumentForStream($stream)
+ {
+ if ( ! is_resource($stream) || get_resource_type($stream) != "stream") {
+ throw InvalidArgumentException::invalidType('$stream', $stream, 'resource');
+ }
+
+ $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, 'MongoDB\Driver\GridFS\StreamWrapper');
+ }
+
+ return $metadata['wrapper_data']->getFile();
+ }
+
+ /**
+ * Opens a readable stream for the GridFS file.
+ *
+ * @param stdClass $file GridFS file document
+ * @return resource
+ */
+ private function openDownloadStreamByFile(stdClass $file)
+ {
+ $path = $this->createPathForFile($file);
+ $context = stream_context_create([
+ self::$streamWrapperProtocol => [
+ 'collectionWrapper' => $this->collectionWrapper,
+ 'file' => $file,
+ ],
+ ]);
+
+ return fopen($path, 'r', false, $context);
+ }
+
+ /**
+ * Registers the GridFS stream wrapper if it is not already registered.
+ */
+ private function registerStreamWrapper()
+ {
+ if (in_array(self::$streamWrapperProtocol, stream_get_wrappers())) {
+ return;
+ }
+
+ StreamWrapper::register(self::$streamWrapperProtocol);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS;
+
+use MongoDB\Collection;
+use MongoDB\UpdateResult;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadPreference;
+use stdClass;
+
+/**
+ * CollectionWrapper abstracts the GridFS files and chunks collections.
+ *
+ * @internal
+ */
+class CollectionWrapper
+{
+ private $bucketName;
+ private $chunksCollection;
+ private $databaseName;
+ private $checkedIndexes = false;
+ private $filesCollection;
+
+ /**
+ * Constructs a GridFS collection wrapper.
+ *
+ * @see Collection::__construct() for supported options
+ * @param Manager $manager Manager instance from the driver
+ * @param string $databaseName Database name
+ * @param string $bucketName Bucket name
+ * @param array $collectionOptions Collection options
+ * @throws InvalidArgumentException
+ */
+ public function __construct(Manager $manager, $databaseName, $bucketName, array $collectionOptions = [])
+ {
+ $this->databaseName = (string) $databaseName;
+ $this->bucketName = (string) $bucketName;
+
+ $this->filesCollection = new Collection($manager, $databaseName, sprintf('%s.files', $bucketName), $collectionOptions);
+ $this->chunksCollection = new Collection($manager, $databaseName, sprintf('%s.chunks', $bucketName), $collectionOptions);
+ }
+
+ /**
+ * Deletes all GridFS chunks for a given file ID.
+ *
+ * @param mixed $id
+ */
+ public function deleteChunksByFilesId($id)
+ {
+ $this->chunksCollection->deleteMany(['files_id' => $id]);
+ }
+
+ /**
+ * Deletes a GridFS file and related chunks by ID.
+ *
+ * @param mixed $id
+ */
+ public function deleteFileAndChunksById($id)
+ {
+ $this->filesCollection->deleteOne(['_id' => $id]);
+ $this->chunksCollection->deleteMany(['files_id' => $id]);
+ }
+
+ /**
+ * Drops the GridFS files and chunks collections.
+ */
+ public function dropCollections()
+ {
+ $this->filesCollection->drop(['typeMap' => []]);
+ $this->chunksCollection->drop(['typeMap' => []]);
+ }
+
+ /**
+ * Finds GridFS chunk documents for a given file ID and optional offset.
+ *
+ * @param mixed $id File ID
+ * @param integer $fromChunk Starting chunk (inclusive)
+ * @return Cursor
+ */
+ public function findChunksByFileId($id, $fromChunk = 0)
+ {
+ return $this->chunksCollection->find(
+ [
+ 'files_id' => $id,
+ 'n' => ['$gte' => $fromChunk],
+ ],
+ [
+ 'sort' => ['n' => 1],
+ 'typeMap' => ['root' => 'stdClass'],
+ ]
+ );
+ }
+
+ /**
+ * Finds a GridFS file document for a given filename and revision.
+ *
+ * Revision numbers are defined as follows:
+ *
+ * * 0 = the original stored file
+ * * 1 = the first revision
+ * * 2 = the second revision
+ * * etc…
+ * * -2 = the second most recent revision
+ * * -1 = the most recent revision
+ *
+ * @see Bucket::downloadToStreamByName()
+ * @see Bucket::openDownloadStreamByName()
+ * @param string $filename
+ * @param integer $revision
+ * @return stdClass|null
+ */
+ public function findFileByFilenameAndRevision($filename, $revision)
+ {
+ $filename = (string) $filename;
+ $revision = (integer) $revision;
+
+ if ($revision < 0) {
+ $skip = abs($revision) - 1;
+ $sortOrder = -1;
+ } else {
+ $skip = $revision;
+ $sortOrder = 1;
+ }
+
+ return $this->filesCollection->findOne(
+ ['filename' => $filename],
+ [
+ 'skip' => $skip,
+ 'sort' => ['uploadDate' => $sortOrder],
+ 'typeMap' => ['root' => 'stdClass'],
+ ]
+ );
+ }
+
+ /**
+ * Finds a GridFS file document for a given ID.
+ *
+ * @param mixed $id
+ * @return stdClass|null
+ */
+ public function findFileById($id)
+ {
+ return $this->filesCollection->findOne(
+ ['_id' => $id],
+ ['typeMap' => ['root' => 'stdClass']]
+ );
+ }
+
+ /**
+ * Finds documents from the GridFS bucket's files collection.
+ *
+ * @see Find::__construct() for supported options
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Additional options
+ * @return Cursor
+ */
+ public function findFiles($filter, array $options = [])
+ {
+ return $this->filesCollection->find($filter, $options);
+ }
+
+ /**
+ * Finds a single document from the GridFS bucket's files collection.
+ *
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Additional options
+ * @return array|object|null
+ */
+ public function findOneFile($filter, array $options = [])
+ {
+ return $this->filesCollection->findOne($filter, $options);
+ }
+
+ /**
+ * Return the bucket name.
+ *
+ * @return string
+ */
+ public function getBucketName()
+ {
+ return $this->bucketName;
+ }
+
+ /**
+ * Return the chunks collection.
+ *
+ * @return Collection
+ */
+ public function getChunksCollection()
+ {
+ return $this->chunksCollection;
+ }
+
+ /**
+ * Return the database name.
+ *
+ * @return string
+ */
+ public function getDatabaseName()
+ {
+ return $this->databaseName;
+ }
+
+ /**
+ * Return the files collection.
+ *
+ * @return Collection
+ */
+ public function getFilesCollection()
+ {
+ return $this->filesCollection;
+ }
+
+ /**
+ * Inserts a document into the chunks collection.
+ *
+ * @param array|object $chunk Chunk document
+ */
+ public function insertChunk($chunk)
+ {
+ if ( ! $this->checkedIndexes) {
+ $this->ensureIndexes();
+ }
+
+ $this->chunksCollection->insertOne($chunk);
+ }
+
+ /**
+ * Inserts a document into the files collection.
+ *
+ * The file document should be inserted after all chunks have been inserted.
+ *
+ * @param array|object $file File document
+ */
+ public function insertFile($file)
+ {
+ if ( ! $this->checkedIndexes) {
+ $this->ensureIndexes();
+ }
+
+ $this->filesCollection->insertOne($file);
+ }
+
+ /**
+ * Updates the filename field in the file document for a given ID.
+ *
+ * @param mixed $id
+ * @param string $filename
+ * @return UpdateResult
+ */
+ public function updateFilenameForId($id, $filename)
+ {
+ return $this->filesCollection->updateOne(
+ ['_id' => $id],
+ ['$set' => ['filename' => (string) $filename]]
+ );
+ }
+
+ /**
+ * Create an index on the chunks collection if it does not already exist.
+ */
+ private function ensureChunksIndex()
+ {
+ foreach ($this->chunksCollection->listIndexes() as $index) {
+ if ($index->isUnique() && $index->getKey() === ['files_id' => 1, 'n' => 1]) {
+ return;
+ }
+ }
+
+ $this->chunksCollection->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
+ }
+
+ /**
+ * Create an index on the files collection if it does not already exist.
+ */
+ private function ensureFilesIndex()
+ {
+ foreach ($this->filesCollection->listIndexes() as $index) {
+ if ($index->getKey() === ['filename' => 1, 'uploadDate' => 1]) {
+ return;
+ }
+ }
+
+ $this->filesCollection->createIndex(['filename' => 1, 'uploadDate' => 1]);
+ }
+
+ /**
+ * Ensure indexes on the files and chunks collections exist.
+ *
+ * This method is called once before the first write operation on a GridFS
+ * bucket. Indexes are only be created if the files collection is empty.
+ */
+ private function ensureIndexes()
+ {
+ if ($this->checkedIndexes) {
+ return;
+ }
+
+ $this->checkedIndexes = true;
+
+ if ( ! $this->isFilesCollectionEmpty()) {
+ return;
+ }
+
+ $this->ensureFilesIndex();
+ $this->ensureChunksIndex();
+ }
+
+ /**
+ * Returns whether the files collection is empty.
+ *
+ * @return boolean
+ */
+ private function isFilesCollectionEmpty()
+ {
+ return null === $this->filesCollection->findOne([], [
+ 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
+ 'projection' => ['_id' => 1],
+ 'typeMap' => [],
+ ]);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS\Exception;
+
+use MongoDB\Exception\RuntimeException;
+
+class CorruptFileException extends RuntimeException
+{
+ /**
+ * Thrown when a chunk is not found for an expected index.
+ *
+ * @param integer $expectedIndex Expected index number
+ * @return self
+ */
+ public static function missingChunk($expectedIndex)
+ {
+ return new static(sprintf('Chunk not found for index "%d"', $expectedIndex));
+ }
+
+ /**
+ * Thrown when a chunk has an unexpected index number.
+ *
+ * @param integer $index Actual index number (i.e. "n" field)
+ * @param integer $expectedIndex Expected index number
+ * @return self
+ */
+ public static function unexpectedIndex($index, $expectedIndex)
+ {
+ return new static(sprintf('Expected chunk to have index "%d" but found "%d"', $expectedIndex, $index));
+ }
+
+ /**
+ * Thrown when a chunk has an unexpected data size.
+ *
+ * @param integer $size Actual size (i.e. "data" field length)
+ * @param integer $expectedSize Expected size
+ * @return self
+ */
+ public static function unexpectedSize($size, $expectedSize)
+ {
+ return new static(sprintf('Expected chunk to have size "%d" but found "%d"', $expectedSize, $size));
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS\Exception;
+
+use MongoDB\Exception\RuntimeException;
+
+class FileNotFoundException extends RuntimeException
+{
+ /**
+ * Thrown when a file cannot be found by its filename and revision.
+ *
+ * @param string $filename Filename
+ * @param integer $revision Revision
+ * @param string $namespace Namespace for the files collection
+ * @return self
+ */
+ public static function byFilenameAndRevision($filename, $revision, $namespace)
+ {
+ return new static(sprintf('File with name "%s" and revision "%d" not found in "%s"', $filename, $revision, $namespace));
+ }
+
+ /**
+ * Thrown when a file cannot be found by its ID.
+ *
+ * @param mixed $id File ID
+ * @param string $namespace Namespace for the files collection
+ * @return self
+ */
+ public static function byId($id, $namespace)
+ {
+ $json = \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP(['_id' => $id]));
+
+ return new static(sprintf('File "%s" not found in "%s"', $json, $namespace));
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS;
+
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\GridFS\Exception\CorruptFileException;
+use IteratorIterator;
+use stdClass;
+
+/**
+ * ReadableStream abstracts the process of reading a GridFS file.
+ *
+ * @internal
+ */
+class ReadableStream
+{
+ private $buffer;
+ private $bufferOffset = 0;
+ private $chunkSize;
+ private $chunkOffset = 0;
+ private $chunksIterator;
+ private $collectionWrapper;
+ private $expectedLastChunkSize = 0;
+ private $file;
+ private $length;
+ private $numChunks = 0;
+
+ /**
+ * Constructs a readable GridFS stream.
+ *
+ * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
+ * @param stdClass $file GridFS file document
+ * @throws CorruptFileException
+ */
+ public function __construct(CollectionWrapper $collectionWrapper, stdClass $file)
+ {
+ if ( ! isset($file->chunkSize) || ! is_integer($file->chunkSize) || $file->chunkSize < 1) {
+ throw new CorruptFileException('file.chunkSize is not an integer >= 1');
+ }
+
+ if ( ! isset($file->length) || ! is_integer($file->length) || $file->length < 0) {
+ throw new CorruptFileException('file.length is not an integer > 0');
+ }
+
+ if ( ! isset($file->_id) && ! property_exists($file, '_id')) {
+ throw new CorruptFileException('file._id does not exist');
+ }
+
+ $this->file = $file;
+ $this->chunkSize = (integer) $file->chunkSize;
+ $this->length = (integer) $file->length;
+
+ $this->collectionWrapper = $collectionWrapper;
+
+ if ($this->length > 0) {
+ $this->numChunks = (integer) ceil($this->length / $this->chunkSize);
+ $this->expectedLastChunkSize = ($this->length - (($this->numChunks - 1) * $this->chunkSize));
+ }
+ }
+
+ /**
+ * Return internal properties for debugging purposes.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'bucketName' => $this->collectionWrapper->getBucketName(),
+ 'databaseName' => $this->collectionWrapper->getDatabaseName(),
+ 'file' => $this->file,
+ ];
+ }
+
+ public function close()
+ {
+ // Nothing to do
+ }
+
+ /**
+ * Return the stream's file document.
+ *
+ * @return stdClass
+ */
+ public function getFile()
+ {
+ return $this->file;
+ }
+
+ /**
+ * Return the stream's size in bytes.
+ *
+ * @return integer
+ */
+ public function getSize()
+ {
+ return $this->length;
+ }
+
+ /**
+ * Return whether the current read position is at the end of the stream.
+ *
+ * @return boolean
+ */
+ public function isEOF()
+ {
+ if ($this->chunkOffset === $this->numChunks - 1) {
+ return $this->bufferOffset >= $this->expectedLastChunkSize;
+ }
+
+ return $this->chunkOffset >= $this->numChunks;
+ }
+
+ /**
+ * Read bytes from the stream.
+ *
+ * Note: this method may return a string smaller than the requested length
+ * if data is not available to be read.
+ *
+ * @param integer $length Number of bytes to read
+ * @return string
+ * @throws InvalidArgumentException if $length is negative
+ */
+ public function readBytes($length)
+ {
+ if ($length < 0) {
+ throw new InvalidArgumentException(sprintf('$length must be >= 0; given: %d', $length));
+ }
+
+ if ($this->chunksIterator === null) {
+ $this->initChunksIterator();
+ }
+
+ if ($this->buffer === null && ! $this->initBufferFromCurrentChunk()) {
+ return '';
+ }
+
+ $data = '';
+
+ while (strlen($data) < $length) {
+ if ($this->bufferOffset >= strlen($this->buffer) && ! $this->initBufferFromNextChunk()) {
+ break;
+ }
+
+ $initialDataLength = strlen($data);
+ $data .= substr($this->buffer, $this->bufferOffset, $length - $initialDataLength);
+ $this->bufferOffset += strlen($data) - $initialDataLength;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Seeks the chunk and buffer offsets for the next read operation.
+ *
+ * @param integer $offset
+ * @throws InvalidArgumentException if $offset is out of range
+ */
+ public function seek($offset)
+ {
+ if ($offset < 0 || $offset > $this->file->length) {
+ throw new InvalidArgumentException(sprintf('$offset must be >= 0 and <= %d; given: %d', $this->file->length, $offset));
+ }
+
+ /* Compute the offsets for the chunk and buffer (i.e. chunk data) from
+ * which we will expect to read after seeking. If the chunk offset
+ * changed, we'll also need to reset the buffer.
+ */
+ $lastChunkOffset = $this->chunkOffset;
+ $this->chunkOffset = (integer) floor($offset / $this->chunkSize);
+ $this->bufferOffset = $offset % $this->chunkSize;
+
+ if ($lastChunkOffset === $this->chunkOffset) {
+ return;
+ }
+
+ if ($this->chunksIterator === null) {
+ return;
+ }
+
+ // Clear the buffer since the current chunk will be changed
+ $this->buffer = null;
+
+ /* If we are seeking to a previous chunk, we need to reinitialize the
+ * chunk iterator.
+ */
+ if ($lastChunkOffset > $this->chunkOffset) {
+ $this->chunksIterator = null;
+ return;
+ }
+
+ /* If we are seeking to a subsequent chunk, we do not need to
+ * reinitalize the chunk iterator. Instead, we can simply move forward
+ * to $this->chunkOffset.
+ */
+ $numChunks = $this->chunkOffset - $lastChunkOffset;
+ for ($i = 0; $i < $numChunks; $i++) {
+ $this->chunksIterator->next();
+ }
+ }
+
+ /**
+ * Return the current position of the stream.
+ *
+ * This is the offset within the stream where the next byte would be read.
+ *
+ * @return integer
+ */
+ public function tell()
+ {
+ return ($this->chunkOffset * $this->chunkSize) + $this->bufferOffset;
+ }
+
+ /**
+ * Initialize the buffer to the current chunk's data.
+ *
+ * @return boolean Whether there was a current chunk to read
+ * @throws CorruptFileException if an expected chunk could not be read successfully
+ */
+ private function initBufferFromCurrentChunk()
+ {
+ if ($this->chunkOffset === 0 && $this->numChunks === 0) {
+ return false;
+ }
+
+ if ( ! $this->chunksIterator->valid()) {
+ throw CorruptFileException::missingChunk($this->chunkOffset);
+ }
+
+ $currentChunk = $this->chunksIterator->current();
+
+ if ($currentChunk->n !== $this->chunkOffset) {
+ throw CorruptFileException::unexpectedIndex($currentChunk->n, $this->chunkOffset);
+ }
+
+ $this->buffer = $currentChunk->data->getData();
+
+ $actualChunkSize = strlen($this->buffer);
+
+ $expectedChunkSize = ($this->chunkOffset === $this->numChunks - 1)
+ ? $this->expectedLastChunkSize
+ : $this->chunkSize;
+
+ if ($actualChunkSize !== $expectedChunkSize) {
+ throw CorruptFileException::unexpectedSize($actualChunkSize, $expectedChunkSize);
+ }
+
+ return true;
+ }
+
+ /**
+ * Advance to the next chunk and initialize the buffer to its data.
+ *
+ * @return boolean Whether there was a next chunk to read
+ * @throws CorruptFileException if an expected chunk could not be read successfully
+ */
+ private function initBufferFromNextChunk()
+ {
+ if ($this->chunkOffset === $this->numChunks - 1) {
+ return false;
+ }
+
+ $this->bufferOffset = 0;
+ $this->chunkOffset++;
+ $this->chunksIterator->next();
+
+ return $this->initBufferFromCurrentChunk();
+ }
+
+ /**
+ * Initializes the chunk iterator starting from the current offset.
+ */
+ private function initChunksIterator()
+ {
+ $cursor = $this->collectionWrapper->findChunksByFileId($this->file->_id, $this->chunkOffset);
+
+ $this->chunksIterator = new IteratorIterator($cursor);
+ $this->chunksIterator->rewind();
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS;
+
+use MongoDB\BSON\UTCDateTime;
+use Exception;
+use stdClass;
+
+/**
+ * Stream wrapper for reading and writing a GridFS file.
+ *
+ * @internal
+ * @see Bucket::openUploadStream()
+ * @see Bucket::openDownloadStream()
+ */
+class StreamWrapper
+{
+ /**
+ * @var resource|null Stream context (set by PHP)
+ */
+ public $context;
+
+ private $mode;
+ private $protocol;
+ private $stream;
+
+ /**
+ * Return the stream's file document.
+ *
+ * @return stdClass
+ */
+ public function getFile()
+ {
+ return $this->stream->getFile();
+ }
+
+ /**
+ * Register the GridFS stream wrapper.
+ *
+ * @param string $protocol Protocol to use for stream_wrapper_register()
+ */
+ public static function register($protocol = 'gridfs')
+ {
+ if (in_array($protocol, stream_get_wrappers())) {
+ stream_wrapper_unregister($protocol);
+ }
+
+ stream_wrapper_register($protocol, get_called_class(), \STREAM_IS_URL);
+ }
+
+ /**
+ * Closes the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-close.php
+ */
+ public function stream_close()
+ {
+ $this->stream->close();
+ }
+
+ /**
+ * Returns whether the file pointer is at the end of the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+ * @return boolean
+ */
+ public function stream_eof()
+ {
+ if ( ! $this->stream instanceof ReadableStream) {
+ return false;
+ }
+
+ return $this->stream->isEOF();
+ }
+
+ /**
+ * Opens the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-open.php
+ * @param string $path Path to the file resource
+ * @param string $mode Mode used to open the file (only "r" and "w" are supported)
+ * @param integer $options Additional flags set by the streams API
+ * @param string $openedPath Not used
+ */
+ public function stream_open($path, $mode, $options, &$openedPath)
+ {
+ $this->initProtocol($path);
+ $this->mode = $mode;
+
+ if ($mode === 'r') {
+ return $this->initReadableStream();
+ }
+
+ if ($mode === 'w') {
+ return $this->initWritableStream();
+ }
+
+ return false;
+ }
+
+ /**
+ * Read bytes from the stream.
+ *
+ * Note: this method may return a string smaller than the requested length
+ * if data is not available to be read.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-read.php
+ * @param integer $length Number of bytes to read
+ * @return string
+ */
+ public function stream_read($length)
+ {
+ if ( ! $this->stream instanceof ReadableStream) {
+ return '';
+ }
+
+ try {
+ return $this->stream->readBytes($length);
+ } catch (Exception $e) {
+ trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), \E_USER_WARNING);
+ return false;
+ }
+ }
+
+ /**
+ * Return the current position of the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+ * @param integer $offset Stream offset to seek to
+ * @param integer $whence One of SEEK_SET, SEEK_CUR, or SEEK_END
+ * @return boolean True if the position was updated and false otherwise
+ */
+ public function stream_seek($offset, $whence = \SEEK_SET)
+ {
+ $size = $this->stream->getSize();
+
+ if ($whence === \SEEK_CUR) {
+ $offset += $this->stream->tell();
+ }
+
+ if ($whence === \SEEK_END) {
+ $offset += $size;
+ }
+
+ // WritableStreams are always positioned at the end of the stream
+ if ($this->stream instanceof WritableStream) {
+ return $offset === $size;
+ }
+
+ if ($offset < 0 || $offset > $size) {
+ return false;
+ }
+
+ $this->stream->seek($offset);
+
+ return true;
+ }
+
+ /**
+ * Return information about the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+ * @return array
+ */
+ public function stream_stat()
+ {
+ $stat = $this->getStatTemplate();
+
+ $stat[2] = $stat['mode'] = $this->stream instanceof ReadableStream
+ ? 0100444 // S_IFREG & S_IRUSR & S_IRGRP & S_IROTH
+ : 0100222; // S_IFREG & S_IWUSR & S_IWGRP & S_IWOTH
+ $stat[7] = $stat['size'] = $this->stream->getSize();
+
+ $file = $this->stream->getFile();
+
+ if (isset($file->uploadDate) && $file->uploadDate instanceof UTCDateTime) {
+ $timestamp = $file->uploadDate->toDateTime()->getTimestamp();
+ $stat[9] = $stat['mtime'] = $timestamp;
+ $stat[10] = $stat['ctime'] = $timestamp;
+ }
+
+ if (isset($file->chunkSize) && is_integer($file->chunkSize)) {
+ $stat[11] = $stat['blksize'] = $file->chunkSize;
+ }
+
+ return $stat;
+ }
+
+ /**
+ * Return the current position of the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+ * @return integer The current position of the stream
+ */
+ public function stream_tell()
+ {
+ return $this->stream->tell();
+ }
+
+ /**
+ * Write bytes to the stream.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-write.php
+ * @param string $data Data to write
+ * @return integer The number of bytes written
+ */
+ public function stream_write($data)
+ {
+ if ( ! $this->stream instanceof WritableStream) {
+ return 0;
+ }
+
+ try {
+ return $this->stream->writeBytes($data);
+ } catch (Exception $e) {
+ trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), \E_USER_WARNING);
+ return false;
+ }
+ }
+
+ /**
+ * Returns a stat template with default values.
+ *
+ * @return array
+ */
+ private function getStatTemplate()
+ {
+ return [
+ 0 => 0, 'dev' => 0,
+ 1 => 0, 'ino' => 0,
+ 2 => 0, 'mode' => 0,
+ 3 => 0, 'nlink' => 0,
+ 4 => 0, 'uid' => 0,
+ 5 => 0, 'gid' => 0,
+ 6 => -1, 'rdev' => -1,
+ 7 => 0, 'size' => 0,
+ 8 => 0, 'atime' => 0,
+ 9 => 0, 'mtime' => 0,
+ 10 => 0, 'ctime' => 0,
+ 11 => -1, 'blksize' => -1,
+ 12 => -1, 'blocks' => -1,
+ ];
+ }
+
+ /**
+ * Initialize the protocol from the given path.
+ *
+ * @see StreamWrapper::stream_open()
+ * @param string $path
+ */
+ private function initProtocol($path)
+ {
+ $parts = explode('://', $path, 2);
+ $this->protocol = $parts[0] ?: 'gridfs';
+ }
+
+ /**
+ * Initialize the internal stream for reading.
+ *
+ * @see StreamWrapper::stream_open()
+ * @return boolean
+ */
+ private function initReadableStream()
+ {
+ $context = stream_context_get_options($this->context);
+
+ $this->stream = new ReadableStream(
+ $context[$this->protocol]['collectionWrapper'],
+ $context[$this->protocol]['file']
+ );
+
+ return true;
+ }
+
+ /**
+ * Initialize the internal stream for writing.
+ *
+ * @see StreamWrapper::stream_open()
+ * @return boolean
+ */
+ private function initWritableStream()
+ {
+ $context = stream_context_get_options($this->context);
+
+ $this->stream = new WritableStream(
+ $context[$this->protocol]['collectionWrapper'],
+ $context[$this->protocol]['filename'],
+ $context[$this->protocol]['options']
+ );
+
+ return true;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\GridFS;
+
+use MongoDB\BSON\Binary;
+use MongoDB\BSON\ObjectId;
+use MongoDB\BSON\UTCDateTime;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use stdClass;
+
+/**
+ * WritableStream abstracts the process of writing a GridFS file.
+ *
+ * @internal
+ */
+class WritableStream
+{
+ private static $defaultChunkSizeBytes = 261120;
+
+ private $buffer = '';
+ private $chunkOffset = 0;
+ private $chunkSize;
+ private $disableMD5;
+ private $collectionWrapper;
+ private $file;
+ private $hashCtx;
+ private $isClosed = false;
+ private $length = 0;
+
+ /**
+ * Constructs a writable GridFS stream.
+ *
+ * Supported options:
+ *
+ * * _id (mixed): File document identifier. Defaults to a new ObjectId.
+ *
+ * * aliases (array of strings): DEPRECATED An array of aliases.
+ * Applications wishing to store aliases should add an aliases field to
+ * the metadata document instead.
+ *
+ * * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
+ * 261120 (i.e. 255 KiB).
+ *
+ * * disableMD5 (boolean): When true, no MD5 sum will be generated.
+ * Defaults to "false".
+ *
+ * * contentType (string): DEPRECATED content type to be stored with the
+ * file. This information should now be added to the metadata.
+ *
+ * * metadata (document): User data for the "metadata" field of the files
+ * collection document.
+ *
+ * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
+ * @param string $filename Filename
+ * @param array $options Upload options
+ * @throws InvalidArgumentException
+ */
+ public function __construct(CollectionWrapper $collectionWrapper, $filename, array $options = [])
+ {
+ $options += [
+ '_id' => new ObjectId,
+ 'chunkSizeBytes' => self::$defaultChunkSizeBytes,
+ 'disableMD5' => false,
+ ];
+
+ if (isset($options['aliases']) && ! \MongoDB\is_string_array($options['aliases'])) {
+ throw InvalidArgumentException::invalidType('"aliases" option', $options['aliases'], 'array of strings');
+ }
+
+ if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
+ throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
+ }
+
+ if (isset($options['chunkSizeBytes']) && $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'])) {
+ throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
+ }
+
+ if (isset($options['contentType']) && ! is_string($options['contentType'])) {
+ throw InvalidArgumentException::invalidType('"contentType" option', $options['contentType'], 'string');
+ }
+
+ if (isset($options['metadata']) && ! is_array($options['metadata']) && ! is_object($options['metadata'])) {
+ throw InvalidArgumentException::invalidType('"metadata" option', $options['metadata'], 'array or object');
+ }
+
+ $this->chunkSize = $options['chunkSizeBytes'];
+ $this->collectionWrapper = $collectionWrapper;
+ $this->disableMD5 = $options['disableMD5'];
+
+ if ( ! $this->disableMD5) {
+ $this->hashCtx = hash_init('md5');
+ }
+
+ $this->file = [
+ '_id' => $options['_id'],
+ 'chunkSize' => $this->chunkSize,
+ 'filename' => (string) $filename,
+ ] + array_intersect_key($options, ['aliases' => 1, 'contentType' => 1, 'metadata' => 1]);
+ }
+
+ /**
+ * Return internal properties for debugging purposes.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'bucketName' => $this->collectionWrapper->getBucketName(),
+ 'databaseName' => $this->collectionWrapper->getDatabaseName(),
+ 'file' => $this->file,
+ ];
+ }
+
+ /**
+ * Closes an active stream and flushes all buffered data to GridFS.
+ */
+ public function close()
+ {
+ if ($this->isClosed) {
+ // TODO: Should this be an error condition? e.g. BadMethodCallException
+ return;
+ }
+
+ if (strlen($this->buffer) > 0) {
+ $this->insertChunkFromBuffer();
+ }
+
+ $this->fileCollectionInsert();
+ $this->isClosed = true;
+ }
+
+ /**
+ * Return the stream's file document.
+ *
+ * @return stdClass
+ */
+ public function getFile()
+ {
+ return (object) $this->file;
+ }
+
+ /**
+ * Return the stream's size in bytes.
+ *
+ * Note: this value will increase as more data is written to the stream.
+ *
+ * @return integer
+ */
+ public function getSize()
+ {
+ return $this->length + strlen($this->buffer);
+ }
+
+ /**
+ * Return the current position of the stream.
+ *
+ * This is the offset within the stream where the next byte would be
+ * written. Since seeking is not supported and writes are appended, this is
+ * always the end of the stream.
+ *
+ * @see WritableStream::getSize()
+ * @return integer
+ */
+ public function tell()
+ {
+ return $this->getSize();
+ }
+
+ /**
+ * Inserts binary data into GridFS via chunks.
+ *
+ * Data will be buffered internally until chunkSizeBytes are accumulated, at
+ * which point a chunk document will be inserted and the buffer reset.
+ *
+ * @param string $data Binary data to write
+ * @return integer
+ */
+ public function writeBytes($data)
+ {
+ if ($this->isClosed) {
+ // TODO: Should this be an error condition? e.g. BadMethodCallException
+ return;
+ }
+
+ $bytesRead = 0;
+
+ while ($bytesRead != strlen($data)) {
+ $initialBufferLength = strlen($this->buffer);
+ $this->buffer .= substr($data, $bytesRead, $this->chunkSize - $initialBufferLength);
+ $bytesRead += strlen($this->buffer) - $initialBufferLength;
+
+ if (strlen($this->buffer) == $this->chunkSize) {
+ $this->insertChunkFromBuffer();
+ }
+ }
+
+ return $bytesRead;
+ }
+
+ private function abort()
+ {
+ try {
+ $this->collectionWrapper->deleteChunksByFilesId($this->file['_id']);
+ } catch (DriverRuntimeException $e) {
+ // We are already handling an error if abort() is called, so suppress this
+ }
+
+ $this->isClosed = true;
+ }
+
+ private function fileCollectionInsert()
+ {
+ $this->file['length'] = $this->length;
+ $this->file['uploadDate'] = new UTCDateTime;
+
+ if ( ! $this->disableMD5) {
+ $this->file['md5'] = hash_final($this->hashCtx);
+ }
+
+ try {
+ $this->collectionWrapper->insertFile($this->file);
+ } catch (DriverRuntimeException $e) {
+ $this->abort();
+
+ throw $e;
+ }
+
+ return $this->file['_id'];
+ }
+
+ private function insertChunkFromBuffer()
+ {
+ if (strlen($this->buffer) == 0) {
+ return;
+ }
+
+ $data = $this->buffer;
+ $this->buffer = '';
+
+ $chunk = [
+ 'files_id' => $this->file['_id'],
+ 'n' => $this->chunkOffset,
+ 'data' => new Binary($data, Binary::TYPE_GENERIC),
+ ];
+
+ if ( ! $this->disableMD5) {
+ hash_update($this->hashCtx, $data);
+ }
+
+ try {
+ $this->collectionWrapper->insertChunk($chunk);
+ } catch (DriverRuntimeException $e) {
+ $this->abort();
+
+ throw $e;
+ }
+
+ $this->length += strlen($data);
+ $this->chunkOffset++;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a multi-document insert operation.
+ */
+class InsertManyResult
+{
+ private $writeResult;
+ private $insertedIds;
+ private $isAcknowledged;
+
+ /**
+ * Constructor.
+ *
+ * @param WriteResult $writeResult
+ * @param mixed[] $insertedIds
+ */
+ public function __construct(WriteResult $writeResult, array $insertedIds)
+ {
+ $this->writeResult = $writeResult;
+ $this->insertedIds = $insertedIds;
+ $this->isAcknowledged = $writeResult->isAcknowledged();
+ }
+
+ /**
+ * Return the number of documents that were inserted.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see InsertManyResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getInsertedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getInsertedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return a map of the inserted documents' IDs.
+ *
+ * The index of each ID in the map corresponds to each document's position
+ * in the bulk operation. If a document had an ID prior to inserting (i.e.
+ * the driver did not generate an ID), the index will contain its "_id"
+ * field value. Any driver-generated ID will be a MongoDB\BSON\ObjectId
+ * instance.
+ *
+ * @return mixed[]
+ */
+ public function getInsertedIds()
+ {
+ return $this->insertedIds;
+ }
+
+ /**
+ * Return whether this insert result was acknowledged by the server.
+ *
+ * If the insert was not acknowledged, other fields from the WriteResult
+ * (e.g. insertedCount) will be undefined.
+ *
+ * @return boolean
+ */
+ public function isAcknowledged()
+ {
+ return $this->writeResult->isAcknowledged();
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a single-document insert operation.
+ */
+class InsertOneResult
+{
+ private $writeResult;
+ private $insertedId;
+ private $isAcknowledged;
+
+ /**
+ * Constructor.
+ *
+ * @param WriteResult $writeResult
+ * @param mixed $insertedId
+ */
+ public function __construct(WriteResult $writeResult, $insertedId)
+ {
+ $this->writeResult = $writeResult;
+ $this->insertedId = $insertedId;
+ $this->isAcknowledged = $writeResult->isAcknowledged();
+ }
+
+ /**
+ * Return the number of documents that were inserted.
+ *
+ * This method should only be called if the write was acknowledged.
+ *
+ * @see InsertOneResult::isAcknowledged()
+ * @return integer
+ * @throws BadMethodCallException is the write result is unacknowledged
+ */
+ public function getInsertedCount()
+ {
+ if ($this->isAcknowledged) {
+ return $this->writeResult->getInsertedCount();
+ }
+
+ throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+ }
+
+ /**
+ * Return the inserted document's ID.
+ *
+ * If the document had an ID prior to inserting (i.e. the driver did not
+ * need to generate an ID), this will contain its "_id". Any
+ * driver-generated ID will be a MongoDB\BSON\ObjectId instance.
+ *
+ * @return mixed
+ */
+ public function getInsertedId()
+ {
+ return $this->insertedId;
+ }
+
+ /**
+ * Return whether this insert was acknowledged by the server.
+ *
+ * If the insert was not acknowledged, other fields from the WriteResult
+ * (e.g. insertedCount) will be undefined.
+ *
+ * If the insert was not acknowledged, other fields from the WriteResult
+ * (e.g. insertedCount) will be undefined and their getter methods should
+ * not be invoked.
+ *
+ * @return boolean
+ */
+ public function isAcknowledged()
+ {
+ return $this->writeResult->isAcknowledged();
+ }
+}
--- /dev/null
+<?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;
+
+use IteratorAggregate;
+use stdClass;
+use Traversable;
+
+/**
+ * Result class for mapReduce command results.
+ *
+ * This class allows for iteration of mapReduce results irrespective of the
+ * output method (e.g. inline, collection) via the IteratorAggregate interface.
+ * It also provides access to command statistics.
+ *
+ * @api
+ * @see \MongoDB\Collection::mapReduce()
+ * @see https://docs.mongodb.com/manual/reference/command/mapReduce/
+ */
+class MapReduceResult implements IteratorAggregate
+{
+ private $getIterator;
+ private $executionTimeMS;
+ private $counts;
+ private $timing;
+
+ /**
+ * Constructor.
+ *
+ * @internal
+ * @param callable $getIterator Callback that returns a Traversable for mapReduce results
+ * @param stdClass $result Result document from the mapReduce command
+ */
+ public function __construct(callable $getIterator, stdClass $result)
+ {
+ $this->getIterator = $getIterator;
+ $this->executionTimeMS = (integer) $result->timeMillis;
+ $this->counts = (array) $result->counts;
+ $this->timing = isset($result->timing) ? (array) $result->timing : [];
+ }
+
+ /**
+ * Returns various count statistics from the mapReduce command.
+ *
+ * @return array
+ */
+ public function getCounts()
+ {
+ return $this->counts;
+ }
+
+ /**
+ * Return the command execution time in milliseconds.
+ *
+ * @return integer
+ */
+ public function getExecutionTimeMS()
+ {
+ return $this->executionTimeMS;
+ }
+
+ /**
+ * Return the mapReduce results as a Traversable.
+ *
+ * @see http://php.net/iteratoraggregate.getiterator
+ * @return Traversable
+ */
+ public function getIterator()
+ {
+ return call_user_func($this->getIterator);
+ }
+
+ /**
+ * Returns various timing statistics from the mapReduce command.
+ *
+ * Note: timing statistics are only available if the mapReduce command's
+ * "verbose" option was true; otherwise, an empty array will be returned.
+ *
+ * @return array
+ */
+ public function getTiming()
+ {
+ return $this->timing;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\Model;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\BSON\Unserializable;
+use ArrayObject;
+use JsonSerializable;
+
+/**
+ * Model class for a BSON array.
+ *
+ * The internal data will be filtered through array_values() during BSON
+ * serialization to ensure that it becomes a BSON array.
+ *
+ * @api
+ */
+class BSONArray extends ArrayObject implements JsonSerializable, Serializable, Unserializable
+{
+ /**
+ * Clone this BSONArray.
+ */
+ public function __clone()
+ {
+ foreach ($this as $key => $value) {
+ $this[$key] = \MongoDB\recursive_copy($value);
+ }
+ }
+
+ /**
+ * Factory method for var_export().
+ *
+ * @see http://php.net/oop5.magic#object.set-state
+ * @see http://php.net/var-export
+ * @param array $properties
+ * @return self
+ */
+ public static function __set_state(array $properties)
+ {
+ $array = new static;
+ $array->exchangeArray($properties);
+
+ return $array;
+ }
+
+ /**
+ * Serialize the array to BSON.
+ *
+ * The array data will be numerically reindexed to ensure that it is stored
+ * as a BSON array.
+ *
+ * @see http://php.net/mongodb-bson-serializable.bsonserialize
+ * @return array
+ */
+ public function bsonSerialize()
+ {
+ return array_values($this->getArrayCopy());
+ }
+
+ /**
+ * Unserialize the document to BSON.
+ *
+ * @see http://php.net/mongodb-bson-unserializable.bsonunserialize
+ * @param array $data Array data
+ */
+ public function bsonUnserialize(array $data)
+ {
+ self::__construct($data);
+ }
+
+ /**
+ * Serialize the array to JSON.
+ *
+ * The array data will be numerically reindexed to ensure that it is stored
+ * as a JSON array.
+ *
+ * @see http://php.net/jsonserializable.jsonserialize
+ * @return array
+ */
+ public function jsonSerialize()
+ {
+ return array_values($this->getArrayCopy());
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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\Model;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\BSON\Unserializable;
+use ArrayObject;
+use JsonSerializable;
+
+/**
+ * Model class for a BSON document.
+ *
+ * The internal data will be cast to an object during BSON serialization to
+ * ensure that it becomes a BSON document.
+ *
+ * @api
+ */
+class BSONDocument extends ArrayObject implements JsonSerializable, Serializable, Unserializable
+{
+ /**
+ * Deep clone this BSONDocument.
+ */
+ public function __clone()
+ {
+ foreach ($this as $key => $value) {
+ $this[$key] = \MongoDB\recursive_copy($value);
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * This overrides the parent constructor to allow property access of entries
+ * by default.
+ *
+ * @see http://php.net/arrayobject.construct
+ */
+ public function __construct($input = [], $flags = ArrayObject::ARRAY_AS_PROPS, $iterator_class = 'ArrayIterator')
+ {
+ parent::__construct($input, $flags, $iterator_class);
+ }
+
+ /**
+ * Factory method for var_export().
+ *
+ * @see http://php.net/oop5.magic#object.set-state
+ * @see http://php.net/var-export
+ * @param array $properties
+ * @return self
+ */
+ public static function __set_state(array $properties)
+ {
+ $document = new static;
+ $document->exchangeArray($properties);
+
+ return $document;
+ }
+
+ /**
+ * Serialize the document to BSON.
+ *
+ * @see http://php.net/mongodb-bson-serializable.bsonserialize
+ * @return object
+ */
+ public function bsonSerialize()
+ {
+ return (object) $this->getArrayCopy();
+ }
+
+ /**
+ * Unserialize the document to BSON.
+ *
+ * @see http://php.net/mongodb-bson-unserializable.bsonunserialize
+ * @param array $data Array data
+ */
+ public function bsonUnserialize(array $data)
+ {
+ parent::__construct($data, ArrayObject::ARRAY_AS_PROPS);
+ }
+
+ /**
+ * Serialize the array to JSON.
+ *
+ * @see http://php.net/jsonserializable.jsonserialize
+ * @return object
+ */
+ public function jsonSerialize()
+ {
+ return (object) $this->getArrayCopy();
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2018 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 MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Model\BSONDocument;
+use Iterator;
+
+/**
+ * Iterator for BSON documents.
+ */
+class BSONIterator implements Iterator
+{
+ private static $bsonSize = 4;
+
+ private $buffer;
+ private $bufferLength;
+ private $current;
+ private $key = 0;
+ private $position = 0;
+ private $options;
+
+ /**
+ * Constructs a BSON Iterator.
+ *
+ * Supported options:
+ *
+ * * typeMap (array): Type map for BSON deserialization.
+ *
+ * @internal
+ * @see http://php.net/manual/en/function.mongodb.bson-tophp.php
+ * @param string $data Concatenated, valid, BSON-encoded documents
+ * @param array $options Iterator options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($data, array $options = [])
+ {
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if ( ! isset($options['typeMap'])) {
+ $options['typeMap'] = [];
+ }
+
+ $this->buffer = $data;
+ $this->bufferLength = strlen($data);
+ $this->options = $options;
+ }
+
+ /**
+ * @see http://php.net/iterator.current
+ * @return mixed
+ */
+ public function current()
+ {
+ return $this->current;
+ }
+
+ /**
+ * @see http://php.net/iterator.key
+ * @return mixed
+ */
+ public function key()
+ {
+ return $this->key;
+ }
+
+ /**
+ * @see http://php.net/iterator.next
+ * @return void
+ */
+ public function next()
+ {
+ $this->key++;
+ $this->current = null;
+ $this->advance();
+ }
+
+ /**
+ * @see http://php.net/iterator.rewind
+ * @return void
+ */
+ public function rewind()
+ {
+ $this->key = 0;
+ $this->position = 0;
+ $this->current = null;
+ $this->advance();
+ }
+
+ /**
+ * @see http://php.net/iterator.valid
+ * @return boolean
+ */
+ public function valid()
+ {
+ return $this->current !== null;
+ }
+
+ private function advance()
+ {
+ if ($this->position === $this->bufferLength) {
+ return;
+ }
+
+ if (($this->bufferLength - $this->position) < self::$bsonSize) {
+ throw new UnexpectedValueException(sprintf('Expected at least %d bytes; %d remaining', self::$bsonSize, $this->bufferLength - $this->position));
+ }
+
+ list(,$documentLength) = unpack('V', substr($this->buffer, $this->position, self::$bsonSize));
+
+ if (($this->bufferLength - $this->position) < $documentLength) {
+ throw new UnexpectedValueException(sprintf('Expected %d bytes; %d remaining', $documentLength, $this->bufferLength - $this->position));
+ }
+
+ $this->current = \MongoDB\BSON\toPHP(substr($this->buffer, $this->position, $documentLength), $this->options['typeMap']);
+ $this->position += $documentLength;
+ }
+}
--- /dev/null
+<?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 Countable;
+use Generator;
+use Iterator;
+use Traversable;
+
+/**
+ * Iterator for wrapping a Traversable and caching its results.
+ *
+ * By caching results, this iterators allows a Traversable to be counted and
+ * rewound multiple times, even if the wrapped object does not natively support
+ * those operations (e.g. MongoDB\Driver\Cursor).
+ *
+ * @internal
+ */
+class CachingIterator implements Countable, Iterator
+{
+ private $items = [];
+ private $iterator;
+ private $iteratorAdvanced = false;
+ private $iteratorExhausted = false;
+
+ /**
+ * Constructor.
+ *
+ * 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.
+ *
+ * @param Traversable $traversable
+ */
+ public function __construct(Traversable $traversable)
+ {
+ $this->iterator = $this->wrapTraversable($traversable);
+ $this->storeCurrentItem();
+ }
+
+ /**
+ * @see http://php.net/countable.count
+ * @return integer
+ */
+ public function count()
+ {
+ $this->exhaustIterator();
+
+ return count($this->items);
+ }
+
+ /**
+ * @see http://php.net/iterator.current
+ * @return mixed
+ */
+ public function current()
+ {
+ return current($this->items);
+ }
+
+ /**
+ * @see http://php.net/iterator.key
+ * @return mixed
+ */
+ public function key()
+ {
+ return key($this->items);
+ }
+
+ /**
+ * @see http://php.net/iterator.next
+ * @return void
+ */
+ public function next()
+ {
+ if ( ! $this->iteratorExhausted) {
+ $this->iterator->next();
+ $this->storeCurrentItem();
+ }
+
+ next($this->items);
+ }
+
+ /**
+ * @see http://php.net/iterator.rewind
+ * @return void
+ */
+ public function rewind()
+ {
+ /* If the iterator has advanced, exhaust it now so that future iteration
+ * can rely on the cache.
+ */
+ if ($this->iteratorAdvanced) {
+ $this->exhaustIterator();
+ }
+
+ reset($this->items);
+ }
+
+ /**
+ * @see http://php.net/iterator.valid
+ * @return boolean
+ */
+ public function valid()
+ {
+ return $this->key() !== null;
+ }
+
+ /**
+ * Ensures that the inner iterator is fully consumed and cached.
+ */
+ private function exhaustIterator()
+ {
+ while ( ! $this->iteratorExhausted) {
+ $this->next();
+ }
+ }
+
+ /**
+ * Stores the current item in the cache.
+ */
+ private function storeCurrentItem()
+ {
+ $key = $this->iterator->key();
+
+ if ($key === null) {
+ return;
+ }
+
+ $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;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 MongoDB\Exception\BadMethodCallException;
+use ArrayAccess;
+
+/**
+ * Collection information model class.
+ *
+ * This class models the collection information returned by the listCollections
+ * command or, for legacy servers, queries on the "system.namespaces"
+ * collection. It provides methods to access options for the collection.
+ *
+ * @api
+ * @see \MongoDB\Database::listCollections()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst
+ */
+class CollectionInfo implements ArrayAccess
+{
+ private $info;
+
+ /**
+ * Constructor.
+ *
+ * @param array $info Collection info
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+
+ /**
+ * Return the collection info as an array.
+ *
+ * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return $this->info;
+ }
+
+ /**
+ * Return the maximum number of documents to keep in the capped collection.
+ *
+ * @return integer|null
+ */
+ public function getCappedMax()
+ {
+ return isset($this->info['options']['max']) ? (integer) $this->info['options']['max'] : null;
+ }
+
+ /**
+ * Return the maximum size (in bytes) of the capped collection.
+ *
+ * @return integer|null
+ */
+ public function getCappedSize()
+ {
+ return isset($this->info['options']['size']) ? (integer) $this->info['options']['size'] : null;
+ }
+
+ /**
+ * Return the collection name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return (string) $this->info['name'];
+ }
+
+ /**
+ * Return the collection options.
+ *
+ * @return array
+ */
+ public function getOptions()
+ {
+ return isset($this->info['options']) ? (array) $this->info['options'] : [];
+ }
+
+ /**
+ * Return whether the collection is a capped collection.
+ *
+ * @return boolean
+ */
+ public function isCapped()
+ {
+ return ! empty($this->info['options']['capped']);
+ }
+
+ /**
+ * Check whether a field exists in the collection information.
+ *
+ * @see http://php.net/arrayaccess.offsetexists
+ * @param mixed $key
+ * @return boolean
+ */
+ public function offsetExists($key)
+ {
+ return array_key_exists($key, $this->info);
+ }
+
+ /**
+ * Return the field's value from the collection information.
+ *
+ * @see http://php.net/arrayaccess.offsetget
+ * @param mixed $key
+ * @return mixed
+ */
+ public function offsetGet($key)
+ {
+ return $this->info[$key];
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayaccess.offsetset
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($key, $value)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayaccess.offsetunset
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($key)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 IteratorIterator;
+
+/**
+ * CollectionInfoIterator for listCollections command results.
+ *
+ * This iterator may be used to wrap a Cursor returned by the listCollections
+ * command.
+ *
+ * @internal
+ * @see \MongoDB\Database::listCollections()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst
+ * @see http://docs.mongodb.org/manual/reference/command/listCollections/
+ */
+class CollectionInfoCommandIterator extends IteratorIterator implements CollectionInfoIterator
+{
+ /**
+ * Return the current element as a CollectionInfo instance.
+ *
+ * @see CollectionInfoIterator::current()
+ * @see http://php.net/iterator.current
+ * @return CollectionInfo
+ */
+ public function current()
+ {
+ return new CollectionInfo(parent::current());
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 Iterator;
+
+/**
+ * CollectionInfoIterator interface.
+ *
+ * This iterator is used for enumerating collections in a database.
+ *
+ * @api
+ * @see \MongoDB\Database::listCollections()
+ */
+interface CollectionInfoIterator extends Iterator
+{
+ /**
+ * Return the current element as a CollectionInfo instance.
+ *
+ * @return CollectionInfo
+ */
+ public function current();
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 MongoDB\Exception\BadMethodCallException;
+use ArrayAccess;
+/**
+ * Database information model class.
+ *
+ * This class models the database information returned by the listDatabases
+ * command. It provides methods to access common database properties.
+ *
+ * @api
+ * @see \MongoDB\Client::listDatabases()
+ * @see http://docs.mongodb.org/manual/reference/command/listDatabases/
+ */
+class DatabaseInfo implements ArrayAccess
+{
+ private $info;
+
+ /**
+ * Constructor.
+ *
+ * @param array $info Database info
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+
+ /**
+ * Return the collection info as an array.
+ *
+ * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return $this->info;
+ }
+
+ /**
+ * Return the database name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return (string) $this->info['name'];
+ }
+
+ /**
+ * Return the databases size on disk (in bytes).
+ *
+ * @return integer
+ */
+ public function getSizeOnDisk()
+ {
+ return (integer) $this->info['sizeOnDisk'];
+ }
+
+ /**
+ * Return whether the database is empty.
+ *
+ * @return boolean
+ */
+ public function isEmpty()
+ {
+ return (boolean) $this->info['empty'];
+ }
+
+ /**
+ * Check whether a field exists in the database information.
+ *
+ * @see http://php.net/arrayaccess.offsetexists
+ * @param mixed $key
+ * @return boolean
+ */
+ public function offsetExists($key)
+ {
+ return array_key_exists($key, $this->info);
+ }
+
+ /**
+ * Return the field's value from the database information.
+ *
+ * @see http://php.net/arrayaccess.offsetget
+ * @param mixed $key
+ * @return mixed
+ */
+ public function offsetGet($key)
+ {
+ return $this->info[$key];
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayaccess.offsetset
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($key, $value)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayaccess.offsetunset
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($key)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 Iterator;
+
+/**
+ * DatabaseInfoIterator interface.
+ *
+ * This iterator is used for enumerating databases on a server.
+ *
+ * @api
+ * @see \MongoDB\Client::listDatabases()
+ */
+interface DatabaseInfoIterator extends Iterator
+{
+ /**
+ * Return the current element as a DatabaseInfo instance.
+ *
+ * @return DatabaseInfo
+ */
+ public function current();
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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;
+
+/**
+ * DatabaseInfoIterator for inline listDatabases command results.
+ *
+ * This iterator may be used to wrap the array returned within the listDatabases
+ * command's single-document result.
+ *
+ * @internal
+ * @see \MongoDB\Client::listDatabases()
+ * @see http://docs.mongodb.org/manual/reference/command/listDatabases/
+ */
+class DatabaseInfoLegacyIterator implements DatabaseInfoIterator
+{
+ private $databases;
+
+ /**
+ * Constructor.
+ *
+ * @param array $databases
+ */
+ public function __construct(array $databases)
+ {
+ $this->databases = $databases;
+ }
+
+ /**
+ * Return the current element as a DatabaseInfo instance.
+ *
+ * @see DatabaseInfoIterator::current()
+ * @see http://php.net/iterator.current
+ * @return DatabaseInfo
+ */
+ public function current()
+ {
+ return new DatabaseInfo(current($this->databases));
+ }
+
+ /**
+ * Return the key of the current element.
+ *
+ * @see http://php.net/iterator.key
+ * @return integer
+ */
+ public function key()
+ {
+ return key($this->databases);
+ }
+
+ /**
+ * Move forward to next element.
+ *
+ * @see http://php.net/iterator.next
+ */
+ public function next()
+ {
+ next($this->databases);
+ }
+
+ /**
+ * Rewind the Iterator to the first element.
+ *
+ * @see http://php.net/iterator.rewind
+ */
+ public function rewind()
+ {
+ reset($this->databases);
+ }
+
+ /**
+ * Checks if current position is valid.
+ *
+ * @see http://php.net/iterator.valid
+ * @return boolean
+ */
+ public function valid()
+ {
+ return key($this->databases) !== null;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 MongoDB\Exception\BadMethodCallException;
+use ArrayAccess;
+
+/**
+ * Index information model class.
+ *
+ * This class models the index information returned by the listIndexes command
+ * or, for legacy servers, queries on the "system.indexes" collection. It
+ * provides methods to access common index options, and allows access to other
+ * options through the ArrayAccess interface (write methods are not supported).
+ * For information on keys and index options, see the referenced
+ * db.collection.createIndex() documentation.
+ *
+ * @api
+ * @see \MongoDB\Collection::listIndexes()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst
+ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
+ */
+class IndexInfo implements ArrayAccess
+{
+ private $info;
+
+ /**
+ * Constructor.
+ *
+ * @param array $info Index info
+ */
+ public function __construct(array $info)
+ {
+ $this->info = $info;
+ }
+
+ /**
+ * Return the collection info as an array.
+ *
+ * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return $this->info;
+ }
+
+ /**
+ * Return the index name to allow casting IndexInfo to string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getName();
+ }
+
+ /**
+ * Return the index key.
+ *
+ * @return array
+ */
+ public function getKey()
+ {
+ return (array) $this->info['key'];
+ }
+
+ /**
+ * Return the index name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return (string) $this->info['name'];
+ }
+
+ /**
+ * Return the index namespace (e.g. "db.collection").
+ *
+ * @return string
+ */
+ public function getNamespace()
+ {
+ return (string) $this->info['ns'];
+ }
+
+ /**
+ * Return the index version.
+ *
+ * @return integer
+ */
+ public function getVersion()
+ {
+ return (integer) $this->info['v'];
+ }
+
+ /**
+ * Return whether or not this index is of type 2dsphere.
+ *
+ * @return boolean
+ */
+ public function is2dSphere()
+ {
+ return array_search('2dsphere', $this->getKey(), true) !== false;
+ }
+
+ /**
+ * Return whether or not this index is of type geoHaystack.
+ *
+ * @return boolean
+ */
+ public function isGeoHaystack()
+ {
+ return array_search('geoHaystack', $this->getKey(), true) !== false;
+ }
+
+ /**
+ * Return whether this is a sparse index.
+ *
+ * @see http://docs.mongodb.org/manual/core/index-sparse/
+ * @return boolean
+ */
+ public function isSparse()
+ {
+ return ! empty($this->info['sparse']);
+ }
+
+ /**
+ * Return whether or not this index is of type text.
+ *
+ * @return boolean
+ */
+ public function isText()
+ {
+ return array_search('text', $this->getKey(), true) !== false;
+ }
+
+ /**
+ * Return whether this is a TTL index.
+ *
+ * @see http://docs.mongodb.org/manual/core/index-ttl/
+ * @return boolean
+ */
+ public function isTtl()
+ {
+ return array_key_exists('expireAfterSeconds', $this->info);
+ }
+
+ /**
+ * Return whether this is a unique index.
+ *
+ * @see http://docs.mongodb.org/manual/core/index-unique/
+ * @return boolean
+ */
+ public function isUnique()
+ {
+ return ! empty($this->info['unique']);
+ }
+
+ /**
+ * Check whether a field exists in the index information.
+ *
+ * @see http://php.net/arrayaccess.offsetexists
+ * @param mixed $key
+ * @return boolean
+ */
+ public function offsetExists($key)
+ {
+ return array_key_exists($key, $this->info);
+ }
+
+ /**
+ * Return the field's value from the index information.
+ *
+ * This method satisfies the Enumerating Indexes specification's requirement
+ * that index fields be made accessible under their original names. It may
+ * also be used to access fields that do not have a helper method.
+ *
+ * @see http://php.net/arrayaccess.offsetget
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst#getting-full-index-information
+ * @param mixed $key
+ * @return mixed
+ */
+ public function offsetGet($key)
+ {
+ return $this->info[$key];
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayaccess.offsetset
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($key, $value)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayaccess.offsetunset
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($key)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 Iterator;
+
+/**
+ * IndexInfoIterator interface.
+ *
+ * This iterator is used for enumerating indexes in a collection.
+ *
+ * @api
+ * @see \MongoDB\Collection::listIndexes()
+ */
+interface IndexInfoIterator extends Iterator
+{
+ /**
+ * Return the current element as a IndexInfo instance.
+ *
+ * @return IndexInfo
+ */
+ public function current();
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 IteratorIterator;
+
+/**
+ * IndexInfoIterator for both listIndexes command and legacy query results.
+ *
+ * This common iterator may be used to wrap a Cursor returned by both the
+ * listIndexes command and, for legacy servers, queries on the "system.indexes"
+ * collection.
+ *
+ * @internal
+ * @see \MongoDB\Collection::listIndexes()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst
+ * @see http://docs.mongodb.org/manual/reference/command/listIndexes/
+ * @see http://docs.mongodb.org/manual/reference/system-collections/
+ */
+class IndexInfoIteratorIterator extends IteratorIterator implements IndexInfoIterator
+{
+ /**
+ * Return the current element as an IndexInfo instance.
+ *
+ * @see IndexInfoIterator::current()
+ * @see http://php.net/iterator.current
+ * @return IndexInfo
+ */
+ public function current()
+ {
+ return new IndexInfo(parent::current());
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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 MongoDB\BSON\Serializable;
+use MongoDB\Exception\InvalidArgumentException;
+
+/**
+ * Index input model class.
+ *
+ * This class is used to validate user input for index creation.
+ *
+ * @internal
+ * @see \MongoDB\Collection::createIndexes()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst
+ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
+ */
+class IndexInput implements Serializable
+{
+ private $index;
+
+ /**
+ * Constructor.
+ *
+ * @param array $index Index specification
+ * @throws InvalidArgumentException
+ */
+ public function __construct(array $index)
+ {
+ if ( ! isset($index['key'])) {
+ throw new InvalidArgumentException('Required "key" document is missing from index specification');
+ }
+
+ if ( ! is_array($index['key']) && ! is_object($index['key'])) {
+ throw InvalidArgumentException::invalidType('"key" option', $index['key'], 'array or object');
+ }
+
+ foreach ($index['key'] as $fieldName => $order) {
+ if ( ! is_int($order) && ! is_float($order) && ! is_string($order)) {
+ throw InvalidArgumentException::invalidType(sprintf('order value for "%s" field within "key" option', $fieldName), $order, 'numeric or string');
+ }
+ }
+
+ 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'] = \MongoDB\generate_index_name($index['key']);
+ }
+
+ if ( ! is_string($index['name'])) {
+ throw InvalidArgumentException::invalidType('"name" option', $index['name'], 'string');
+ }
+
+ $this->index = $index;
+ }
+
+ /**
+ * Return the index name.
+ *
+ * @param string
+ */
+ public function __toString()
+ {
+ return $this->index['name'];
+ }
+
+ /**
+ * Serialize the index information to BSON for index creation.
+ *
+ * @see \MongoDB\Collection::createIndexes()
+ * @see http://php.net/mongodb-bson-serializable.bsonserialize
+ * @return array
+ */
+ public function bsonSerialize()
+ {
+ return $this->index;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2016-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 ArrayIterator;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Iterator for applying a type map to documents in inline command results.
+ *
+ * @internal
+ */
+class TypeMapArrayIterator extends ArrayIterator
+{
+ private $typeMap;
+
+ /**
+ * Constructor.
+ *
+ * @param array $documents
+ * @param array $typeMap
+ */
+ public function __construct(array $documents = [], array $typeMap)
+ {
+ parent::__construct($documents);
+
+ $this->typeMap = $typeMap;
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.append
+ * @throws BadMethodCallException
+ */
+ public function append($value)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.asort
+ * @throws BadMethodCallException
+ */
+ public function asort()
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Return the current element with the type map applied to it.
+ *
+ * @see http://php.net/arrayiterator.current
+ * @return array|object
+ */
+ public function current()
+ {
+ return \MongoDB\apply_type_map_to_document(parent::current(), $this->typeMap);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.ksort
+ * @throws BadMethodCallException
+ */
+ public function ksort()
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.natcasesort
+ * @throws BadMethodCallException
+ */
+ public function natcasesort()
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.natsort
+ * @throws BadMethodCallException
+ */
+ public function natsort()
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Return the value from the provided offset with the type map applied.
+ *
+ * @see http://php.net/arrayiterator.offsetget
+ * @param mixed $offset
+ * @return array|object
+ */
+ public function offsetGet($offset)
+ {
+ return \MongoDB\apply_type_map_to_document(parent::offsetGet($offset), $this->typeMap);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.offsetset
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($index, $newval)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.offsetunset
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($index)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.uasort
+ * @throws BadMethodCallException
+ */
+ public function uasort($cmp_function)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+
+ /**
+ * Not supported.
+ *
+ * @see http://php.net/arrayiterator.uksort
+ * @throws BadMethodCallException
+ */
+ public function uksort($cmp_function)
+ {
+ throw BadMethodCallException::classIsImmutable(__CLASS__);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\TypeMapArrayIterator;
+use ArrayIterator;
+use stdClass;
+use Traversable;
+
+/**
+ * Operation for the aggregate command.
+ *
+ * @api
+ * @see \MongoDB\Collection::aggregate()
+ * @see http://docs.mongodb.org/manual/reference/command/aggregate/
+ */
+class Aggregate implements Executable
+{
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForDocumentLevelValidation = 4;
+ private static $wireVersionForReadConcern = 4;
+ private static $wireVersionForWriteConcern = 5;
+
+ private $databaseName;
+ private $collectionName;
+ private $pipeline;
+ private $options;
+
+ /**
+ * Constructs an aggregate command.
+ *
+ * Supported options:
+ *
+ * * allowDiskUse (boolean): Enables writing to temporary files. When set
+ * to true, aggregation stages can write data to the _tmp sub-directory
+ * in the dbPath directory. The default is false.
+ *
+ * * batchSize (integer): The number of documents to return per batch.
+ *
+ * * bypassDocumentValidation (boolean): If true, allows the write to
+ * circumvent document level validation. This only applies when the $out
+ * stage is specified.
+ *
+ * For servers < 3.2, this option is ignored as document level validation
+ * is not available.
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * * comment (string): An arbitrary string to help trace the operation
+ * through the database profiler, currentOp, and logs.
+ *
+ * * explain (boolean): Specifies whether or not to return the information
+ * on the processing of the pipeline.
+ *
+ * * 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.
+ *
+ * * maxTimeMS (integer): The maximum amount of time to allow the query to
+ * run.
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): Read concern. Note that a
+ * "majority" read concern is not compatible with the $out stage.
+ *
+ * This is not supported for server versions < 3.2 and will result in an
+ * exception at execution time if used.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+ *
+ * This option is ignored if the $out stage is specified.
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * typeMap (array): Type map for BSON deserialization. This will be
+ * applied to the returned Cursor (it is not sent to the server).
+ *
+ * * useCursor (boolean): Indicates whether the command will request that
+ * the server provide results using a cursor. The default is true.
+ *
+ * This option allows users to turn off cursors if necessary to aid in
+ * mongod/mongos upgrades.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern. This only
+ * applies when the $out stage is specified.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * Note: Collection-agnostic commands (e.g. $currentOp) may be executed by
+ * specifying null for the collection name.
+ *
+ * @param string $databaseName Database name
+ * @param string|null $collectionName Collection name
+ * @param array $pipeline List of pipeline operations
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, array $pipeline, array $options = [])
+ {
+ $expectedIndex = 0;
+
+ foreach ($pipeline as $i => $operation) {
+ if ($i !== $expectedIndex) {
+ throw new InvalidArgumentException(sprintf('$pipeline is not a list (unexpected index: "%s")', $i));
+ }
+
+ if ( ! is_array($operation) && ! is_object($operation)) {
+ throw InvalidArgumentException::invalidType(sprintf('$pipeline[%d]', $i), $operation, 'array or object');
+ }
+
+ $expectedIndex += 1;
+ }
+
+ $options += [
+ 'allowDiskUse' => false,
+ 'useCursor' => true,
+ ];
+
+ if ( ! is_bool($options['allowDiskUse'])) {
+ throw InvalidArgumentException::invalidType('"allowDiskUse" option', $options['allowDiskUse'], 'boolean');
+ }
+
+ if (isset($options['batchSize']) && ! is_integer($options['batchSize'])) {
+ throw InvalidArgumentException::invalidType('"batchSize" option', $options['batchSize'], 'integer');
+ }
+
+ if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
+ throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
+ }
+
+ if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+ throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
+ }
+
+ if (isset($options['comment']) && ! is_string($options['comment'])) {
+ throw InvalidArgumentException::invalidType('"comment" option', $options['comment'], 'string');
+ }
+
+ if (isset($options['explain']) && ! is_bool($options['explain'])) {
+ throw InvalidArgumentException::invalidType('"explain" option', $options['explain'], 'boolean');
+ }
+
+ if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) {
+ throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], 'string or array or object');
+ }
+
+ if (isset($options['maxAwaitTimeMS']) && ! is_integer($options['maxAwaitTimeMS'])) {
+ throw InvalidArgumentException::invalidType('"maxAwaitTimeMS" option', $options['maxAwaitTimeMS'], 'integer');
+ }
+
+ if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
+ throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if ( ! is_bool($options['useCursor'])) {
+ throw InvalidArgumentException::invalidType('"useCursor" option', $options['useCursor'], 'boolean');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['batchSize']) && ! $options['useCursor']) {
+ throw new InvalidArgumentException('"batchSize" option should not be used if "useCursor" is false');
+ }
+
+ if (isset($options['readConcern']) && $options['readConcern']->isDefault()) {
+ unset($options['readConcern']);
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ if ( ! empty($options['explain'])) {
+ $options['useCursor'] = false;
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = isset($collectionName) ? (string) $collectionName : null;
+ $this->pipeline = $pipeline;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return Traversable
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if collation, read concern, or write concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ if (isset($this->options['readConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ throw UnsupportedException::readConcernNotSupported();
+ }
+
+ if (isset($this->options['writeConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForWriteConcern)) {
+ throw UnsupportedException::writeConcernNotSupported();
+ }
+
+ $hasExplain = ! empty($this->options['explain']);
+ $hasOutStage = \MongoDB\is_last_pipeline_operator_out($this->pipeline);
+
+ $command = $this->createCommand($server);
+ $options = $this->createOptions($hasOutStage, $hasExplain);
+
+ $cursor = ($hasOutStage && ! $hasExplain)
+ ? $server->executeReadWriteCommand($this->databaseName, $command, $options)
+ : $server->executeReadCommand($this->databaseName, $command, $options);
+
+ if ($this->options['useCursor'] || $hasExplain) {
+ if (isset($this->options['typeMap'])) {
+ $cursor->setTypeMap($this->options['typeMap']);
+ }
+
+ return $cursor;
+ }
+
+ $result = current($cursor->toArray());
+
+ if ( ! isset($result->result) || ! is_array($result->result)) {
+ throw new UnexpectedValueException('aggregate command did not return a "result" array');
+ }
+
+ if (isset($this->options['typeMap'])) {
+ return new TypeMapArrayIterator($result->result, $this->options['typeMap']);
+ }
+
+ return new ArrayIterator($result->result);
+ }
+
+ /**
+ * Create the aggregate command.
+ *
+ * @param Server $server
+ * @return Command
+ */
+ private function createCommand(Server $server)
+ {
+ $cmd = [
+ 'aggregate' => isset($this->collectionName) ? $this->collectionName : 1,
+ 'pipeline' => $this->pipeline,
+ ];
+ $cmdOptions = [];
+
+ $cmd['allowDiskUse'] = $this->options['allowDiskUse'];
+
+ if (isset($this->options['bypassDocumentValidation']) && \MongoDB\server_supports_feature($server, self::$wireVersionForDocumentLevelValidation)) {
+ $cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
+ }
+
+ foreach (['comment', 'explain', 'maxTimeMS'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = $this->options[$option];
+ }
+ }
+
+ if (isset($this->options['collation'])) {
+ $cmd['collation'] = (object) $this->options['collation'];
+ }
+
+ if (isset($this->options['hint'])) {
+ $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']) {
+ $cmd['cursor'] = isset($this->options["batchSize"])
+ ? ['batchSize' => $this->options["batchSize"]]
+ : new stdClass;
+ }
+
+ return new Command($cmd, $cmdOptions);
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executereadcommand.php
+ * @see http://php.net/manual/en/mongodb-driver-server.executereadwritecommand.php
+ * @param boolean $hasOutStage
+ * @return array
+ */
+ private function createOptions($hasOutStage, $hasExplain)
+ {
+ $options = [];
+
+ if (isset($this->options['readConcern'])) {
+ $options['readConcern'] = $this->options['readConcern'];
+ }
+
+ if ( ! $hasOutStage && isset($this->options['readPreference'])) {
+ $options['readPreference'] = $this->options['readPreference'];
+ }
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ if ($hasOutStage && ! $hasExplain && isset($this->options['writeConcern'])) {
+ $options['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\BulkWriteResult;
+use MongoDB\Driver\BulkWrite as Bulk;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for executing multiple write operations.
+ *
+ * @api
+ * @see \MongoDB\Collection::bulkWrite()
+ */
+class BulkWrite implements Executable
+{
+ const DELETE_MANY = 'deleteMany';
+ const DELETE_ONE = 'deleteOne';
+ const INSERT_ONE = 'insertOne';
+ const REPLACE_ONE = 'replaceOne';
+ const UPDATE_MANY = 'updateMany';
+ const UPDATE_ONE = 'updateOne';
+
+ private static $wireVersionForArrayFilters = 6;
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForDocumentLevelValidation = 4;
+
+ private $databaseName;
+ private $collectionName;
+ private $operations;
+ private $options;
+ private $isArrayFiltersUsed = false;
+ private $isCollationUsed = false;
+
+ /**
+ * Constructs a bulk write operation.
+ *
+ * Example array structure for all supported operation types:
+ *
+ * [
+ * [ 'deleteMany' => [ $filter, $options ] ],
+ * [ 'deleteOne' => [ $filter, $options ] ],
+ * [ 'insertOne' => [ $document ] ],
+ * [ 'replaceOne' => [ $filter, $replacement, $options ] ],
+ * [ 'updateMany' => [ $filter, $update, $options ] ],
+ * [ 'updateOne' => [ $filter, $update, $options ] ],
+ * ]
+ *
+ * Arguments correspond to the respective Operation classes; however, the
+ * writeConcern option is specified for the top-level bulk write operation
+ * instead of each individual operation.
+ *
+ * Supported options for deleteMany and deleteOne operations:
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * Supported options for replaceOne, updateMany, and updateOne operations:
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * * upsert (boolean): When true, a new document is created if no document
+ * matches the query. The default is false.
+ *
+ * Supported options for updateMany and updateOne operations:
+ *
+ * * arrayFilters (document array): A set of filters specifying to which
+ * array elements an update should apply.
+ *
+ * This is not supported for server versions < 3.6 and will result in an
+ * exception at execution time if used.
+ *
+ * Supported options for the bulk write operation:
+ *
+ * * bypassDocumentValidation (boolean): If true, allows the write to
+ * circumvent document level validation. The default is false.
+ *
+ * For servers < 3.2, this option is ignored as document level validation
+ * is not available.
+ *
+ * * ordered (boolean): If true, when an insert fails, return without
+ * performing the remaining writes. If false, when a write fails,
+ * continue with the remaining writes, if any. The default is true.
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array[] $operations List of write operations
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, array $operations, array $options = [])
+ {
+ if (empty($operations)) {
+ throw new InvalidArgumentException('$operations is empty');
+ }
+
+ $expectedIndex = 0;
+
+ foreach ($operations as $i => $operation) {
+ if ($i !== $expectedIndex) {
+ throw new InvalidArgumentException(sprintf('$operations is not a list (unexpected index: "%s")', $i));
+ }
+
+ if ( ! is_array($operation)) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]', $i), $operation, 'array');
+ }
+
+ if (count($operation) !== 1) {
+ throw new InvalidArgumentException(sprintf('Expected one element in $operation[%d], actually: %d', $i, count($operation)));
+ }
+
+ $type = key($operation);
+ $args = current($operation);
+
+ if ( ! isset($args[0]) && ! array_key_exists(0, $args)) {
+ throw new InvalidArgumentException(sprintf('Missing first argument for $operations[%d]["%s"]', $i, $type));
+ }
+
+ if ( ! is_array($args[0]) && ! is_object($args[0])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][0]', $i, $type), $args[0], 'array or object');
+ }
+
+ switch ($type) {
+ case self::INSERT_ONE:
+ break;
+
+ case self::DELETE_MANY:
+ case self::DELETE_ONE:
+ if ( ! isset($args[1])) {
+ $args[1] = [];
+ }
+
+ if ( ! is_array($args[1])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]', $i, $type), $args[1], 'array');
+ }
+
+ $args[1]['limit'] = ($type === self::DELETE_ONE ? 1 : 0);
+
+ if (isset($args[1]['collation'])) {
+ $this->isCollationUsed = true;
+
+ if ( ! is_array($args[1]['collation']) && ! is_object($args[1]['collation'])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]["collation"]', $i, $type), $args[1]['collation'], 'array or object');
+ }
+ }
+
+ $operations[$i][$type][1] = $args[1];
+
+ break;
+
+ case self::REPLACE_ONE:
+ if ( ! isset($args[1]) && ! array_key_exists(1, $args)) {
+ throw new InvalidArgumentException(sprintf('Missing second argument for $operations[%d]["%s"]', $i, $type));
+ }
+
+ if ( ! is_array($args[1]) && ! is_object($args[1])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]', $i, $type), $args[1], 'array or object');
+ }
+
+ if (\MongoDB\is_first_key_operator($args[1])) {
+ throw new InvalidArgumentException(sprintf('First key in $operations[%d]["%s"][1] is an update operator', $i, $type));
+ }
+
+ if ( ! isset($args[2])) {
+ $args[2] = [];
+ }
+
+ if ( ! is_array($args[2])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]', $i, $type), $args[2], 'array');
+ }
+
+ $args[2]['multi'] = false;
+ $args[2] += ['upsert' => false];
+
+ if (isset($args[2]['collation'])) {
+ $this->isCollationUsed = true;
+
+ if ( ! is_array($args[2]['collation']) && ! is_object($args[2]['collation'])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["collation"]', $i, $type), $args[2]['collation'], 'array or object');
+ }
+ }
+
+ if ( ! is_bool($args[2]['upsert'])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["upsert"]', $i, $type), $args[2]['upsert'], 'boolean');
+ }
+
+ $operations[$i][$type][2] = $args[2];
+
+ break;
+
+ case self::UPDATE_MANY:
+ case self::UPDATE_ONE:
+ if ( ! isset($args[1]) && ! array_key_exists(1, $args)) {
+ throw new InvalidArgumentException(sprintf('Missing second argument for $operations[%d]["%s"]', $i, $type));
+ }
+
+ if ( ! is_array($args[1]) && ! is_object($args[1])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]', $i, $type), $args[1], 'array or object');
+ }
+
+ if ( ! \MongoDB\is_first_key_operator($args[1])) {
+ throw new InvalidArgumentException(sprintf('First key in $operations[%d]["%s"][1] is not an update operator', $i, $type));
+ }
+
+ if ( ! isset($args[2])) {
+ $args[2] = [];
+ }
+
+ if ( ! is_array($args[2])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]', $i, $type), $args[2], 'array');
+ }
+
+ $args[2]['multi'] = ($type === self::UPDATE_MANY);
+ $args[2] += ['upsert' => false];
+
+ if (isset($args[2]['arrayFilters'])) {
+ $this->isArrayFiltersUsed = true;
+
+ if ( ! is_array($args[2]['arrayFilters'])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["arrayFilters"]', $i, $type), $args[2]['arrayFilters'], 'array');
+ }
+ }
+
+ if (isset($args[2]['collation'])) {
+ $this->isCollationUsed = true;
+
+ if ( ! is_array($args[2]['collation']) && ! is_object($args[2]['collation'])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["collation"]', $i, $type), $args[2]['collation'], 'array or object');
+ }
+ }
+
+ if ( ! is_bool($args[2]['upsert'])) {
+ throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["upsert"]', $i, $type), $args[2]['upsert'], 'boolean');
+ }
+
+ $operations[$i][$type][2] = $args[2];
+
+ break;
+
+ default:
+ throw new InvalidArgumentException(sprintf('Unknown operation type "%s" in $operations[%d]', $type, $i));
+ }
+
+ $expectedIndex += 1;
+ }
+
+ $options += ['ordered' => true];
+
+ if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
+ throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
+ }
+
+ if ( ! is_bool($options['ordered'])) {
+ throw InvalidArgumentException::invalidType('"ordered" option', $options['ordered'], 'boolean');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->operations = $operations;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return BulkWriteResult
+ * @throws UnsupportedException if array filters or collation is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if ($this->isArrayFiltersUsed && ! \MongoDB\server_supports_feature($server, self::$wireVersionForArrayFilters)) {
+ throw UnsupportedException::arrayFiltersNotSupported();
+ }
+
+ if ($this->isCollationUsed && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ $options = ['ordered' => $this->options['ordered']];
+
+ if (isset($this->options['bypassDocumentValidation']) && \MongoDB\server_supports_feature($server, self::$wireVersionForDocumentLevelValidation)) {
+ $options['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
+ }
+
+ $bulk = new Bulk($options);
+ $insertedIds = [];
+
+ foreach ($this->operations as $i => $operation) {
+ $type = key($operation);
+ $args = current($operation);
+
+ switch ($type) {
+ case self::DELETE_MANY:
+ case self::DELETE_ONE:
+ $bulk->delete($args[0], $args[1]);
+ break;
+
+ case self::INSERT_ONE:
+ $insertedIds[$i] = $bulk->insert($args[0]);
+ break;
+
+ case self::REPLACE_ONE:
+ case self::UPDATE_MANY:
+ case self::UPDATE_ONE:
+ $bulk->update($args[0], $args[1], $args[2]);
+ }
+ }
+
+ $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createOptions());
+
+ return new BulkWriteResult($writeResult, $insertedIds);
+ }
+
+ /**
+ * Create options for executing the bulk write.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executebulkwrite.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ if (isset($this->options['writeConcern'])) {
+ $options['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for the count command.
+ *
+ * @api
+ * @see \MongoDB\Collection::count()
+ * @see http://docs.mongodb.org/manual/reference/command/count/
+ */
+class Count implements Executable, Explainable
+{
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForReadConcern = 4;
+
+ private $databaseName;
+ private $collectionName;
+ private $filter;
+ private $options;
+
+ /**
+ * Constructs a count command.
+ *
+ * Supported options:
+ *
+ * * collation (document): Collation specification.
+ *
+ * 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.
+ *
+ * * limit (integer): The maximum number of documents to count.
+ *
+ * * maxTimeMS (integer): The maximum amount of time to allow the query to
+ * run.
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): Read concern.
+ *
+ * This is not supported for server versions < 3.2 and will result in an
+ * exception at execution time if used.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * skip (integer): The number of documents to skip before returning the
+ * documents.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, $filter = [], array $options = [])
+ {
+ if ( ! is_array($filter) && ! is_object($filter)) {
+ throw InvalidArgumentException::invalidType('$filter', $filter, 'array or object');
+ }
+
+ if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+ 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 or array or object');
+ }
+
+ if (isset($options['limit']) && ! is_integer($options['limit'])) {
+ throw InvalidArgumentException::invalidType('"limit" option', $options['limit'], 'integer');
+ }
+
+ if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
+ throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['skip']) && ! is_integer($options['skip'])) {
+ throw InvalidArgumentException::invalidType('"skip" option', $options['skip'], 'integer');
+ }
+
+ if (isset($options['readConcern']) && $options['readConcern']->isDefault()) {
+ unset($options['readConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->filter = $filter;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return integer
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if collation or read concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ if (isset($this->options['readConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ throw UnsupportedException::readConcernNotSupported();
+ }
+
+ $cursor = $server->executeReadCommand($this->databaseName, new Command($this->createCommandDocument()), $this->createOptions());
+ $result = current($cursor->toArray());
+
+ // Older server versions may return a float
+ if ( ! isset($result->n) || ! (is_integer($result->n) || is_float($result->n))) {
+ throw new UnexpectedValueException('count command did not return a numeric "n" value');
+ }
+
+ return (integer) $result->n;
+ }
+
+ public function getCommandDocument(Server $server)
+ {
+ return $this->createCommandDocument();
+ }
+
+ /**
+ * Create the count command document.
+ *
+ * @return array
+ */
+ private function createCommandDocument()
+ {
+ $cmd = ['count' => $this->collectionName];
+
+ if ( ! empty($this->filter)) {
+ $cmd['query'] = (object) $this->filter;
+ }
+
+ if (isset($this->options['collation'])) {
+ $cmd['collation'] = (object) $this->options['collation'];
+ }
+
+ if (isset($this->options['hint'])) {
+ $cmd['hint'] = is_array($this->options['hint']) ? (object) $this->options['hint'] : $this->options['hint'];
+ }
+
+ foreach (['limit', 'maxTimeMS', 'skip'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = $this->options[$option];
+ }
+ }
+
+ return $cmd;
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executereadcommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['readConcern'])) {
+ $options['readConcern'] = $this->options['readConcern'];
+ }
+
+ if (isset($this->options['readPreference'])) {
+ $options['readPreference'] = $this->options['readPreference'];
+ }
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for obtaining an exact count of documents in a collection
+ *
+ * @api
+ * @see \MongoDB\Collection::countDocuments()
+ * @see https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst#countdocuments
+ */
+class CountDocuments implements Executable
+{
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForReadConcern = 4;
+
+ private $databaseName;
+ private $collectionName;
+ private $filter;
+ private $options;
+
+ /**
+ * Constructs an aggregate command for counting documents
+ *
+ * Supported options:
+ *
+ * * collation (document): Collation specification.
+ *
+ * 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.
+ *
+ * * limit (integer): The maximum number of documents to count.
+ *
+ * * maxTimeMS (integer): The maximum amount of time to allow the query to
+ * run.
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): Read concern.
+ *
+ * This is not supported for server versions < 3.2 and will result in an
+ * exception at execution time if used.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * skip (integer): The number of documents to skip before returning the
+ * documents.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, $filter, array $options = [])
+ {
+ if ( ! is_array($filter) && ! is_object($filter)) {
+ throw InvalidArgumentException::invalidType('$filter', $filter, 'array or object');
+ }
+
+ if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+ 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 or array or object');
+ }
+
+ if (isset($options['limit']) && ! is_integer($options['limit'])) {
+ throw InvalidArgumentException::invalidType('"limit" option', $options['limit'], 'integer');
+ }
+
+ if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
+ throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['skip']) && ! is_integer($options['skip'])) {
+ throw InvalidArgumentException::invalidType('"skip" option', $options['skip'], 'integer');
+ }
+
+ if (isset($options['readConcern']) && $options['readConcern']->isDefault()) {
+ unset($options['readConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->filter = $filter;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return integer
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if collation or read concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ if (isset($this->options['readConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ throw UnsupportedException::readConcernNotSupported();
+ }
+
+ $cursor = $server->executeReadCommand($this->databaseName, new Command($this->createCommandDocument()), $this->createOptions());
+ $allResults = $cursor->toArray();
+
+ /* If there are no documents to count, the aggregation pipeline has no items to group, and
+ * hence the result is an empty array (PHPLIB-376) */
+ if (count($allResults) == 0) {
+ return 0;
+ }
+
+ $result = current($allResults);
+ if ( ! isset($result->n) || ! (is_integer($result->n) || is_float($result->n))) {
+ throw new UnexpectedValueException('count command did not return a numeric "n" value');
+ }
+
+ return (integer) $result->n;
+ }
+
+ /**
+ * Create the count command document.
+ *
+ * @return array
+ */
+ private function createCommandDocument()
+ {
+ $pipeline = [
+ ['$match' => (object) $this->filter]
+ ];
+
+ if (isset($this->options['skip'])) {
+ $pipeline[] = ['$skip' => $this->options['skip']];
+ }
+
+ if (isset($this->options['limit'])) {
+ $pipeline[] = ['$limit' => $this->options['limit']];
+ }
+
+ $pipeline[] = ['$group' => ['_id' => null, 'n' => ['$sum' => 1]]];
+
+ $cmd = [
+ 'aggregate' => $this->collectionName,
+ 'pipeline' => $pipeline,
+ 'cursor' => (object) [],
+ ];
+
+ if (isset($this->options['collation'])) {
+ $cmd['collation'] = (object) $this->options['collation'];
+ }
+
+ if (isset($this->options['hint'])) {
+ $cmd['hint'] = is_array($this->options['hint']) ? (object) $this->options['hint'] : $this->options['hint'];
+ }
+
+ if (isset($this->options['maxTimeMS'])) {
+ $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
+ }
+
+ return $cmd;
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executereadcommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['readConcern'])) {
+ $options['readConcern'] = $this->options['readConcern'];
+ }
+
+ if (isset($this->options['readPreference'])) {
+ $options['readPreference'] = $this->options['readPreference'];
+ }
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for the create command.
+ *
+ * @api
+ * @see \MongoDB\Database::createCollection()
+ * @see http://docs.mongodb.org/manual/reference/command/create/
+ */
+class CreateCollection implements Executable
+{
+ const USE_POWER_OF_2_SIZES = 1;
+ const NO_PADDING = 2;
+
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForWriteConcern = 5;
+
+ private $databaseName;
+ private $collectionName;
+ private $options = [];
+
+ /**
+ * Constructs a create command.
+ *
+ * Supported options:
+ *
+ * * autoIndexId (boolean): Specify false to disable the automatic creation
+ * of an index on the _id field. For replica sets, this option cannot be
+ * false. The default is true.
+ *
+ * This option has been deprecated since MongoDB 3.2. As of MongoDB 4.0,
+ * this option cannot be false when creating a replicated collection
+ * (i.e. a collection outside of the local database in any mongod mode).
+ *
+ * * capped (boolean): Specify true to create a capped collection. If set,
+ * the size option must also be specified. The default is false.
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * * flags (integer): Options for the MMAPv1 storage engine only. Must be a
+ * bitwise combination CreateCollection::USE_POWER_OF_2_SIZES and
+ * CreateCollection::NO_PADDING. The default is
+ * CreateCollection::USE_POWER_OF_2_SIZES.
+ *
+ * * indexOptionDefaults (document): Default configuration for indexes when
+ * creating the collection.
+ *
+ * * max (integer): The maximum number of documents allowed in the capped
+ * collection. The size option takes precedence over this limit.
+ *
+ * * 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.
+ *
+ * * size (integer): The maximum number of bytes for a capped collection.
+ *
+ * * storageEngine (document): Storage engine options.
+ *
+ * * typeMap (array): Type map for BSON deserialization. This will only be
+ * used for the returned command result document.
+ *
+ * * validationAction (string): Validation action.
+ *
+ * * validationLevel (string): Validation level.
+ *
+ * * validator (document): Validation rules or expressions.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * @see http://source.wiredtiger.com/2.4.1/struct_w_t___s_e_s_s_i_o_n.html#a358ca4141d59c345f401c58501276bbb
+ * @see https://docs.mongodb.org/manual/core/document-validation/
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, array $options = [])
+ {
+ if (isset($options['autoIndexId']) && ! is_bool($options['autoIndexId'])) {
+ throw InvalidArgumentException::invalidType('"autoIndexId" option', $options['autoIndexId'], 'boolean');
+ }
+
+ if (isset($options['capped']) && ! is_bool($options['capped'])) {
+ throw InvalidArgumentException::invalidType('"capped" option', $options['capped'], 'boolean');
+ }
+
+ if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+ throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
+ }
+
+ if (isset($options['flags']) && ! is_integer($options['flags'])) {
+ throw InvalidArgumentException::invalidType('"flags" option', $options['flags'], 'integer');
+ }
+
+ if (isset($options['indexOptionDefaults']) && ! is_array($options['indexOptionDefaults']) && ! is_object($options['indexOptionDefaults'])) {
+ throw InvalidArgumentException::invalidType('"indexOptionDefaults" option', $options['indexOptionDefaults'], 'array or object');
+ }
+
+ if (isset($options['max']) && ! is_integer($options['max'])) {
+ throw InvalidArgumentException::invalidType('"max" option', $options['max'], 'integer');
+ }
+
+ 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'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['size']) && ! is_integer($options['size'])) {
+ throw InvalidArgumentException::invalidType('"size" option', $options['size'], 'integer');
+ }
+
+ if (isset($options['storageEngine']) && ! is_array($options['storageEngine']) && ! is_object($options['storageEngine'])) {
+ throw InvalidArgumentException::invalidType('"storageEngine" option', $options['storageEngine'], 'array or object');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if (isset($options['validationAction']) && ! is_string($options['validationAction'])) {
+ throw InvalidArgumentException::invalidType('"validationAction" option', $options['validationAction'], 'string');
+ }
+
+ if (isset($options['validationLevel']) && ! is_string($options['validationLevel'])) {
+ throw InvalidArgumentException::invalidType('"validationLevel" option', $options['validationLevel'], 'string');
+ }
+
+ if (isset($options['validator']) && ! is_array($options['validator']) && ! is_object($options['validator'])) {
+ throw InvalidArgumentException::invalidType('"validator" option', $options['validator'], 'array or object');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ if (isset($options['autoIndexId'])) {
+ trigger_error('The "autoIndexId" option is deprecated and will be removed in a future release', E_USER_DEPRECATED);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return array|object Command result document
+ * @throws UnsupportedException if collation or write concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ if (isset($this->options['writeConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForWriteConcern)) {
+ throw UnsupportedException::writeConcernNotSupported();
+ }
+
+ $cursor = $server->executeWriteCommand($this->databaseName, $this->createCommand(), $this->createOptions());
+
+ if (isset($this->options['typeMap'])) {
+ $cursor->setTypeMap($this->options['typeMap']);
+ }
+
+ return current($cursor->toArray());
+ }
+
+ /**
+ * Create the create command.
+ *
+ * @return Command
+ */
+ private function createCommand()
+ {
+ $cmd = ['create' => $this->collectionName];
+
+ foreach (['autoIndexId', 'capped', 'flags', 'max', 'maxTimeMS', 'size', 'validationAction', 'validationLevel'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = $this->options[$option];
+ }
+ }
+
+ foreach (['collation', 'indexOptionDefaults', 'storageEngine', 'validator'] as $option) {
+ if (isset($this->options[$option])) {
+ $cmd[$option] = (object) $this->options[$option];
+ }
+ }
+
+ return new Command($cmd);
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ if (isset($this->options['writeConcern'])) {
+ $options['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\BulkWrite as Bulk;
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\IndexInput;
+
+/**
+ * Operation for the createIndexes command.
+ *
+ * @api
+ * @see \MongoDB\Collection::createIndex()
+ * @see \MongoDB\Collection::createIndexes()
+ * @see http://docs.mongodb.org/manual/reference/command/createIndexes/
+ */
+class CreateIndexes implements Executable
+{
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForWriteConcern = 5;
+
+ private $databaseName;
+ private $collectionName;
+ private $indexes = [];
+ private $isCollationUsed = false;
+ private $options = [];
+
+ /**
+ * Constructs a createIndexes command.
+ *
+ * Supported options:
+ *
+ * * 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.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array[] $indexes List of index specifications
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, array $indexes, array $options = [])
+ {
+ if (empty($indexes)) {
+ throw new InvalidArgumentException('$indexes is empty');
+ }
+
+ $expectedIndex = 0;
+
+ foreach ($indexes as $i => $index) {
+ if ($i !== $expectedIndex) {
+ throw new InvalidArgumentException(sprintf('$indexes is not a list (unexpected index: "%s")', $i));
+ }
+
+ if ( ! is_array($index)) {
+ throw InvalidArgumentException::invalidType(sprintf('$index[%d]', $i), $index, 'array');
+ }
+
+ if ( ! isset($index['ns'])) {
+ $index['ns'] = $databaseName . '.' . $collectionName;
+ }
+
+ if (isset($index['collation'])) {
+ $this->isCollationUsed = true;
+ }
+
+ $this->indexes[] = new IndexInput($index);
+
+ $expectedIndex += 1;
+ }
+
+ 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'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return string[] The names of the created indexes
+ * @throws UnsupportedException if collation or write concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if ($this->isCollationUsed && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ if (isset($this->options['writeConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForWriteConcern)) {
+ throw UnsupportedException::writeConcernNotSupported();
+ }
+
+ $this->executeCommand($server);
+
+ return array_map(function(IndexInput $index) { return (string) $index; }, $this->indexes);
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ if (isset($this->options['writeConcern'])) {
+ $options['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $options;
+ }
+
+ /**
+ * Create one or more indexes for the collection using the createIndexes
+ * command.
+ *
+ * @param Server $server
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ private function executeCommand(Server $server)
+ {
+ $cmd = [
+ 'createIndexes' => $this->collectionName,
+ 'indexes' => $this->indexes,
+ ];
+
+ if (isset($this->options['maxTimeMS'])) {
+ $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
+ }
+
+ $server->executeWriteCommand($this->databaseName, new Command($cmd), $this->createOptions());
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Exception\InvalidArgumentException;
+
+/**
+ * Operation for executing a database command.
+ *
+ * @api
+ * @see \MongoDB\Database::command()
+ */
+class DatabaseCommand implements Executable
+{
+ private $databaseName;
+ private $command;
+ private $options;
+
+ /**
+ * Constructs a command.
+ *
+ * Supported options:
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): The read preference to
+ * use when executing the command. This may be used when issuing the
+ * command to a replica set or mongos node to ensure that the driver sets
+ * the wire protocol accordingly or adds the read preference to the
+ * command document, respectively.
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * typeMap (array): Type map for BSON deserialization. This will be
+ * applied to the returned Cursor (it is not sent to the server).
+ *
+ * @param string $databaseName Database name
+ * @param array|object $command Command document
+ * @param array $options Options for command execution
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $command, array $options = [])
+ {
+ if ( ! is_array($command) && ! is_object($command)) {
+ throw InvalidArgumentException::invalidType('$command', $command, 'array or object');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->command = ($command instanceof Command) ? $command : new Command($command);
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return Cursor
+ */
+ public function execute(Server $server)
+ {
+ $cursor = $server->executeCommand($this->databaseName, $this->command, $this->createOptions());
+
+ if (isset($this->options['typeMap'])) {
+ $cursor->setTypeMap($this->options['typeMap']);
+ }
+
+ return $cursor;
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executecommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['readPreference'])) {
+ $options['readPreference'] = $this->options['readPreference'];
+ }
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\DeleteResult;
+use MongoDB\Driver\BulkWrite as Bulk;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for the delete command.
+ *
+ * This class is used internally by the DeleteMany and DeleteOne operation
+ * classes.
+ *
+ * @internal
+ * @see http://docs.mongodb.org/manual/reference/command/delete/
+ */
+class Delete implements Executable, Explainable
+{
+ private static $wireVersionForCollation = 5;
+
+ private $databaseName;
+ private $collectionName;
+ private $filter;
+ private $limit;
+ private $options;
+
+ /**
+ * Constructs a delete command.
+ *
+ * Supported options:
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.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.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array|object $filter Query by which to delete documents
+ * @param integer $limit The number of matching documents to
+ * delete. Must be 0 or 1, for all or a
+ * single document, respectively.
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, $filter, $limit, array $options = [])
+ {
+ if ( ! is_array($filter) && ! is_object($filter)) {
+ throw InvalidArgumentException::invalidType('$filter', $filter, 'array or object');
+ }
+
+ if ($limit !== 0 && $limit !== 1) {
+ throw new InvalidArgumentException('$limit must be 0 or 1');
+ }
+
+ if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+ throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->filter = $filter;
+ $this->limit = $limit;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return DeleteResult
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ $bulk = new Bulk();
+ $bulk->delete($this->filter, $this->createDeleteOptions());
+
+ $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions());
+
+ return new DeleteResult($writeResult);
+ }
+
+ public function getCommandDocument(Server $server)
+ {
+ $cmd = ['delete' => $this->collectionName, 'deletes' => [['q' => $this->filter] + $this->createDeleteOptions()]];
+
+ if (isset($this->options['writeConcern'])) {
+ $cmd['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $cmd;
+ }
+
+ /**
+ * Create options for the delete command.
+ *
+ * Note that these options are different from the bulk write options, which
+ * are created in createExecuteOptions().
+ *
+ * @return array
+ */
+ private function createDeleteOptions()
+ {
+ $deleteOptions = ['limit' => $this->limit];
+
+ if (isset($this->options['collation'])) {
+ $deleteOptions['collation'] = (object) $this->options['collation'];
+ }
+
+ return $deleteOptions;
+ }
+
+ /**
+ * Create options for executing the bulk write.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executebulkwrite.php
+ * @return array
+ */
+ private function createExecuteOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ if (isset($this->options['writeConcern'])) {
+ $options['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\DeleteResult;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for deleting multiple document with the delete command.
+ *
+ * @api
+ * @see \MongoDB\Collection::deleteOne()
+ * @see http://docs.mongodb.org/manual/reference/command/delete/
+ */
+class DeleteMany implements Executable, Explainable
+{
+ private $delete;
+
+ /**
+ * Constructs a delete command.
+ *
+ * Supported options:
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.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.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array|object $filter Query by which to delete documents
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, $filter, array $options = [])
+ {
+ $this->delete = new Delete($databaseName, $collectionName, $filter, 0, $options);
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return DeleteResult
+ * @throws UnsupportedException if collation is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ return $this->delete->execute($server);
+ }
+
+ public function getCommandDocument(Server $server)
+ {
+ return $this->delete->getCommandDocument($server);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\DeleteResult;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for deleting a single document with the delete command.
+ *
+ * @api
+ * @see \MongoDB\Collection::deleteOne()
+ * @see http://docs.mongodb.org/manual/reference/command/delete/
+ */
+class DeleteOne implements Executable, Explainable
+{
+ private $delete;
+
+ /**
+ * Constructs a delete command.
+ *
+ * Supported options:
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.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.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array|object $filter Query by which to delete documents
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, $filter, array $options = [])
+ {
+ $this->delete = new Delete($databaseName, $collectionName, $filter, 1, $options);
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return DeleteResult
+ * @throws UnsupportedException if collation is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ return $this->delete->execute($server);
+ }
+
+ public function getCommandDocument(Server $server)
+ {
+ return $this->delete->getCommandDocument($server);
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for the distinct command.
+ *
+ * @api
+ * @see \MongoDB\Collection::distinct()
+ * @see http://docs.mongodb.org/manual/reference/command/distinct/
+ */
+class Distinct implements Executable, Explainable
+{
+ private static $wireVersionForCollation = 5;
+ private static $wireVersionForReadConcern = 4;
+
+ private $databaseName;
+ private $collectionName;
+ private $fieldName;
+ private $filter;
+ private $options;
+
+ /**
+ * Constructs a distinct command.
+ *
+ * Supported options:
+ *
+ * * collation (document): Collation specification.
+ *
+ * This is not supported for server versions < 3.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.
+ *
+ * * readConcern (MongoDB\Driver\ReadConcern): Read concern.
+ *
+ * This is not supported for server versions < 3.2 and will result in an
+ * exception at execution time if used.
+ *
+ * * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param string $fieldName Field for which to return distinct values
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, $fieldName, $filter = [], array $options = [])
+ {
+ if ( ! is_array($filter) && ! is_object($filter)) {
+ throw InvalidArgumentException::invalidType('$filter', $filter, 'array or object');
+ }
+
+ if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+ throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
+ }
+
+ if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
+ throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
+ }
+
+ if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ }
+
+ if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ }
+
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['readConcern']) && $options['readConcern']->isDefault()) {
+ unset($options['readConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->fieldName = (string) $fieldName;
+ $this->filter = $filter;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return mixed[]
+ * @throws UnexpectedValueException if the command response was malformed
+ * @throws UnsupportedException if collation or read concern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+ throw UnsupportedException::collationNotSupported();
+ }
+
+ if (isset($this->options['readConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ throw UnsupportedException::readConcernNotSupported();
+ }
+
+ $cursor = $server->executeReadCommand($this->databaseName, new Command($this->createCommandDocument()), $this->createOptions());
+ $result = current($cursor->toArray());
+
+ if ( ! isset($result->values) || ! is_array($result->values)) {
+ throw new UnexpectedValueException('distinct command did not return a "values" array');
+ }
+
+ return $result->values;
+ }
+
+ public function getCommandDocument(Server $server)
+ {
+ return $this->createCommandDocument();
+ }
+
+ /**
+ * Create the distinct command document.
+ *
+ * @return array
+ */
+ private function createCommandDocument()
+ {
+ $cmd = [
+ 'distinct' => $this->collectionName,
+ 'key' => $this->fieldName,
+ ];
+
+ if ( ! empty($this->filter)) {
+ $cmd['query'] = (object) $this->filter;
+ }
+
+ if (isset($this->options['collation'])) {
+ $cmd['collation'] = (object) $this->options['collation'];
+ }
+
+ if (isset($this->options['maxTimeMS'])) {
+ $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
+ }
+
+ return $cmd;
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executereadcommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['readConcern'])) {
+ $options['readConcern'] = $this->options['readConcern'];
+ }
+
+ if (isset($this->options['readPreference'])) {
+ $options['readPreference'] = $this->options['readPreference'];
+ }
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for the drop command.
+ *
+ * @api
+ * @see \MongoDB\Collection::drop()
+ * @see \MongoDB\Database::dropCollection()
+ * @see http://docs.mongodb.org/manual/reference/command/drop/
+ */
+class DropCollection implements Executable
+{
+ private static $errorMessageNamespaceNotFound = 'ns not found';
+ private static $wireVersionForWriteConcern = 5;
+
+ private $databaseName;
+ private $collectionName;
+ private $options;
+
+ /**
+ * Constructs a drop command.
+ *
+ * Supported options:
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * typeMap (array): Type map for BSON deserialization. This will be used
+ * for the returned command result document.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * @param string $databaseName Database name
+ * @param string $collectionName Collection name
+ * @param array $options Command options
+ * @throws InvalidArgumentException for parameter/option parsing errors
+ */
+ public function __construct($databaseName, $collectionName, array $options = [])
+ {
+ if (isset($options['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->collectionName = (string) $collectionName;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return array|object Command result document
+ * @throws UnsupportedException if writeConcern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['writeConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForWriteConcern)) {
+ throw UnsupportedException::writeConcernNotSupported();
+ }
+
+ $command = new Command(['drop' => $this->collectionName]);
+
+ try {
+ $cursor = $server->executeWriteCommand($this->databaseName, $command, $this->createOptions());
+ } catch (DriverRuntimeException $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];
+ }
+
+ throw $e;
+ }
+
+ if (isset($this->options['typeMap'])) {
+ $cursor->setTypeMap($this->options['typeMap']);
+ }
+
+ return current($cursor->toArray());
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php
+ * @return array
+ */
+ private function createOptions()
+ {
+ $options = [];
+
+ if (isset($this->options['session'])) {
+ $options['session'] = $this->options['session'];
+ }
+
+ if (isset($this->options['writeConcern'])) {
+ $options['writeConcern'] = $this->options['writeConcern'];
+ }
+
+ return $options;
+ }
+}
--- /dev/null
+<?php
+/*
+ * Copyright 2015-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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for the dropDatabase command.
+ *
+ * @api
+ * @see \MongoDB\Client::dropDatabase()
+ * @see \MongoDB\Database::drop()
+ * @see http://docs.mongodb.org/manual/reference/command/dropDatabase/
+ */
+class DropDatabase implements Executable
+{
+ private static $wireVersionForWriteConcern = 5;
+
+ private $databaseName;
+ private $options;
+
+ /**
+ * Constructs a dropDatabase command.
+ *
+ * Supported options:
+ *
+ * * session (MongoDB\Driver\Session): Client session.
+ *
+ * Sessions are not supported for server versions < 3.6.
+ *
+ * * typeMap (array): Type map for BSON deserialization. This will be used
+ * for the returned command result document.
+ *
+ * * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+ *
+ * This is not supported for server versions < 3.4 and will result in an
+ * exception at execution time if used.
+ *
+ * @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['session']) && ! $options['session'] instanceof Session) {
+ throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+ }
+
+ if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+ throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+ }
+
+ if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ }
+
+ if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+ unset($options['writeConcern']);
+ }
+
+ $this->databaseName = (string) $databaseName;
+ $this->options = $options;
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @see Executable::execute()
+ * @param Server $server
+ * @return array|object Command result document
+ * @throws UnsupportedException if writeConcern is used and unsupported
+ * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+ */
+ public function execute(Server $server)
+ {
+ if (isset($this->options['writeConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForWriteConcern)) {
+ throw UnsupportedException::writeConcernNotSupported();
+ }
+
+ $command = new Command(['dropDatabase' => 1]);
+ $cursor = $server->executeWriteCommand($this->databaseName, $command, $this->createOptions());
+
+ if (isset($this->options['typeMap'])) {
+ $cursor->setTypeMap($this->options['typeMap']);
+ }
+
+ return current($cursor->toArray());
+ }
+
+ /**
+ * Create options for executing the command.
+ *
+ * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php
+ * @return array
+ */
+ private function createOptions()