Explorar o código

Merge pull request #17 from alcaeus/type-conversion

Ensure methods return correct values
Andreas %!s(int64=10) %!d(string=hai) anos
pai
achega
eb4e0e2480

+ 1 - 1
lib/Alcaeus/MongoDbAdapter/AbstractCursor.php

@@ -110,7 +110,7 @@ abstract class AbstractCursor
     {
     {
         $document = $this->ensureIterator()->current();
         $document = $this->ensureIterator()->current();
         if ($document !== null) {
         if ($document !== null) {
-            $document = TypeConverter::convertObjectToLegacyArray($document);
+            $document = TypeConverter::toLegacy($document);
         }
         }
 
 
         return $document;
         return $document;

+ 13 - 3
lib/Alcaeus/MongoDbAdapter/Helper/WriteConcern.php

@@ -51,9 +51,9 @@ trait WriteConcern
     /**
     /**
      * @param string|int $wstring
      * @param string|int $wstring
      * @param int $wtimeout
      * @param int $wtimeout
-     * @return bool
+     * @return \MongoDB\Driver\WriteConcern
      */
      */
-    protected function setWriteConcernFromParameters($wstring, $wtimeout = 0)
+    protected function createWriteConcernFromParameters($wstring, $wtimeout)
     {
     {
         if (! is_string($wstring) && ! is_int($wstring)) {
         if (! is_string($wstring) && ! is_int($wstring)) {
             trigger_error("w for WriteConcern must be a string or integer", E_WARNING);
             trigger_error("w for WriteConcern must be a string or integer", E_WARNING);
@@ -61,7 +61,17 @@ trait WriteConcern
         }
         }
 
 
         // Ensure wtimeout is not < 0
         // Ensure wtimeout is not < 0
-        $this->writeConcern = new \MongoDB\Driver\WriteConcern($wstring, max($wtimeout, 0));
+        return new \MongoDB\Driver\WriteConcern($wstring, max($wtimeout, 0));
+    }
+
+    /**
+     * @param string|int $wstring
+     * @param int $wtimeout
+     * @return bool
+     */
+    protected function setWriteConcernFromParameters($wstring, $wtimeout = 0)
+    {
+        $this->writeConcern = $this->createWriteConcernFromParameters($wstring, $wtimeout);
 
 
         return true;
         return true;
     }
     }

+ 85 - 30
lib/Alcaeus/MongoDbAdapter/TypeConverter.php

@@ -20,53 +20,78 @@ namespace Alcaeus\MongoDbAdapter;
  */
  */
 class TypeConverter
 class TypeConverter
 {
 {
-    public static function convertLegacyArrayToObject($array)
-    {
-        // TODO: provide actual class once mongodb/mongo-php-library#78 has been merged
-        $result = [];
-
-        foreach ($array as $key => $value) {
-            $result[$key] = (is_array($value)) ? static::convertLegacyArrayToObject($value) : static::convertToBSONType($value);
-        }
-
-        return self::ensureCorrectType($result);
-    }
-
-    public static function convertObjectToLegacyArray($object)
-    {
-        $result = [];
-
-        foreach ($object as $key => $value) {
-            // TODO: use actual class instead of \stdClass once mongodb/mongo-php-library#78 has been merged
-            $result[$key] = ($value instanceof \stdClass || is_array($value)) ? static::convertObjectToLegacyArray($value) : static::convertToLegacyType($value);
-        }
-
-        return $result;
-    }
-
-    public static function convertToLegacyType($value)
+    /**
+     * Converts a legacy type to the new BSON type
+     *
+     * This method handles type conversion from ext-mongo to ext-mongodb:
+     *  - For all types (MongoId, MongoDate, etc.) it returns the correct BSON
+     *    object instance
+     *  - For arrays and objects it iterates over properties and converts each
+     *    item individually
+     *  - For other types it returns the value unconverted
+     *
+     * @param mixed $value
+     * @return mixed
+     */
+    public static function fromLegacy($value)
     {
     {
         switch (true) {
         switch (true) {
-            case $value instanceof \MongoDB\BSON\ObjectID:
-                return new \MongoId($value);
+            case $value instanceof TypeInterface:
+                return $value->toBSONType();
+            case is_array($value):
+            case is_object($value);
+                $result = [];
+
+                foreach ($value as $key => $item) {
+                    $result[$key] = self::fromLegacy($item);
+                }
 
 
+                return self::ensureCorrectType($result);
             default:
             default:
                 return $value;
                 return $value;
         }
         }
     }
     }
 
 
-    public static function convertToBSONType($value)
+    /**
+     * Converts a BSON type to the legacy types
+     *
+     * This method handles type conversion from ext-mongodb to ext-mongo:
+     *  - For all instances of \MongoDB\BSON\Type it returns an object of the
+     *    corresponding legacy type (MongoId, MongoDate, etc.)
+     *  - For arrays and objects it iterates over properties and converts each
+     *    item individually
+     *  - For other types it returns the value unconverted
+     *
+     * @param mixed $value
+     * @return mixed
+     */
+    public static function toLegacy($value)
     {
     {
         switch (true) {
         switch (true) {
-            case $value instanceof TypeInterface:
-                return $value->toBSONType();
+            case $value instanceof \MongoDB\BSON\Type:
+                return self::convertBSONObjectToLegacy($value);
+            case is_array($value):
+            case is_object($value):
+                $result = [];
 
 
+                foreach ($value as $key => $item) {
+                    $result[$key] = self::toLegacy($item);
+                }
+
+                return $result;
             default:
             default:
                 return $value;
                 return $value;
         }
         }
     }
     }
 
 
     /**
     /**
+     * Helper method to find out if an array has numerical indexes
+     *
+     * For performance reason, this method checks the first array index only.
+     * More thorough inspection of the array might be needed.
+     * Note: Returns true for empty arrays to preserve compatibility with empty
+     * lists.
+     *
      * @param array $array
      * @param array $array
      * @return bool
      * @return bool
      */
      */
@@ -76,6 +101,36 @@ class TypeConverter
     }
     }
 
 
     /**
     /**
+     * Converter method to convert a BSON object to its legacy type
+     *
+     * @param \MongoDB\BSON\Type $value
+     * @return mixed
+     */
+    private static function convertBSONObjectToLegacy(\MongoDB\BSON\Type $value)
+    {
+        switch (true) {
+            case $value instanceof \MongoDB\BSON\ObjectID:
+                return new \MongoId($value);
+            case $value instanceof \MongoDB\BSON\Binary:
+                return new \MongoBinData($value);
+            case $value instanceof \MongoDB\BSON\Javascript:
+                return new \MongoCode($value);
+            case $value instanceof \MongoDB\BSON\MaxKey:
+                return new \MongoMaxKey();
+            case $value instanceof \MongoDB\BSON\MinKey:
+                return new \MongoMinKey();
+            case $value instanceof \MongoDB\BSON\Regex:
+                return new \MongoRegex($value);
+            case $value instanceof \MongoDB\BSON\Timestamp:
+                return new \MongoTimestamp($value);
+            case $value instanceof \MongoDB\BSON\UTCDatetime:
+                return new \MongoDate($value);
+            default:
+                return $value;
+        }
+    }
+
+    /**
      * Converts all arrays with non-numeric keys to stdClass
      * Converts all arrays with non-numeric keys to stdClass
      *
      *
      * @param array $array
      * @param array $array

+ 12 - 0
lib/Mongo/Mongo.php

@@ -198,6 +198,18 @@ class Mongo extends MongoClient
     }
     }
 
 
     /**
     /**
+     * Choose a new secondary for slaveOkay reads
+     *
+     * @link www.php.net/manual/en/mongo.switchslave.php
+     * @return string The address of the secondary this connection is using for reads. This may be the same as the previous address as addresses are randomly chosen. It may return only one address if only one secondary (or only the primary) is available.
+     * @throws MongoException (error code 15) if it is called on a non-replica-set connection. It will also throw MongoExceptions if it cannot find anyone (primary or secondary) to read from (error code 16).
+     */
+    public function switchSlave()
+    {
+        $this->notImplemented();
+    }
+
+    /**
      * Creates a database error on the database.
      * Creates a database error on the database.
      *
      *
      * @link http://www.php.net/manual/en/mongo.forceerror.php
      * @link http://www.php.net/manual/en/mongo.forceerror.php

+ 0 - 12
lib/Mongo/MongoClient.php

@@ -291,18 +291,6 @@ class MongoClient
     }
     }
 
 
     /**
     /**
-     * Choose a new secondary for slaveOkay reads
-     *
-     * @link www.php.net/manual/en/mongo.switchslave.php
-     * @return string The address of the secondary this connection is using for reads. This may be the same as the previous address as addresses are randomly chosen. It may return only one address if only one secondary (or only the primary) is available.
-     * @throws MongoException (error code 15) if it is called on a non-replica-set connection. It will also throw MongoExceptions if it cannot find anyone (primary or secondary) to read from (error code 16).
-     */
-    public function switchSlave()
-    {
-        $this->notImplemented();
-    }
-
-    /**
      * String representation of this connection
      * String representation of this connection
      *
      *
      * @link http://www.php.net/manual/en/mongoclient.tostring.php
      * @link http://www.php.net/manual/en/mongoclient.tostring.php

+ 17 - 1
lib/Mongo/MongoCode.php

@@ -13,7 +13,7 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
  */
 
 
-class MongoCode
+class MongoCode implements \Alcaeus\MongoDbAdapter\TypeInterface
 {
 {
     /**
     /**
      * @var string
      * @var string
@@ -32,6 +32,11 @@ class MongoCode
      */
      */
     public function __construct($code, array $scope = [])
     public function __construct($code, array $scope = [])
     {
     {
+        if ($code instanceof \MongoDB\BSON\Javascript) {
+            // @todo Use properties from object once they are accessible
+            $code = '';
+        }
+
         $this->code = $code;
         $this->code = $code;
         $this->scope = $scope;
         $this->scope = $scope;
     }
     }
@@ -44,4 +49,15 @@ class MongoCode
     {
     {
         return $this->code;
         return $this->code;
     }
     }
+
+    /**
+     * Converts this MongoCode to the new BSON JavaScript type
+     *
+     * @return \MongoDB\BSON\Javascript
+     * @internal This method is not part of the ext-mongo API
+     */
+    public function toBSONType()
+    {
+        return new \MongoDB\BSON\Javascript($this->code, $this->scope);
+    }
 }
 }

+ 165 - 54
lib/Mongo/MongoCollection.php

@@ -170,14 +170,13 @@ class MongoCollection
         ];
         ];
 
 
         // Convert cursor option
         // Convert cursor option
-        if (! isset($options['cursor']) || $options['cursor'] === true || $options['cursor'] === []) {
-            // Cursor option needs to be an object convert bools and empty arrays since those won't be handled by TypeConverter
-            $options['cursor'] = new \stdClass;
+        if (! isset($options['cursor'])) {
+            $options['cursor'] = true;
         }
         }
 
 
         $command += $options;
         $command += $options;
 
 
-        $cursor = new MongoCommandCursor($this->db->getConnection(), (string)$this, $command);
+        $cursor = new MongoCommandCursor($this->db->getConnection(), (string) $this, $command);
         $cursor->setReadPreference($this->getReadPreference());
         $cursor->setReadPreference($this->getReadPreference());
 
 
         return $cursor;
         return $cursor;
@@ -224,7 +223,7 @@ class MongoCollection
      */
      */
     public function drop()
     public function drop()
     {
     {
-        return $this->collection->drop();
+        return TypeConverter::toLegacy($this->collection->drop());
     }
     }
 
 
     /**
     /**
@@ -257,7 +256,21 @@ class MongoCollection
      */
      */
     public function insert($a, array $options = [])
     public function insert($a, array $options = [])
     {
     {
-        return $this->collection->insertOne(TypeConverter::convertLegacyArrayToObject($a), $options);
+        $result = $this->collection->insertOne(
+            TypeConverter::fromLegacy($a),
+            $this->convertWriteConcernOptions($options)
+        );
+
+        if (! $result->isAcknowledged()) {
+            return true;
+        }
+
+        return [
+            'ok' => 1.0,
+            'n' => 0,
+            'err' => null,
+            'errmsg' => null,
+        ];
     }
     }
 
 
     /**
     /**
@@ -267,11 +280,27 @@ class MongoCollection
      * @param array $a An array of arrays.
      * @param array $a An array of arrays.
      * @param array $options Options for the inserts.
      * @param array $options Options for the inserts.
      * @throws MongoCursorException
      * @throws MongoCursorException
-     * @return mixed f "safe" is set, returns an associative array with the status of the inserts ("ok") and any error that may have occured ("err"). Otherwise, returns TRUE if the batch insert was successfully sent, FALSE otherwise.
+     * @return mixed If "safe" is set, returns an associative array with the status of the inserts ("ok") and any error that may have occured ("err"). Otherwise, returns TRUE if the batch insert was successfully sent, FALSE otherwise.
      */
      */
     public function batchInsert(array $a, array $options = [])
     public function batchInsert(array $a, array $options = [])
     {
     {
-        return $this->collection->insertMany($a, $options);
+        $result = $this->collection->insertMany(
+            TypeConverter::fromLegacy($a),
+            $this->convertWriteConcernOptions($options)
+        );
+
+        if (! $result->isAcknowledged()) {
+            return true;
+        }
+
+        return [
+            'connectionId' => 0,
+            'n' => 0,
+            'syncMillis' => 0,
+            'writtenTo' => null,
+            'err' => null,
+            'errmsg' => null,
+        ];
     }
     }
 
 
     /**
     /**
@@ -286,10 +315,29 @@ class MongoCollection
      */
      */
     public function update(array $criteria , array $newobj, array $options = [])
     public function update(array $criteria , array $newobj, array $options = [])
     {
     {
-        $multiple = ($options['multiple']) ? $options['multiple'] : false;
+        $multiple = isset($options['multiple']) ? $options['multiple'] : false;
         $method = $multiple ? 'updateMany' : 'updateOne';
         $method = $multiple ? 'updateMany' : 'updateOne';
+        unset($options['multiple']);
 
 
-        return $this->collection->$method($criteria, $newobj, $options);
+        /** @var \MongoDB\UpdateResult $result */
+        $result = $this->collection->$method(
+            TypeConverter::fromLegacy($criteria),
+            TypeConverter::fromLegacy($newobj),
+            $this->convertWriteConcernOptions($options)
+        );
+
+        if (! $result->isAcknowledged()) {
+            return true;
+        }
+
+        return [
+            'ok' => 1.0,
+            'nModified' => $result->getModifiedCount(),
+            'n' => $result->getMatchedCount(),
+            'err' => null,
+            'errmsg' => null,
+            'updatedExisting' => $result->getUpsertedCount() == 0,
+        ];
     }
     }
 
 
     /**
     /**
@@ -305,10 +353,25 @@ class MongoCollection
      */
      */
     public function remove(array $criteria = [], array $options = [])
     public function remove(array $criteria = [], array $options = [])
     {
     {
-        $multiple = isset($options['justOne']) ? !$options['justOne'] : false;
+        $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
         $method = $multiple ? 'deleteMany' : 'deleteOne';
         $method = $multiple ? 'deleteMany' : 'deleteOne';
 
 
-        return $this->collection->$method($criteria, $options);
+        /** @var \MongoDB\DeleteResult $result */
+        $result = $this->collection->$method(
+            TypeConverter::fromLegacy($criteria),
+            $this->convertWriteConcernOptions($options)
+        );
+
+        if (! $result->isAcknowledged()) {
+            return true;
+        }
+
+        return [
+            'ok' => 1.0,
+            'n' => $result->getDeletedCount(),
+            'err' => null,
+            'errmsg' => null
+        ];
     }
     }
 
 
     /**
     /**
@@ -321,7 +384,7 @@ class MongoCollection
      */
      */
     public function find(array $query = [], array $fields = [])
     public function find(array $query = [], array $fields = [])
     {
     {
-        $cursor = new MongoCursor($this->db->getConnection(), (string)$this, $query, $fields);
+        $cursor = new MongoCursor($this->db->getConnection(), (string) $this, $query, $fields);
         $cursor->setReadPreference($this->getReadPreference());
         $cursor->setReadPreference($this->getReadPreference());
 
 
         return $cursor;
         return $cursor;
@@ -337,11 +400,12 @@ class MongoCollection
      */
      */
     public function distinct($key, array $query = [])
     public function distinct($key, array $query = [])
     {
     {
-        return array_map([TypeConverter::class, 'convertToLegacyType'], $this->collection->distinct($key, $query));
+        return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, $query));
     }
     }
 
 
     /**
     /**
      * Update a document and return it
      * Update a document and return it
+     *
      * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
      * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
      * @param array $query The query criteria to search for.
      * @param array $query The query criteria to search for.
      * @param array $update The update criteria.
      * @param array $update The update criteria.
@@ -351,26 +415,26 @@ class MongoCollection
      */
      */
     public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
     public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
     {
     {
-        $query = TypeConverter::convertLegacyArrayToObject($query);
+        $query = TypeConverter::fromLegacy($query);
 
 
         if (isset($options['remove'])) {
         if (isset($options['remove'])) {
             unset($options['remove']);
             unset($options['remove']);
             $document = $this->collection->findOneAndDelete($query, $options);
             $document = $this->collection->findOneAndDelete($query, $options);
         } else {
         } else {
-            $update = is_array($update) ? TypeConverter::convertLegacyArrayToObject($update) : [];
+            $update = is_array($update) ? TypeConverter::fromLegacy($update) : [];
 
 
             if (isset($options['new'])) {
             if (isset($options['new'])) {
                 $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
                 $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
                 unset($options['new']);
                 unset($options['new']);
             }
             }
 
 
-            $options['projection'] = is_array($fields) ? TypeConverter::convertLegacyArrayToObject($fields) : [];
+            $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : [];
 
 
             $document = $this->collection->findOneAndUpdate($query, $update, $options);
             $document = $this->collection->findOneAndUpdate($query, $update, $options);
         }
         }
 
 
         if ($document) {
         if ($document) {
-            $document = TypeConverter::convertObjectToLegacyArray($document);
+            $document = TypeConverter::toLegacy($document);
         }
         }
 
 
         return $document;
         return $document;
@@ -378,16 +442,20 @@ class MongoCollection
 
 
     /**
     /**
      * Querys this collection, returning a single element
      * Querys this collection, returning a single element
+     *
      * @link http://www.php.net/manual/en/mongocollection.findone.php
      * @link http://www.php.net/manual/en/mongocollection.findone.php
      * @param array $query The fields for which to search.
      * @param array $query The fields for which to search.
      * @param array $fields Fields of the results to return.
      * @param array $fields Fields of the results to return.
+     * @param array $options
      * @return array|null
      * @return array|null
      */
      */
-    public function findOne(array $query = [], array $fields = [])
+    public function findOne(array $query = [], array $fields = [], array $options = [])
     {
     {
-        $document = $this->collection->findOne(TypeConverter::convertLegacyArrayToObject($query), ['projection' => $fields]);
+        $options = ['projection' => $fields] + $options;
+
+        $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
         if ($document !== null) {
         if ($document !== null) {
-            $document = TypeConverter::convertObjectToLegacyArray($document);
+            $document = TypeConverter::toLegacy($document);
         }
         }
 
 
         return $document;
         return $document;
@@ -395,6 +463,7 @@ class MongoCollection
 
 
     /**
     /**
      * Creates an index on the given field(s), or does nothing if the index already exists
      * Creates an index on the given field(s), or does nothing if the index already exists
+     *
      * @link http://www.php.net/manual/en/mongocollection.createindex.php
      * @link http://www.php.net/manual/en/mongocollection.createindex.php
      * @param array $keys Field or fields to use as index.
      * @param array $keys Field or fields to use as index.
      * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
      * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
@@ -416,12 +485,13 @@ class MongoCollection
     }
     }
 
 
     /**
     /**
-     * @deprecated Use MongoCollection::createIndex() instead.
      * Creates an index on the given field(s), or does nothing if the index already exists
      * Creates an index on the given field(s), or does nothing if the index already exists
+     *
      * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
      * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
      * @param array $keys Field or fields to use as index.
      * @param array $keys Field or fields to use as index.
      * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
      * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
      * @return boolean always true
      * @return boolean always true
+     * @deprecated Use MongoCollection::createIndex() instead.
      */
      */
     public function ensureIndex(array $keys, array $options = [])
     public function ensureIndex(array $keys, array $options = [])
     {
     {
@@ -432,6 +502,7 @@ class MongoCollection
 
 
     /**
     /**
      * Deletes an index from this collection
      * Deletes an index from this collection
+     *
      * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
      * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
      * @param string|array $keys Field or fields from which to delete the index.
      * @param string|array $keys Field or fields from which to delete the index.
      * @return array Returns the database response.
      * @return array Returns the database response.
@@ -441,26 +512,28 @@ class MongoCollection
         if (is_string($keys)) {
         if (is_string($keys)) {
             $indexName = $keys;
             $indexName = $keys;
         } elseif (is_array($keys)) {
         } elseif (is_array($keys)) {
-            $indexName = self::toIndexString($keys);
+            $indexName = \MongoDB\generate_index_name($keys);
         } else {
         } else {
             throw new \InvalidArgumentException();
             throw new \InvalidArgumentException();
         }
         }
 
 
-        return TypeConverter::convertObjectToLegacyArray($this->collection->dropIndex($indexName));
+        return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
     }
     }
 
 
     /**
     /**
      * Delete all indexes for this collection
      * Delete all indexes for this collection
+     *
      * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
      * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
      * @return array Returns the database response.
      * @return array Returns the database response.
      */
      */
     public function deleteIndexes()
     public function deleteIndexes()
     {
     {
-        return TypeConverter::convertObjectToLegacyArray($this->collection->dropIndexes());
+        return TypeConverter::toLegacy($this->collection->dropIndexes());
     }
     }
 
 
     /**
     /**
      * Returns an array of index names for this collection
      * Returns an array of index names for this collection
+     *
      * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
      * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
      * @return array Returns a list of index names.
      * @return array Returns a list of index names.
      */
      */
@@ -480,13 +553,15 @@ class MongoCollection
 
 
     /**
     /**
      * Counts the number of documents in this collection
      * Counts the number of documents in this collection
+     *
      * @link http://www.php.net/manual/en/mongocollection.count.php
      * @link http://www.php.net/manual/en/mongocollection.count.php
      * @param array|stdClass $query
      * @param array|stdClass $query
+     * @param array $options
      * @return int Returns the number of documents matching the query.
      * @return int Returns the number of documents matching the query.
      */
      */
-    public function count($query = [])
+    public function count($query = [], array $options = [])
     {
     {
-        return $this->collection->count($query);
+        return $this->collection->count(TypeConverter::fromLegacy($query), $options);
     }
     }
 
 
     /**
     /**
@@ -504,30 +579,48 @@ class MongoCollection
     public function save($a, array $options = [])
     public function save($a, array $options = [])
     {
     {
         if (is_object($a)) {
         if (is_object($a)) {
-            $a = (array)$a;
+            $a = (array) $a;
         }
         }
+
         if ( ! array_key_exists('_id', $a)) {
         if ( ! array_key_exists('_id', $a)) {
             $id = new \MongoId();
             $id = new \MongoId();
         } else {
         } else {
             $id = $a['_id'];
             $id = $a['_id'];
             unset($a['_id']);
             unset($a['_id']);
         }
         }
-        $filter = ['_id' => $id];
-        $filter = TypeConverter::convertLegacyArrayToObject($filter);
-        $a = TypeConverter::convertLegacyArrayToObject($a);
-        return $this->collection->updateOne($filter, ['$set' => $a], ['upsert' => true]);
+        $options['upsert'] = true;
+
+        return $this->update(['_id' => $id], ['$set' => $a], $options);
     }
     }
 
 
     /**
     /**
      * Creates a database reference
      * Creates a database reference
      *
      *
      * @link http://www.php.net/manual/en/mongocollection.createdbref.php
      * @link http://www.php.net/manual/en/mongocollection.createdbref.php
-     * @param array $a Object to which to create a reference.
+     * @param array|object $document_or_id Object to which to create a reference.
      * @return array Returns a database reference array.
      * @return array Returns a database reference array.
      */
      */
-    public function createDBRef(array $a)
+    public function createDBRef($document_or_id)
     {
     {
-        return \MongoDBRef::create($this->name, $a['_id']);
+        if ($document_or_id instanceof \MongoId) {
+            $id = $document_or_id;
+        } elseif (is_object($document_or_id)) {
+            if (! isset($document_or_id->_id)) {
+                return null;
+            }
+
+            $id = $document_or_id->_id;
+        } elseif (is_array($document_or_id)) {
+            if (! isset($document_or_id['_id'])) {
+                return null;
+            }
+
+            $id = $document_or_id['_id'];
+        } else {
+            $id = $document_or_id;
+        }
+
+        return MongoDBRef::create($this->name, $id);
     }
     }
 
 
     /**
     /**
@@ -539,21 +632,7 @@ class MongoCollection
      */
      */
     public function getDBRef(array $ref)
     public function getDBRef(array $ref)
     {
     {
-        return \MongoDBRef::get($this->db, $ref);
-    }
-
-    /**
-     * @param mixed $keys
-     * @static
-     * @return string
-     */
-    protected static function toIndexString($keys)
-    {
-        $result = '';
-        foreach ($keys as $name => $direction) {
-            $result .= sprintf('%s_%d', $name, $direction);
-        }
-        return $result;
+        return $this->db->getDBRef($ref);
     }
     }
 
 
     /**
     /**
@@ -562,7 +641,7 @@ class MongoCollection
      * @link http://www.php.net/manual/en/mongocollection.group.php
      * @link http://www.php.net/manual/en/mongocollection.group.php
      * @param mixed $keys Fields to group by. If an array or non-code object is passed, it will be the key used to group results.
      * @param mixed $keys Fields to group by. If an array or non-code object is passed, it will be the key used to group results.
      * @param array $initial Initial value of the aggregation counter object.
      * @param array $initial Initial value of the aggregation counter object.
-     * @param MongoCode $reduce A function that aggregates (reduces) the objects iterated.
+     * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
      * @param array $condition An condition that must be true for a row to be considered.
      * @param array $condition An condition that must be true for a row to be considered.
      * @return array
      * @return array
      */
      */
@@ -571,9 +650,7 @@ class MongoCollection
         if (is_string($reduce)) {
         if (is_string($reduce)) {
             $reduce = new MongoCode($reduce);
             $reduce = new MongoCode($reduce);
         }
         }
-        if ( ! $reduce instanceof MongoCode) {
-            throw new \InvalidArgumentExcption('reduce parameter should be a string or MongoCode instance.');
-        }
+
         $command = [
         $command = [
             'group' => [
             'group' => [
                 'ns' => $this->name,
                 'ns' => $this->name,
@@ -634,5 +711,39 @@ class MongoCollection
             $this->collection = $this->collection->withOptions($options);
             $this->collection = $this->collection->withOptions($options);
         }
         }
     }
     }
+
+    /**
+     * Converts legacy write concern options to a WriteConcern object
+     *
+     * @param array $options
+     * @return array
+     */
+    private function convertWriteConcernOptions(array $options)
+    {
+        if (isset($options['safe'])) {
+            $options['w'] = ($options['safe']) ? 1 : 0;
+        }
+
+        if (isset($options['wtimeout']) && !isset($options['wTimeoutMS'])) {
+            $options['wTimeoutMS'] = $options['wtimeout'];
+        }
+
+        if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
+            $collectionWriteConcern = $this->getWriteConcern();
+            $writeConcern = $this->createWriteConcernFromParameters(
+                isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
+                isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
+            );
+
+            $options['writeConcern'] = $writeConcern;
+        }
+
+        unset($options['safe']);
+        unset($options['w']);
+        unset($options['wTimeout']);
+        unset($options['wTimeoutMS']);
+
+        return $options;
+    }
 }
 }
 
 

+ 8 - 1
lib/Mongo/MongoCommandCursor.php

@@ -53,7 +53,14 @@ class MongoCommandCursor extends AbstractCursor implements MongoCursorInterface
     protected function ensureCursor()
     protected function ensureCursor()
     {
     {
         if ($this->cursor === null) {
         if ($this->cursor === null) {
-            $this->cursor = $this->db->command(TypeConverter::convertLegacyArrayToObject($this->command), $this->getOptions());
+            $convertedCommand = TypeConverter::fromLegacy($this->command);
+            if (isset($convertedCommand->cursor)) {
+                if ($convertedCommand->cursor === true || $convertedCommand->cursor === []) {
+                    $convertedCommand->cursor = new \stdClass();
+                }
+            }
+
+            $this->cursor = $this->db->command($convertedCommand, $this->getOptions());
         }
         }
 
 
         return $this->cursor;
         return $this->cursor;

+ 33 - 5
lib/Mongo/MongoDB.php

@@ -14,6 +14,7 @@
  */
  */
 
 
 use Alcaeus\MongoDbAdapter\Helper;
 use Alcaeus\MongoDbAdapter\Helper;
+use Alcaeus\MongoDbAdapter\TypeConverter;
 use MongoDB\Model\CollectionInfo;
 use MongoDB\Model\CollectionInfo;
 
 
 /**
 /**
@@ -116,9 +117,36 @@ class MongoDB
     }
     }
 
 
     /**
     /**
+     * Returns information about collections in this database
+     *
+     * @link http://www.php.net/manual/en/mongodb.getcollectioninfo.php
+     * @param array $options An array of options for listing the collections.
+     * @return array
+     */
+    public function getCollectionInfo(array $options = [])
+    {
+        // The includeSystemCollections option is no longer supported
+        if (isset($options['includeSystemCollections'])) {
+            unset($options['includeSystemCollections']);
+        }
+
+        $collections = $this->db->listCollections($options);
+
+        $getCollectionInfo = function (CollectionInfo $collectionInfo) {
+            return [
+                'name' => $collectionInfo->getName(),
+                'options' => $collectionInfo->getOptions(),
+            ];
+        };
+
+        return array_map($getCollectionInfo, iterator_to_array($collections));
+    }
+
+    /**
      * Get all collections from this database
      * Get all collections from this database
      *
      *
      * @link http://www.php.net/manual/en/mongodb.getcollectionnames.php
      * @link http://www.php.net/manual/en/mongodb.getcollectionnames.php
+     * @param array $options An array of options for listing the collections.
      * @return array Returns the names of the all the collections in the database as an array
      * @return array Returns the names of the all the collections in the database as an array
      */
      */
     public function getCollectionNames(array $options = [])
     public function getCollectionNames(array $options = [])
@@ -134,7 +162,7 @@ class MongoDB
             return $collectionInfo->getName();
             return $collectionInfo->getName();
         };
         };
 
 
-        return array_map($getCollectionName, (array)$collections);
+        return array_map($getCollectionName, iterator_to_array($collections));
     }
     }
 
 
     /**
     /**
@@ -193,7 +221,7 @@ class MongoDB
      */
      */
     public function drop()
     public function drop()
     {
     {
-        return $this->db->drop();
+        return TypeConverter::toLegacy($this->db->drop());
     }
     }
 
 
     /**
     /**
@@ -206,7 +234,7 @@ class MongoDB
      */
      */
     public function repair($preserve_cloned_files = FALSE, $backup_original_files = FALSE)
     public function repair($preserve_cloned_files = FALSE, $backup_original_files = FALSE)
     {
     {
-        return [];
+        $this->notImplemented();
     }
     }
 
 
     /**
     /**
@@ -251,7 +279,7 @@ class MongoDB
             $coll = $coll->getName();
             $coll = $coll->getName();
         }
         }
 
 
-        return $this->db->dropCollection((string) $coll);
+        return TypeConverter::toLegacy($this->db->dropCollection((string) $coll));
     }
     }
 
 
     /**
     /**
@@ -259,7 +287,7 @@ class MongoDB
      *
      *
      * @link http://www.php.net/manual/en/mongodb.listcollections.php
      * @link http://www.php.net/manual/en/mongodb.listcollections.php
      * @param array $options
      * @param array $options
-     * @return array Returns a list of MongoCollections.
+     * @return MongoCollection[] Returns a list of MongoCollections.
      */
      */
     public function listCollections(array $options = [])
     public function listCollections(array $options = [])
     {
     {

+ 30 - 0
tests/Alcaeus/MongoDbAdapter/MongoCodeTest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+/**
+ * @author alcaeus <alcaeus@alcaeus.org>
+ */
+class MongoCodeTest extends TestCase
+{
+    public function testCreate()
+    {
+        $code = new \MongoCode('code', ['scope' => 'bleh']);
+        $this->assertAttributeSame('code', 'code', $code);
+        $this->assertAttributeSame(['scope' => 'bleh'], 'scope', $code);
+
+        $this->assertSame('code', (string) $code);
+
+        $bsonCode = $code->toBSONType();
+        $this->assertInstanceOf('MongoDB\BSON\Javascript', $bsonCode);
+    }
+
+    public function testCreateWithBsonObject()
+    {
+        $bsonCode = new \MongoDB\BSON\Javascript('code', ['scope' => 'bleh']);
+        $code = new \MongoCode($bsonCode);
+
+        $this->assertAttributeSame('', 'code', $code);
+        $this->assertAttributeSame([], 'scope', $code);
+    }
+}

+ 133 - 1
tests/Alcaeus/MongoDbAdapter/MongoCollectionTest.php

@@ -19,7 +19,13 @@ class MongoCollectionTest extends TestCase
         $id = '54203e08d51d4a1f868b456e';
         $id = '54203e08d51d4a1f868b456e';
         $collection = $this->getCollection();
         $collection = $this->getCollection();
 
 
-        $collection->insert(['_id' => new \MongoId($id), 'foo' => 'bar']);
+        $expected = [
+            'ok' => 1.0,
+            'n' => 0,
+            'err' => null,
+            'errmsg' => null,
+        ];
+        $this->assertSame($expected, $collection->insert(['_id' => new \MongoId($id), 'foo' => 'bar']));
 
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
         $this->assertSame(1, $newCollection->count());
@@ -32,6 +38,121 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('bar', 'foo', $object);
         $this->assertAttributeSame('bar', 'foo', $object);
     }
     }
 
 
+    public function testUnacknowledgedWrite()
+    {
+        $this->assertTrue($this->getCollection()->insert(['foo' => 'bar'], ['w' => 0]));
+    }
+
+    public function testInsertMany()
+    {
+        $expected = [
+            'connectionId' => 0,
+            'n' => 0,
+            'syncMillis' => 0,
+            'writtenTo' => null,
+            'err' => null,
+            'errmsg' => null
+        ];
+
+        $documents = [
+            ['foo' => 'bar'],
+            ['bar' => 'foo']
+        ];
+        $this->assertSame($expected, $this->getCollection()->batchInsert($documents));
+    }
+
+    public function testUpdateOne()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $expected = [
+            'ok' => 1.0,
+            'nModified' => 1,
+            'n' => 1,
+            'err' => null,
+            'errmsg' => null,
+            'updatedExisting' => true,
+        ];
+
+        $result = $this->getCollection()->update(['foo' => 'bar'], ['$set' => ['foo' => 'foo']]);
+        $this->assertSame($expected, $result);
+
+        $this->assertSame(1, $this->getCheckDatabase()->selectCollection('test')->count(['foo' => 'foo']));
+    }
+
+    public function testUpdateMany()
+    {
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'foo']);
+        $expected = [
+            'ok' => 1.0,
+            'nModified' => 2,
+            'n' => 3,
+            'err' => null,
+            'errmsg' => null,
+            'updatedExisting' => true,
+        ];
+
+        $result = $this->getCollection()->update(['change' => true], ['$set' => ['foo' => 'foo']], ['multiple' => true]);
+        $this->assertSame($expected, $result);
+
+        $this->assertSame(3, $this->getCheckDatabase()->selectCollection('test')->count(['foo' => 'foo']));
+    }
+
+    public function testUnacknowledgedUpdate()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $this->getCollection()->insert(['foo' => 'bar']);
+
+        $this->assertTrue($this->getCollection()->update(['foo' => 'bar'], ['$set' => ['foo' => 'foo']], ['w' => 0]));
+    }
+
+    public function testRemoveMultiple()
+    {
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'foo']);
+        $expected = [
+            'ok' => 1.0,
+            'n' => 2,
+            'err' => null,
+            'errmsg' => null,
+        ];
+
+        $result = $this->getCollection()->remove(['foo' => 'bar']);
+        $this->assertSame($expected, $result);
+
+        $this->assertSame(1, $this->getCheckDatabase()->selectCollection('test')->count());
+    }
+
+    public function testRemoveSingle()
+    {
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'foo']);
+        $expected = [
+            'ok' => 1.0,
+            'n' => 1,
+            'err' => null,
+            'errmsg' => null,
+        ];
+
+        $result = $this->getCollection()->remove(['foo' => 'bar'], ['justOne' => true]);
+        $this->assertSame($expected, $result);
+
+        $this->assertSame(2, $this->getCheckDatabase()->selectCollection('test')->count());
+    }
+
+    public function testRemoveUnacknowledged()
+    {
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'bar']);
+        $this->getCollection()->insert(['change' => true, 'foo' => 'foo']);
+
+        $this->assertTrue($this->getCollection()->remove(['foo' => 'bar'], ['w' => 0]));
+    }
+
     public function testFindReturnsCursor()
     public function testFindReturnsCursor()
     {
     {
         $this->prepareData();
         $this->prepareData();
@@ -479,6 +600,17 @@ class MongoCollectionTest extends TestCase
         );
         );
     }
     }
 
 
+    public function testDrop()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $expected = [
+            'ns' => (string) $this->getCollection(),
+            'nIndexesWas' => 1,
+            'ok' => 1.0
+        ];
+        $this->assertSame($expected, $this->getCollection()->drop());
+    }
+
     /**
     /**
      * @return \MongoCollection
      * @return \MongoCollection
      */
      */

+ 1 - 1
tests/Alcaeus/MongoDbAdapter/MongoCommandCursorTest.php

@@ -27,7 +27,7 @@ class MongoCommandCursorTest extends TestCase
                         '$match' => ['foo' => 'bar']
                         '$match' => ['foo' => 'bar']
                     ]
                     ]
                 ],
                 ],
-                'cursor' => new \stdClass()
+                'cursor' => true,
             ],
             ],
             'fields' => null,
             'fields' => null,
             'started_iterating' => false,
             'started_iterating' => false,

+ 51 - 0
tests/Alcaeus/MongoDbAdapter/MongoDBTest.php

@@ -128,4 +128,55 @@ class MongoDBTest extends TestCase
         $this->getCollection()->insert(['foo' => 'bar']);
         $this->getCollection()->insert(['foo' => 'bar']);
         $this->assertEquals(['ok' => 1, 'retval' => 1], $db->execute("return db.test.count();"));
         $this->assertEquals(['ok' => 1, 'retval' => 1], $db->execute("return db.test.count();"));
     }
     }
+
+    public function testGetCollectionNames()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $this->assertContains('test', $this->getDatabase()->getCollectionNames());
+    }
+
+    public function testGetCollectionInfo()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+
+        foreach ($this->getDatabase()->getCollectionInfo() as $collectionInfo) {
+            if ($collectionInfo['name'] === 'test') {
+                $this->assertSame(['name' => 'test', 'options' => []], $collectionInfo);
+                return;
+            }
+        }
+
+        $this->fail('The test collection was not found');
+    }
+
+    public function testListCollections()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        foreach ($this->getDatabase()->listCollections() as $collection) {
+            $this->assertInstanceOf('MongoCollection', $collection);
+
+            if ($collection->getName() === 'test') {
+                return;
+            }
+        }
+
+        $this->fail('The test collection was not found');
+    }
+
+    public function testDrop()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $this->assertSame(['dropped' => 'mongo-php-adapter', 'ok' => 1.0], $this->getDatabase()->drop());
+    }
+
+    public function testDropCollection()
+    {
+        $this->getCollection()->insert(['foo' => 'bar']);
+        $expected = [
+            'ns' => (string) $this->getCollection(),
+            'nIndexesWas' => 1,
+            'ok' => 1.0
+        ];
+        $this->assertSame($expected, $this->getDatabase()->dropCollection('test'));
+    }
 }
 }