Bladeren bron

add mongodb

NoteStar 5 jaren geleden
bovenliggende
commit
88e4c202fb
46 gewijzigde bestanden met toevoegingen van 10248 en 7 verwijderingen
  1. 2 5
      addons/fecmall/furnilife_theme/app/appfront/theme/furnilife/widgets/header.php
  2. 1 1
      vendor/fancyecommerce/fecshop/services/email/views/customer/account/login/subject_en.php
  3. 1 1
      vendor/fancyecommerce/fecshop/services/email/views/customer/account/register/subject_en.php
  4. 159 0
      vendor/yiisoft/yii2-mongodb/CHANGELOG.md
  5. 32 0
      vendor/yiisoft/yii2-mongodb/LICENSE.md
  6. 62 0
      vendor/yiisoft/yii2-mongodb/README.md
  7. 52 0
      vendor/yiisoft/yii2-mongodb/UPGRADE.md
  8. 38 0
      vendor/yiisoft/yii2-mongodb/composer.json
  9. 131 0
      vendor/yiisoft/yii2-mongodb/src/ActiveFixture.php
  10. 222 0
      vendor/yiisoft/yii2-mongodb/src/ActiveQuery.php
  11. 414 0
      vendor/yiisoft/yii2-mongodb/src/ActiveRecord.php
  12. 189 0
      vendor/yiisoft/yii2-mongodb/src/BatchQueryResult.php
  13. 198 0
      vendor/yiisoft/yii2-mongodb/src/Cache.php
  14. 436 0
      vendor/yiisoft/yii2-mongodb/src/Collection.php
  15. 842 0
      vendor/yiisoft/yii2-mongodb/src/Command.php
  16. 435 0
      vendor/yiisoft/yii2-mongodb/src/Connection.php
  17. 158 0
      vendor/yiisoft/yii2-mongodb/src/Database.php
  18. 25 0
      vendor/yiisoft/yii2-mongodb/src/Exception.php
  19. 131 0
      vendor/yiisoft/yii2-mongodb/src/LogBuilder.php
  20. 293 0
      vendor/yiisoft/yii2-mongodb/src/Migration.php
  21. 635 0
      vendor/yiisoft/yii2-mongodb/src/Query.php
  22. 916 0
      vendor/yiisoft/yii2-mongodb/src/QueryBuilder.php
  23. 184 0
      vendor/yiisoft/yii2-mongodb/src/Session.php
  24. 271 0
      vendor/yiisoft/yii2-mongodb/src/console/controllers/MigrateController.php
  25. 115 0
      vendor/yiisoft/yii2-mongodb/src/debug/ExplainAction.php
  26. 121 0
      vendor/yiisoft/yii2-mongodb/src/debug/MongoDbPanel.php
  27. 116 0
      vendor/yiisoft/yii2-mongodb/src/debug/views/detail.php
  28. 183 0
      vendor/yiisoft/yii2-mongodb/src/file/ActiveQuery.php
  29. 335 0
      vendor/yiisoft/yii2-mongodb/src/file/ActiveRecord.php
  30. 327 0
      vendor/yiisoft/yii2-mongodb/src/file/Collection.php
  31. 149 0
      vendor/yiisoft/yii2-mongodb/src/file/Cursor.php
  32. 319 0
      vendor/yiisoft/yii2-mongodb/src/file/Download.php
  33. 39 0
      vendor/yiisoft/yii2-mongodb/src/file/Query.php
  34. 415 0
      vendor/yiisoft/yii2-mongodb/src/file/StreamWrapper.php
  35. 280 0
      vendor/yiisoft/yii2-mongodb/src/file/Upload.php
  36. 283 0
      vendor/yiisoft/yii2-mongodb/src/gii/model/Generator.php
  37. 83 0
      vendor/yiisoft/yii2-mongodb/src/gii/model/default/model.php
  38. 14 0
      vendor/yiisoft/yii2-mongodb/src/gii/model/form.php
  39. 215 0
      vendor/yiisoft/yii2-mongodb/src/i18n/MongoDbMessageSource.php
  40. 79 0
      vendor/yiisoft/yii2-mongodb/src/log/MongoDbTarget.php
  41. 1078 0
      vendor/yiisoft/yii2-mongodb/src/rbac/MongoDbManager.php
  42. 22 0
      vendor/yiisoft/yii2-mongodb/src/rbac/Permission.php
  43. 22 0
      vendor/yiisoft/yii2-mongodb/src/rbac/Role.php
  44. 84 0
      vendor/yiisoft/yii2-mongodb/src/validators/MongoDateValidator.php
  45. 115 0
      vendor/yiisoft/yii2-mongodb/src/validators/MongoIdValidator.php
  46. 27 0
      vendor/yiisoft/yii2-mongodb/src/views/migration.php

+ 2 - 5
addons/fecmall/furnilife_theme/app/appfront/theme/furnilife/widgets/header.php

@@ -12,16 +12,13 @@
 		=            header top         =
 		=============================================-->
 
-<div class="header-top pt-15 pb-15">
+<div class="header-top pb-15">
     <input type="hidden" class="currentBaseUrl" value="<?= $currentBaseUrl ?>" />
     <input type="hidden" class="logoutUrl" value="<?= $logoutUrl ?>" />
     <input type="hidden" class="logoutStr" value="<?= Yii::$service->page->translate->__('Logout'); ?>" />
     <input type="hidden" class="welcome_str" value="<?= Yii::$service->page->translate->__('Welcome!'); ?>" />
-    <div class="toptop">
-        <a href=""><img src="<?= Yii::$service->image->getImgUrl('appfront/custom/top_top.png'); ?>" width="100%"> </a>
-    </div>
     <div class="container">
-        <div class="row top_logo">
+        <div class="row ">
             <div class="col-12 col-lg-3">
                 <div class="header-top-dropdown d-flex pt-15 ">
                     <!--=======  single dropdown  =======-->

+ 1 - 1
vendor/fancyecommerce/fecshop/services/email/views/customer/account/login/subject_en.php

@@ -1 +1 @@
-you hava login your account on fecsop!
+you hava login your account on One Smile!

+ 1 - 1
vendor/fancyecommerce/fecshop/services/email/views/customer/account/register/subject_en.php

@@ -1 +1 @@
-you hava register account on fecsop!
+you hava register account on One Smile!

+ 159 - 0
vendor/yiisoft/yii2-mongodb/CHANGELOG.md

@@ -0,0 +1,159 @@
+Yii Framework 2 mongodb extension Change Log
+============================================
+
+2.1.7 March 30, 2018
+--------------------
+
+- Bug #251: Fixed `yii\mongodb\ActiveQuery::indexBy()` does not apply while using Yii 2.0.14 (klimov-paul)
+- Enh: `yii\mongodb\Session` now relies on error handler to display errors (samdark)
+
+
+2.1.6 February 13, 2018
+-----------------------
+
+- Bug #241: Fixed `yii\mongodb\Command::aggregate()` without 'cursor' option produces error on MongoDB Server 3.6 (Lisio, klimov-paul)
+- Bug #247: Fixed `yii\mongodb\Collection::dropIndex()` unable to drop index specified with sort via index plugin (klimov-paul)
+
+
+2.1.5 November 03, 2017
+-----------------------
+
+- Bug #223: Usage of deprecated `yii\base\Object` changed to `yii\base\BaseObject` allowing compatibility with PHP 7.2 (klimov-paul)
+- Bug #227: Fixed `yii\mongodb\file\Collection::remove()` does not removes all file chunks in case `limit` is specified (klimov-paul)
+- Bug #228: Fixed `yii\mongodb\Command::aggregate()` does not support 'cursor' option (klimov-paul)
+- Enh #224: Provided support for 'migrate/fresh' command to truncate database and apply migrations again (klimov-paul)
+- Enh #225: Added `yii\mongodb\Migration::$compact` supporting `yii\console\controllers\BaseMigrateController::$compact` option (klimov-paul)
+- Chg #158: Data structure for `yii\mongodb\i18n\MongoDbMessageSource` changed avoiding usage message key as BSON key (klimov-paul)
+
+
+2.1.4 June 23, 2017
+-------------------
+
+- Bug #187: Fixed exception is thrown on `yii\mongodb\rbac\MongoDbManager::invalidateCache()` invocation (jafaripur)
+- Bug #201: Fixed selection of master/slave server for read/write operations at `yii\mongodb\Command` (KhristenkoYura)
+- Bug #205: Fixed negative value passed to `yii\mongodb\Query::limit()` or `yii\mongodb\Query::offset()` does not disables query limit or offset correspondingly (klimov-paul)
+- Bug #207: Fixed `yii\mongodb\validators\MongoDateValidator` corrupts date value, while validating existing `MongoDB\BSON\UTCDateTime` instance (klimov-paul)
+- Bug #210: Fixed `yii\mongodb\debug\MongoDbPanel` overrides explain action of `yii\debug\panels\DbPanel` (Liv1020, klimov-paul)
+- Bug #213: Made `MigrateController` compatible with Yii 2.0.12 (cebe)
+
+
+2.1.3 February 15, 2017
+-----------------------
+
+- Bug #168: Fixed `yii\mongodb\Command::update()` uses `upsert` option by default (klimov-paul)
+- Bug #170: Fixed incorrect order of migrations history in case `yii\mongodb\console\controllers\MigrateController::$migrationNamespaces` is in use (klimov-paul)
+- Bug #173: Fixed `yii\mongodb\ActiveQuery` does not respects relational link at methods `count()`, `distinct()`, `sum()`, `average()`, `modify()` (tuyakhov, klimov-paul)
+- Bug #176: Fixed `yii\mongodb\validators\MongoDateValidator` uses seconds instead of milliseconds while creating `MongoDB\BSON\UTCDateTime` instance (reza-id, klimov-paul)
+- Bug #179: Fixed `yii\mongodb\file\Upload` unable to handle custom `_id` value, if it does not provided as `\MongoDB\BSON\ObjectID` instance (klimov-paul)
+- Bug #186: Fixed `yii\mongodb\rbac\MongoDbManager::getRolesByUser()` results now includes default roles (klimov-paul)
+- Enh #171: Added support for `yii\db\QueryInterface::emulateExecution()` to force returning an empty result for a query (klimov-paul)
+- Enh #177: Method `yii\mongodb\ActiveQuery::exists()` optimized avoiding redundant ActiveRecord and relations population (klimov-paul)
+
+
+2.1.2 October 31, 2016
+----------------------
+
+- Bug #150: Fixed `yii\mongodb\Query::exists()` always returning true (klimov-paul)
+- Bug #155: Fixed `yii\mongodb\Query` unable to process `not` condition with `null` compare value (klimov-paul)
+- Enh #152: Added support for namespaced migrations via [[yii\mongodb\console\controllers\MigrateController::migrationNamespaces]] (klimov-paul)
+- Enh #153: Added `yii\mongodb\rbac\MongoDbManager::getChildRoles()` method allowing finding child roles for the given one (githubjeka, klimov-paul)
+- Enh #154: Methods `scalar()` and `column()` added to `yii\mongodb\Query` (klimov-paul)
+
+
+2.1.1 August 29, 2016
+---------------------
+
+- Bug #136: Fixed `yii\mongodb\Collection::findOne()` returns `false` instead of `null` on empty result (klimov-paul)
+- Bug #142: Fixed `yii\mongodb\Migration::createIndexes()` triggers E_NOTICE (klimov-paul)
+- Bug #145: Fixed `yii\mongodb\ActiveFixture` fails to find default data file if `collectionName` is specified in array format (klimov-paul)
+- Bug #146: Fixed `yii\mongodb\ActiveRecord` and `yii\mongodb\file\ActiveRecord` looses `_id` custom value on insertion (lxyfirst, klimov-paul)
+- Enh #147: Added unknown methods `stream_seek` and `stream_tell` to `yii\mongodb\file\StreamWrapper` for `fseek()` and `ftell()` (AstRonin)
+- Enh: Added `yii\mongodb\Migration::listCollections()` method (klimov-paul)
+
+
+2.1.0 June 27, 2016
+-------------------
+
+- Enh #33: Added support for batch (bulk) write operations (klimov-paul)
+- Enh #56: Now 'mongodb' PHP extension used instead of 'mongo' (klimov-paul, hardsetting, Sammaye)
+- Enh #76: Added ability to disable logging and/or profiling for the commands and queries (klimov-paul)
+- Enh #77: Added support for fetching data from MongoDB in batches (klimov-paul)
+- Enh #79: `yii\mongodb\ActiveRecord::toArray()` provides better representation for BSON objects in recursive mode (klimov-paul, rowdyroad)
+
+
+2.0.5 May 9, 2016
+-----------------
+
+- Bug #40: Fixed `yii\mongodb\ActiveFixture` throws exception on empty fixture data (darkunz)
+- Bug #73: Fixed `yii\mongodb\Collection::buildInCondition()` unable to process composite 'IN' condition (klimov-paul)
+- Bug #75: Fixed `yii\mongodb\Collection::distinct()` always returns `false` on empty condition for MongoDB 2.8 (boxoft)
+- Bug #101: Fixed `yii\mongodb\Collection::buildCondition()` does not compose 'IN' condition for the values with broken index sequence (klimov-paul)
+- Bug: Avoid serializing PHP 7 errors (zuozp8, cebe)
+- Enh #23: Added support for complex sort specification at `yii\mongodb\Query` (raoptimus)
+- Enh #24: `yii\mongodb\Query` now contains a `andFilterCompare()` method that allows filtering using operators in the query value (lennartvdd)
+- Enh #27: Added support for saving extra fields in session collection for `yii\mongodb\Session` (klimov-paul)
+- Enh #35: Added support for cursor options setup at `yii\mongodb\Query` (klimov-paul)
+- Enh #36: Added support for compare operators (like '>', '<' and so on) at `yii\mongodb\Query` (klimov-paul)
+- Enh #37: Now `yii\mongodb\Collection::buildInCondition` is not added '$in' for array contains one element (webdevsega)
+- Enh #41: Added `yii\mongodb\Connection::driverOptions` allowing setup of the options for the MongoDB driver (klimov-paul)
+- Enh #57: Added i18n support via `yii\mongodb\i18n\MongoDbMessageSource` (klimov-paul)
+- Enh #69: Fixed log target to display exceptions like DbTarget in Yii core, also avoids problems with Exceptions that contain closures (cebe)
+- Enh #74: Added explain method to `MongoDbPanel` debug panel (webdevsega)
+- Enh #87: Added RBAC support via `yii\mongodb\rbac\MongoDbManager` (klimov-paul)
+- Enh #102: `MongoDbTarget` now uses `batchInsert()` while exporting log messages (klimov-paul)
+
+
+2.0.4 May 10, 2015
+------------------
+
+- Bug #7010: Fixed `yii\mongodb\Query::one()` fails on PHP MongoDB extension version 1.6.x (im-kulikov, klimov-paul)
+- Enh #5802: Added `yii\mongodb\validators\MongoIdValidator` and `yii\mongodb\validators\MongoDateValidator` validators (klimov-paul)
+- Enh #7798: Added support for 'NOT' conditions at `yii\mongodb\Collection` (klimov-paul)
+- Chg #7924: Migrations in history are now ordered by time applied allowing to roll back in reverse order no matter how these were applied (klimov-paul)
+
+
+2.0.3 March 01, 2015
+--------------------
+
+- Bug #7010: Fixed `yii\mongodb\Query::select` now allows excluding fields (Sammaye, klimov-paul)
+
+
+2.0.2 January 11, 2015
+----------------------
+
+- Bug #6376: Fixed lazy load of relations to `yii\mongodb\file\ActiveRecord` (klimov-paul)
+
+
+2.0.1 December 07, 2014
+-----------------------
+
+- Bug #6026: Fixed `yii\mongodb\ActiveRecord` saves `null` as `_id`, if attributes are empty (klimov-paul)
+- Enh #3855: Added debug toolbar panel for MongoDB (klimov-paul)
+- Enh #5592: Added support for 'findAndModify' operation at `yii\mongodb\Query` and `yii\mongodb\ActiveQuery` (klimov-paul)
+
+
+2.0.0 October 12, 2014
+----------------------
+
+- Bug #5303: Fixed `yii\mongodb\Collection` unable to fetch default database name from DSN with parameters (klimov-paul)
+- Bug #5411: Fixed `yii\mongodb\ActiveRecord` unable to fetch 'hasMany' referred by array of `\MongoId` (klimov-paul)
+
+
+2.0.0-rc September 27, 2014
+---------------------------
+
+- Bug #2337: `yii\mongodb\Collection::buildLikeCondition()` fixed to escape regular expression (klimov-paul)
+- Bug #3385: Fixed "The 'connected' property is deprecated" (samdark)
+- Bug #4879: Fixed `yii\mongodb\Collection::buildInCondition()` handles non-sequent key arrays (klimov-paul)
+- Enh #3520: Added `unlinkAll()`-method to active record to remove all records of a model relation (NmDimas, samdark, cebe)
+- Enh #3778: Gii generator for Active Record model added (klimov-paul)
+- Enh #3947: Migration support added (klimov-paul)
+- Enh #4048: Added `init` event to `ActiveQuery` classes (qiangxue)
+- Enh #4086: changedAttributes of afterSave Event now contain old values (dizews)
+- Enh #4335: `yii\mongodb\log\MongoDbTarget` log target added (klimov-paul)
+
+
+2.0.0-beta April 13, 2014
+-------------------------
+
+- Initial release.

+ 32 - 0
vendor/yiisoft/yii2-mongodb/LICENSE.md

@@ -0,0 +1,32 @@
+The Yii framework is free software. It is released under the terms of
+the following BSD License.
+
+Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in
+   the documentation and/or other materials provided with the
+   distribution.
+ * Neither the name of Yii Software LLC nor the names of its
+   contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.

+ 62 - 0
vendor/yiisoft/yii2-mongodb/README.md

@@ -0,0 +1,62 @@
+<p align="center">
+    <a href="https://www.mongodb.com/" target="_blank" rel="external">
+        <img src="https://webassets.mongodb.com/_com_assets/cms/mongodb-logo-rgb-j6w271g1xn.jpg" height="80px">
+    </a>
+    <h1 align="center">MongoDB Extension for Yii 2</h1>
+    <br>
+</p>
+
+This extension provides the [MongoDB](https://www.mongodb.com/) integration for the [Yii framework 2.0](http://www.yiiframework.com).
+
+For license information check the [LICENSE](LICENSE.md)-file.
+
+Documentation is at [docs/guide/README.md](docs/guide/README.md).
+
+[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-mongodb/v/stable.png)](https://packagist.org/packages/yiisoft/yii2-mongodb)
+[![Total Downloads](https://poser.pugx.org/yiisoft/yii2-mongodb/downloads.png)](https://packagist.org/packages/yiisoft/yii2-mongodb)
+[![Build Status](https://travis-ci.org/yiisoft/yii2-mongodb.svg?branch=master)](https://travis-ci.org/yiisoft/yii2-mongodb)
+
+
+Installation
+------------
+
+This extension requires [MongoDB PHP Extension](http://us1.php.net/manual/en/set.mongodb.php) version 1.0.0 or higher.
+
+This extension requires MongoDB server version 3.0 or higher.
+
+The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
+
+Either run
+
+```
+php composer.phar require --prefer-dist yiisoft/yii2-mongodb
+```
+
+or add
+
+```
+"yiisoft/yii2-mongodb": "~2.1.0"
+```
+
+to the require section of your composer.json.
+
+Configuration
+-------------
+
+To use this extension, simply add the following code in your application configuration:
+
+```php
+return [
+    //....
+    'components' => [
+        'mongodb' => [
+            'class' => '\yii\mongodb\Connection',
+            'dsn' => 'mongodb://@localhost:27017/mydatabase',
+            'options' => [
+                "username" => "Username",
+                "password" => "Password"
+            ]
+        ],
+    ],
+];
+```

+ 52 - 0
vendor/yiisoft/yii2-mongodb/UPGRADE.md

@@ -0,0 +1,52 @@
+Upgrading Instructions for Yii Framework v2
+===========================================
+
+!!!IMPORTANT!!!
+
+The following upgrading instructions are cumulative. That is,
+if you want to upgrade from version A to version C and there is
+version B between A and C, you need to following the instructions
+for both A and B.
+
+Upgrade from Yii 2.0.5
+----------------------
+
+* PHP [mongodb](http://php.net/manual/en/set.mongodb.php) extension is now used instead of [mongo](http://php.net/manual/en/book.mongo.php).
+  Make sure you have 'mongodb' extension at your environment. Some features based on old driver may become unavailable.
+  In particular: fields `Connection::mongoClient`, `Database::mongoDb` and `Collection::mongoCollection` are no longer exist.
+  Old driver type classes such as `\MongoId`, `\MongoCode`, `\MongoDate` and so on, are no longer returned or
+  recognized. Make sure you are using their analogs from `\MongoDB\BSON\*` namespace.
+
+* MongoDB server versions < 3.0 are no longer supported. Make sure you are running MongoDB server >= 3.0
+
+* The signature of the following `\yii\mongodb\Collection` methods has been changed: `aggregate()`, `distinct()`,
+  `find()`, `findOne()`, `findAndModify()`. Make sure you invoke those methods correctly. In case you are
+  extending `\yii\mongodb\Collection`, you should check, if overridden methods match parent declaration.
+
+* Command and query composition methods at `\yii\mongodb\Collection`, such as `buildCondition()`, `ensureMongoId()`
+  and so on, have been removed. Use `\yii\mongodb\QueryBuilder` methods instead.
+
+* Method `Database::executeCommand()` has been removed. Use `Command` class for plain MongoDB command execution.
+  You may create command with database scope using `Database::createCommand()` method.
+
+* Method `Collection::fullTextSearch()` has been removed. Use `$text` query condition instead.
+
+* Method `Collection::getName()` has been removed. Use `Collection::name` in order to get collection self name.
+
+* For GridFS `yii\mongodb\file\Download` is returned instead of `\MongoGridFSFile` for the query result set.
+
+* Cursor composed via `yii\mongodb\file\Collection::find()` now returns result in the same format as `yii\mongodb\file\Query::one()`.
+  If you wish to perform file manipulations on returned row you should use `file` key instead of direct method invocations.
+
+Upgrade from Yii 2.0.1
+----------------------
+
+* MongoDB PHP extension min version raised up to 1.5.0. You should upgrade your environment in case you are
+  using older version.
+
+Upgrade from Yii 2.0.0
+----------------------
+
+* MongoDB PHP extension min version raised up to 1.4.0. You should upgrade your environment in case you are
+  using older version.
+

+ 38 - 0
vendor/yiisoft/yii2-mongodb/composer.json

@@ -0,0 +1,38 @@
+{
+    "name": "yiisoft/yii2-mongodb",
+    "description": "MongoDB extension for the Yii framework",
+    "keywords": ["yii2", "mongo", "mongodb", "active-record", "gridfs"],
+    "type": "yii2-extension",
+    "license": "BSD-3-Clause",
+    "support": {
+        "issues": "https://github.com/yiisoft/yii2-mongodb/issues",
+        "forum": "http://www.yiiframework.com/forum/",
+        "wiki": "http://www.yiiframework.com/wiki/",
+        "irc": "irc://irc.freenode.net/yii",
+        "source": "https://github.com/yiisoft/yii2-mongodb"
+    },
+    "authors": [
+        {
+            "name": "Paul Klimov",
+            "email": "klimov.paul@gmail.com"
+        }
+    ],
+    "require": {
+        "yiisoft/yii2": "~2.0.14",
+        "ext-mongodb": ">=1.0.0"
+    },
+    "repositories": [
+        {
+            "type": "composer",
+            "url": "https://asset-packagist.org"
+        }
+    ],
+    "autoload": {
+        "psr-4": { "yii\\mongodb\\": "src" }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "2.1.x-dev"
+        }
+    }
+}

+ 131 - 0
vendor/yiisoft/yii2-mongodb/src/ActiveFixture.php

@@ -0,0 +1,131 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use Yii;
+use yii\base\InvalidConfigException;
+use yii\test\BaseActiveFixture;
+
+/**
+ * ActiveFixture represents a fixture backed up by a [[modelClass|MongoDB ActiveRecord class]] or a [[collectionName|MongoDB collection]].
+ *
+ * Either [[modelClass]] or [[collectionName]] must be set. You should also provide fixture data in the file
+ * specified by [[dataFile]] or overriding [[getData()]] if you want to use code to generate the fixture data.
+ *
+ * When the fixture is being loaded, it will first call [[resetCollection()]] to remove any existing data in the collection.
+ * It will then populate the collection with the data returned by [[getData()]].
+ *
+ * After the fixture is loaded, you can access the loaded data via the [[data]] property. If you set [[modelClass]],
+ * you will also be able to retrieve an instance of [[modelClass]] with the populated data via [[getModel()]].
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class ActiveFixture extends BaseActiveFixture
+{
+    /**
+     * @var Connection|string the DB connection object or the application component ID of the DB connection.
+     */
+    public $db = 'mongodb';
+    /**
+     * @var string|array the collection name that this fixture is about. If this property is not set,
+     * the collection name will be determined via [[modelClass]].
+     * @see Connection::getCollection()
+     */
+    public $collectionName;
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function init()
+    {
+        parent::init();
+        if (!isset($this->modelClass) && !isset($this->collectionName)) {
+            throw new InvalidConfigException('Either "modelClass" or "collectionName" must be set.');
+        }
+    }
+
+    /**
+     * Loads the fixture data.
+     * The default implementation will first reset the MongoDB collection and then populate it with the data
+     * returned by [[getData()]].
+     */
+    public function load()
+    {
+        $this->resetCollection();
+        $this->data = [];
+        $data = $this->getData();
+        if (empty($data)) {
+            return;
+        }
+        $this->getCollection()->batchInsert($data);
+        foreach ($data as $alias => $row) {
+            $this->data[$alias] = $row;
+        }
+    }
+
+    /**
+     * Returns collection used by this fixture.
+     * @return Collection related collection.
+     */
+    protected function getCollection()
+    {
+        return $this->db->getCollection($this->getCollectionName());
+    }
+
+    /**
+     * Returns collection name used by this fixture.
+     * @return array|string related collection name
+     */
+    protected function getCollectionName()
+    {
+        if ($this->collectionName) {
+            return $this->collectionName;
+        }
+
+        /* @var $modelClass ActiveRecord */
+        $modelClass = $this->modelClass;
+        return $modelClass::collectionName();
+    }
+
+    /**
+     * Returns the fixture data.
+     *
+     * This method is called by [[loadData()]] to get the needed fixture data.
+     *
+     * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
+     * The file should return an array of data rows (column name => column value), each corresponding to a row in the collection.
+     *
+     * If the data file does not exist, an empty array will be returned.
+     *
+     * @return array the data rows to be inserted into the collection.
+     */
+    protected function getData()
+    {
+        if ($this->dataFile === null) {
+            $class = new \ReflectionClass($this);
+
+            $collectionName = $this->getCollectionName();
+            $dataFile = dirname($class->getFileName()) . '/data/' . (is_array($collectionName) ? implode('.', $collectionName) : $collectionName) . '.php';
+
+            return is_file($dataFile) ? require($dataFile) : [];
+        }
+
+        return parent::getData();
+    }
+
+    /**
+     * Removes all existing data from the specified collection and resets sequence number if any.
+     * This method is called before populating fixture data into the collection associated with this fixture.
+     */
+    protected function resetCollection()
+    {
+        $this->getCollection()->remove();
+    }
+}

+ 222 - 0
vendor/yiisoft/yii2-mongodb/src/ActiveQuery.php

@@ -0,0 +1,222 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use yii\db\ActiveQueryInterface;
+use yii\db\ActiveQueryTrait;
+use yii\db\ActiveRelationTrait;
+
+/**
+ * ActiveQuery represents a Mongo query associated with an Active Record class.
+ *
+ * An ActiveQuery can be a normal query or be used in a relational context.
+ *
+ * ActiveQuery instances are usually created by [[ActiveRecord::find()]].
+ * Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]].
+ *
+ * Normal Query
+ * ------------
+ *
+ * ActiveQuery instances are usually created by [[ActiveRecord::find()]].
+ *
+ * Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
+ * [[orderBy()]] to customize the query options.
+ *
+ * ActiveQuery also provides the following additional query options:
+ *
+ * - [[with()]]: list of relations that this query should be performed with.
+ * - [[asArray()]]: whether to return each record as an array.
+ *
+ * These options can be configured using methods of the same name. For example:
+ *
+ * ```php
+ * $customers = Customer::find()->with('orders')->asArray()->all();
+ * ```
+ *
+ * Relational query
+ * ----------------
+ *
+ * In relational context ActiveQuery represents a relation between two Active Record classes.
+ *
+ * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
+ * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
+ * a getter method which calls one of the above methods and returns the created ActiveQuery object.
+ *
+ * A relation is specified by [[link]] which represents the association between columns
+ * of different collections; and the multiplicity of the relation is indicated by [[multiple]].
+ *
+ * If a relation involves a junction collection, it may be specified by [[via()]].
+ * This methods may only be called in a relational context. Same is true for [[inverseOf()]], which
+ * marks a relation as inverse of another relation.
+ *
+ * @property Collection $collection Collection instance. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class ActiveQuery extends Query implements ActiveQueryInterface
+{
+    use ActiveQueryTrait;
+    use ActiveRelationTrait;
+
+    /**
+     * @event Event an event that is triggered when the query is initialized via [[init()]].
+     */
+    const EVENT_INIT = 'init';
+
+
+    /**
+     * Constructor.
+     * @param array $modelClass the model class associated with this query
+     * @param array $config configurations to be applied to the newly created query object
+     */
+    public function __construct($modelClass, $config = [])
+    {
+        $this->modelClass = $modelClass;
+        parent::__construct($config);
+    }
+
+    /**
+     * Initializes the object.
+     * This method is called at the end of the constructor. The default implementation will trigger
+     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
+     * to ensure triggering of the event.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->trigger(self::EVENT_INIT);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function prepare()
+    {
+        if ($this->primaryModel !== null) {
+            // lazy loading
+            if ($this->via instanceof self) {
+                // via pivot collection
+                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
+                $this->filterByModels($viaModels);
+            } elseif (is_array($this->via)) {
+                // via relation
+                /* @var $viaQuery ActiveQuery */
+                list($viaName, $viaQuery) = $this->via;
+                if ($viaQuery->multiple) {
+                    $viaModels = $viaQuery->all();
+                    $this->primaryModel->populateRelation($viaName, $viaModels);
+                } else {
+                    $model = $viaQuery->one();
+                    $this->primaryModel->populateRelation($viaName, $model);
+                    $viaModels = $model === null ? [] : [$model];
+                }
+                $this->filterByModels($viaModels);
+            } else {
+                $this->filterByModels([$this->primaryModel]);
+            }
+        }
+
+        return parent::prepare();
+    }
+
+    /**
+     * Executes query and returns all results as an array.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If null, the Mongo connection returned by [[modelClass]] will be used.
+     * @return array|ActiveRecord the query results. If the query results in nothing, an empty array will be returned.
+     */
+    public function all($db = null)
+    {
+        return parent::all($db);
+    }
+
+    /**
+     * Executes query and returns a single row of result.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If null, the Mongo connection returned by [[modelClass]] will be used.
+     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
+     * the query result may be either an array or an ActiveRecord object. Null will be returned
+     * if the query results in nothing.
+     */
+    public function one($db = null)
+    {
+        $row = parent::one($db);
+        if ($row !== false) {
+            $models = $this->populate([$row]);
+            return reset($models) ?: null;
+        }
+        return null;
+    }
+
+    /**
+     * Performs 'findAndModify' query and returns a single row of result.
+     * Warning: in case 'new' option is set to 'false' (which is by default) usage of this method may lead
+     * to unexpected behavior at some Active Record features, because object will be populated by outdated data.
+     * @param array $update update criteria
+     * @param array $options list of options in format: optionName => optionValue.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * @return ActiveRecord|array|null the original document, or the modified document when $options['new'] is set.
+     * Depending on the setting of [[asArray]], the query result may be either an array or an ActiveRecord object.
+     * Null will be returned if the query results in nothing.
+     */
+    public function modify($update, $options = [], $db = null)
+    {
+        $row = parent::modify($update, $options, $db);
+        if ($row !== null) {
+            $models = $this->populate([$row]);
+            return reset($models) ?: null;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the Mongo collection for this query.
+     * @param Connection $db Mongo connection.
+     * @return Collection collection instance.
+     */
+    public function getCollection($db = null)
+    {
+        /* @var $modelClass ActiveRecord */
+        $modelClass = $this->modelClass;
+        if ($db === null) {
+            $db = $modelClass::getDb();
+        }
+        if ($this->from === null) {
+            $this->from = $modelClass::collectionName();
+        }
+
+        return $db->getCollection($this->from);
+    }
+
+    /**
+     * Converts the raw query results into the format as specified by this query.
+     * This method is internally used to convert the data fetched from MongoDB
+     * into the format as required by this query.
+     * @param array $rows the raw query result from MongoDB
+     * @return array the converted query result
+     */
+    public function populate($rows)
+    {
+        if (empty($rows)) {
+            return [];
+        }
+
+        $models = $this->createModels($rows);
+        if (!empty($this->with)) {
+            $this->findWith($this->with, $models);
+        }
+        if (!$this->asArray) {
+            foreach ($models as $model) {
+                $model->afterFind();
+            }
+        }
+
+        return parent::populate($models);
+    }
+}

+ 414 - 0
vendor/yiisoft/yii2-mongodb/src/ActiveRecord.php

@@ -0,0 +1,414 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use MongoDB\BSON\Binary;
+use MongoDB\BSON\Type;
+use Yii;
+use yii\base\InvalidConfigException;
+use yii\db\BaseActiveRecord;
+use yii\db\StaleObjectException;
+use yii\helpers\ArrayHelper;
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/**
+ * ActiveRecord is the base class for classes representing Mongo documents in terms of objects.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+abstract class ActiveRecord extends BaseActiveRecord
+{
+    /**
+     * Returns the Mongo connection used by this AR class.
+     * By default, the "mongodb" application component is used as the Mongo connection.
+     * You may override this method if you want to use a different database connection.
+     * @return Connection the database connection used by this AR class.
+     */
+    public static function getDb()
+    {
+        return Yii::$app->get('mongodb');
+    }
+
+    /**
+     * Updates all documents in the collection using the provided attribute values and conditions.
+     * For example, to change the status to be 1 for all customers whose status is 2:
+     *
+     * ```php
+     * Customer::updateAll(['status' => 1], ['status' => 2]);
+     * ```
+     *
+     * @param array $attributes attribute values (name-value pairs) to be saved into the collection
+     * @param array $condition description of the objects to update.
+     * Please refer to [[Query::where()]] on how to specify this parameter.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int the number of documents updated.
+     */
+    public static function updateAll($attributes, $condition = [], $options = [])
+    {
+        return static::getCollection()->update($condition, $attributes, $options);
+    }
+
+    /**
+     * Updates all documents in the collection using the provided counter changes and conditions.
+     * For example, to increment all customers' age by 1,
+     *
+     * ```php
+     * Customer::updateAllCounters(['age' => 1]);
+     * ```
+     *
+     * @param array $counters the counters to be updated (attribute name => increment value).
+     * Use negative values if you want to decrement the counters.
+     * @param array $condition description of the objects to update.
+     * Please refer to [[Query::where()]] on how to specify this parameter.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int the number of documents updated.
+     */
+    public static function updateAllCounters($counters, $condition = [], $options = [])
+    {
+        return static::getCollection()->update($condition, ['$inc' => $counters], $options);
+    }
+
+    /**
+     * Deletes documents in the collection using the provided conditions.
+     * WARNING: If you do not specify any condition, this method will delete documents rows in the collection.
+     *
+     * For example, to delete all customers whose status is 3:
+     *
+     * ```php
+     * Customer::deleteAll(['status' => 3]);
+     * ```
+     *
+     * @param array $condition description of the objects to delete.
+     * Please refer to [[Query::where()]] on how to specify this parameter.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int the number of documents deleted.
+     */
+    public static function deleteAll($condition = [], $options = [])
+    {
+        return static::getCollection()->remove($condition, $options);
+    }
+
+    /**
+     * {@inheritdoc}
+     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
+     */
+    public static function find()
+    {
+        return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
+    }
+
+    /**
+     * Declares the name of the Mongo collection associated with this AR class.
+     *
+     * Collection name can be either a string or array:
+     *  - if string considered as the name of the collection inside the default database.
+     *  - if array - first element considered as the name of the database, second - as
+     *    name of collection inside that database
+     *
+     * By default this method returns the class name as the collection name by calling [[Inflector::camel2id()]].
+     * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes
+     * 'order_item'. You may override this method if the collection is not named after this convention.
+     * @return string|array the collection name
+     */
+    public static function collectionName()
+    {
+        return Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
+    }
+
+    /**
+     * Return the Mongo collection instance for this AR class.
+     * @return Collection collection instance.
+     */
+    public static function getCollection()
+    {
+        return static::getDb()->getCollection(static::collectionName());
+    }
+
+    /**
+     * Returns the primary key name(s) for this AR class.
+     * The default implementation will return ['_id'].
+     *
+     * Note that an array should be returned even for a collection with single primary key.
+     *
+     * @return string[] the primary keys of the associated Mongo collection.
+     */
+    public static function primaryKey()
+    {
+        return ['_id'];
+    }
+
+    /**
+     * Returns the list of all attribute names of the model.
+     * This method must be overridden by child classes to define available attributes.
+     * Note: primary key attribute "_id" should be always present in returned array.
+     * For example:
+     *
+     * ```php
+     * public function attributes()
+     * {
+     *     return ['_id', 'name', 'address', 'status'];
+     * }
+     * ```
+     *
+     * @throws \yii\base\InvalidConfigException if not implemented
+     * @return array list of attribute names.
+     */
+    public function attributes()
+    {
+        throw new InvalidConfigException('The attributes() method of mongodb ActiveRecord has to be implemented by child classes.');
+    }
+
+    /**
+     * Inserts a row into the associated Mongo collection using the attribute values of this record.
+     *
+     * This method performs the following steps in order:
+     *
+     * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
+     *    fails, it will skip the rest of the steps;
+     * 2. call [[afterValidate()]] when `$runValidation` is true.
+     * 3. call [[beforeSave()]]. If the method returns false, it will skip the
+     *    rest of the steps;
+     * 4. insert the record into collection. If this fails, it will skip the rest of the steps;
+     * 5. call [[afterSave()]];
+     *
+     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
+     * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
+     * will be raised by the corresponding methods.
+     *
+     * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
+     *
+     * If the primary key  is null during insertion, it will be populated with the actual
+     * value after insertion.
+     *
+     * For example, to insert a customer record:
+     *
+     * ```php
+     * $customer = new Customer();
+     * $customer->name = $name;
+     * $customer->email = $email;
+     * $customer->insert();
+     * ```
+     *
+     * @param bool $runValidation whether to perform validation before saving the record.
+     * If the validation fails, the record will not be inserted into the collection.
+     * @param array $attributes list of attributes that need to be saved. Defaults to null,
+     * meaning all attributes that are loaded will be saved.
+     * @return bool whether the attributes are valid and the record is inserted successfully.
+     * @throws \Exception in case insert failed.
+     */
+    public function insert($runValidation = true, $attributes = null)
+    {
+        if ($runValidation && !$this->validate($attributes)) {
+            return false;
+        }
+        $result = $this->insertInternal($attributes);
+
+        return $result;
+    }
+
+    /**
+     * @see ActiveRecord::insert()
+     */
+    protected function insertInternal($attributes = null)
+    {
+        if (!$this->beforeSave(true)) {
+            return false;
+        }
+        $values = $this->getDirtyAttributes($attributes);
+        if (empty($values)) {
+            $currentAttributes = $this->getAttributes();
+            foreach ($this->primaryKey() as $key) {
+                if (isset($currentAttributes[$key])) {
+                    $values[$key] = $currentAttributes[$key];
+                }
+            }
+        }
+        $newId = static::getCollection()->insert($values);
+        if ($newId !== null) {
+            $this->setAttribute('_id', $newId);
+            $values['_id'] = $newId;
+        }
+
+        $changedAttributes = array_fill_keys(array_keys($values), null);
+        $this->setOldAttributes($values);
+        $this->afterSave(true, $changedAttributes);
+
+        return true;
+    }
+
+    /**
+     * @see ActiveRecord::update()
+     * @throws StaleObjectException
+     */
+    protected function updateInternal($attributes = null)
+    {
+        if (!$this->beforeSave(false)) {
+            return false;
+        }
+        $values = $this->getDirtyAttributes($attributes);
+        if (empty($values)) {
+            $this->afterSave(false, $values);
+            return 0;
+        }
+        $condition = $this->getOldPrimaryKey(true);
+        $lock = $this->optimisticLock();
+        if ($lock !== null) {
+            if (!isset($values[$lock])) {
+                $values[$lock] = $this->$lock + 1;
+            }
+            $condition[$lock] = $this->$lock;
+        }
+        // We do not check the return value of update() because it's possible
+        // that it doesn't change anything and thus returns 0.
+        $rows = static::getCollection()->update($condition, $values);
+
+        if ($lock !== null && !$rows) {
+            throw new StaleObjectException('The object being updated is outdated.');
+        }
+
+        if (isset($values[$lock])) {
+            $this->$lock = $values[$lock];
+        }
+
+        $changedAttributes = [];
+        foreach ($values as $name => $value) {
+            $changedAttributes[$name] = $this->getOldAttribute($name);
+            $this->setOldAttribute($name, $value);
+        }
+        $this->afterSave(false, $changedAttributes);
+
+        return $rows;
+    }
+
+    /**
+     * Deletes the document corresponding to this active record from the collection.
+     *
+     * This method performs the following steps in order:
+     *
+     * 1. call [[beforeDelete()]]. If the method returns false, it will skip the
+     *    rest of the steps;
+     * 2. delete the document from the collection;
+     * 3. call [[afterDelete()]].
+     *
+     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
+     * will be raised by the corresponding methods.
+     *
+     * @return int|bool the number of documents deleted, or false if the deletion is unsuccessful for some reason.
+     * Note that it is possible the number of documents deleted is 0, even though the deletion execution is successful.
+     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
+     * being deleted is outdated.
+     * @throws \Exception in case delete failed.
+     */
+    public function delete()
+    {
+        $result = false;
+        if ($this->beforeDelete()) {
+            $result = $this->deleteInternal();
+            $this->afterDelete();
+        }
+
+        return $result;
+    }
+
+    /**
+     * @see ActiveRecord::delete()
+     * @throws StaleObjectException
+     */
+    protected function deleteInternal()
+    {
+        // we do not check the return value of deleteAll() because it's possible
+        // the record is already deleted in the database and thus the method will return 0
+        $condition = $this->getOldPrimaryKey(true);
+        $lock = $this->optimisticLock();
+        if ($lock !== null) {
+            $condition[$lock] = $this->$lock;
+        }
+        $result = static::getCollection()->remove($condition);
+        if ($lock !== null && !$result) {
+            throw new StaleObjectException('The object being deleted is outdated.');
+        }
+        $this->setOldAttributes(null);
+
+        return $result;
+    }
+
+    /**
+     * Returns a value indicating whether the given active record is the same as the current one.
+     * The comparison is made by comparing the collection names and the primary key values of the two active records.
+     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
+     * @param ActiveRecord $record record to compare to
+     * @return bool whether the two active records refer to the same row in the same Mongo collection.
+     */
+    public function equals($record)
+    {
+        if ($this->isNewRecord || $record->isNewRecord) {
+            return false;
+        }
+
+        return $this->collectionName() === $record->collectionName() && (string) $this->getPrimaryKey() === (string) $record->getPrimaryKey();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toArray(array $fields = [], array $expand = [], $recursive = true)
+    {
+        $data = parent::toArray($fields, $expand, false);
+        if (!$recursive) {
+            return $data;
+        }
+        return $this->toArrayInternal($data);
+    }
+
+    /**
+     * Converts data to array recursively, converting MongoDB BSON objects to readable values.
+     * @param mixed $data the data to be converted into an array.
+     * @return array the array representation of the data.
+     * @since 2.1
+     */
+    private function toArrayInternal($data)
+    {
+        if (is_array($data)) {
+            foreach ($data as $key => $value) {
+                if (is_array($value)) {
+                    $data[$key] = $this->toArrayInternal($value);
+                }
+                if (is_object($value)) {
+                    if ($value instanceof Type) {
+                        $data[$key] = $this->dumpBsonObject($value);
+                    } else {
+                        $data[$key] = ArrayHelper::toArray($value);
+                    }
+                }
+            }
+            return $data;
+        } elseif (is_object($data)) {
+            return ArrayHelper::toArray($data);
+        }
+        return [$data];
+    }
+
+    /**
+     * Converts MongoDB BSON object to readable value.
+     * @param Type $object MongoDB BSON object.
+     * @return array|string object dump value.
+     * @since 2.1
+     */
+    private function dumpBsonObject(Type $object)
+    {
+        if ($object instanceof Binary) {
+            return $object->getData();
+        }
+        if (method_exists($object, '__toString')) {
+            return $object->__toString();
+        }
+        return ArrayHelper::toArray($object);
+    }
+}

+ 189 - 0
vendor/yiisoft/yii2-mongodb/src/BatchQueryResult.php

@@ -0,0 +1,189 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use yii\base\BaseObject;
+use Yii;
+
+/**
+ * BatchQueryResult represents a batch query from which you can retrieve data in batches.
+ *
+ * You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by
+ * calling [[Query::batch()]] or [[Query::each()]]. Because BatchQueryResult implements the `Iterator` interface,
+ * you can iterate it to obtain a batch of data in each iteration. For example,
+ *
+ * ```php
+ * $query = (new Query())->from('user');
+ * foreach ($query->batch() as $i => $users) {
+ *     // $users represents the rows in the $i-th batch
+ * }
+ * foreach ($query->each() as $user) {
+ * }
+ * ```
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class BatchQueryResult extends BaseObject implements \Iterator
+{
+    /**
+     * @var Connection the MongoDB connection to be used when performing batch query.
+     * If null, the "mongodb" application component will be used.
+     */
+    public $db;
+    /**
+     * @var Query the query object associated with this batch query.
+     * Do not modify this property directly unless after [[reset()]] is called explicitly.
+     */
+    public $query;
+    /**
+     * @var int the number of rows to be returned in each batch.
+     */
+    public $batchSize = 100;
+    /**
+     * @var bool whether to return a single row during each iteration.
+     * If false, a whole batch of rows will be returned in each iteration.
+     */
+    public $each = false;
+
+    /**
+     * @var array the data retrieved in the current batch
+     */
+    private $_batch;
+    /**
+     * @var mixed the value for the current iteration
+     */
+    private $_value;
+    /**
+     * @var string|int the key for the current iteration
+     */
+    private $_key;
+    /**
+     * @var \Iterator
+     */
+    private $_iterator;
+
+
+    /**
+     * Resets the batch query.
+     * This method will clean up the existing batch query so that a new batch query can be performed.
+     */
+    public function reset()
+    {
+        $this->_iterator = null;
+        $this->_batch = null;
+        $this->_value = null;
+        $this->_key = null;
+    }
+
+    /**
+     * Resets the iterator to the initial state.
+     * This method is required by the interface Iterator.
+     */
+    public function rewind()
+    {
+        $this->reset();
+        $this->next();
+    }
+
+    /**
+     * Moves the internal pointer to the next dataset.
+     * This method is required by the interface Iterator.
+     */
+    public function next()
+    {
+        if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
+            $this->_batch = $this->fetchData();
+            reset($this->_batch);
+        }
+
+        if ($this->each) {
+            $this->_value = current($this->_batch);
+            if ($this->query->indexBy !== null) {
+                $this->_key = key($this->_batch);
+            } elseif (key($this->_batch) !== null) {
+                $this->_key++;
+            } else {
+                $this->_key = null;
+            }
+        } else {
+            $this->_value = $this->_batch;
+            $this->_key = $this->_key === null ? 0 : $this->_key + 1;
+        }
+    }
+
+    /**
+     * Fetches the next batch of data.
+     * @return array the data fetched
+     */
+    protected function fetchData()
+    {
+        if ($this->_iterator === null) {
+            if (empty($this->query->orderBy)) {
+                // setting cursor batch size may setup implicit limit on the query with 'sort'
+                // @see https://jira.mongodb.org/browse/PHP-457
+                $this->query->addOptions(['batchSize' => $this->batchSize]);
+            }
+            $cursor = $this->query->buildCursor($this->db);
+            $token = 'fetch cursor id = ' . $cursor->getId();
+            Yii::info($token, __METHOD__);
+
+            if ($cursor instanceof \Iterator) {
+                $this->_iterator = $cursor;
+            } else {
+                $this->_iterator = new \IteratorIterator($cursor);
+            }
+
+            $this->_iterator->rewind();
+        }
+
+        $rows = [];
+        $count = 0;
+
+        while ($count++ < $this->batchSize) {
+            $row = $this->_iterator->current();
+            if ($row === null) {
+                break;
+            }
+            $this->_iterator->next();
+            //var_dump($row);
+            $rows[] = $row;
+        }
+        return $this->query->populate($rows);
+    }
+
+    /**
+     * Returns the index of the current dataset.
+     * This method is required by the interface Iterator.
+     * @return int the index of the current row.
+     */
+    public function key()
+    {
+        return $this->_key;
+    }
+
+    /**
+     * Returns the current dataset.
+     * This method is required by the interface Iterator.
+     * @return mixed the current dataset.
+     */
+    public function current()
+    {
+        return $this->_value;
+    }
+
+    /**
+     * Returns whether there is a valid dataset at the current position.
+     * This method is required by the interface Iterator.
+     * @return bool whether there is a valid dataset at the current position.
+     */
+    public function valid()
+    {
+        return !empty($this->_batch);
+    }
+}

+ 198 - 0
vendor/yiisoft/yii2-mongodb/src/Cache.php

@@ -0,0 +1,198 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use Yii;
+use yii\base\InvalidConfigException;
+use yii\di\Instance;
+
+/**
+ * Cache implements a cache application component by storing cached data in a MongoDB.
+ *
+ * By default, Cache stores session data in a MongoDB collection named 'cache' inside the default database.
+ * This collection is better to be pre-created with fields 'id' and 'expire' indexed.
+ * The collection name can be changed by setting [[cacheCollection]].
+ *
+ * Please refer to [[\yii\caching\Cache]] for common cache operations that are supported by Cache.
+ *
+ * The following example shows how you can configure the application to use Cache:
+ *
+ * ```php
+ * 'cache' => [
+ *     'class' => 'yii\mongodb\Cache',
+ *     // 'db' => 'mymongodb',
+ *     // 'cacheCollection' => 'my_cache',
+ * ]
+ * ```
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Cache extends \yii\caching\Cache
+{
+    /**
+     * @var Connection|array|string the MongoDB connection object or the application component ID of the MongoDB connection.
+     * After the Cache object is created, if you want to change this property, you should only assign it
+     * with a MongoDB connection object.
+     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
+     */
+    public $db = 'mongodb';
+    /**
+     * @var string|array the name of the MongoDB collection that stores the cache data.
+     * Please refer to [[Connection::getCollection()]] on how to specify this parameter.
+     * This collection is better to be pre-created with fields 'id' and 'expire' indexed.
+     */
+    public $cacheCollection = 'cache';
+    /**
+     * @var int the probability (parts per million) that garbage collection (GC) should be performed
+     * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance.
+     * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all.
+     */
+    public $gcProbability = 100;
+
+
+    /**
+     * Initializes the Cache component.
+     * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection.
+     * @throws InvalidConfigException if [[db]] is invalid.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->db = Instance::ensure($this->db, Connection::className());
+    }
+
+    /**
+     * Retrieves a value from cache with a specified key.
+     * This method should be implemented by child classes to retrieve the data
+     * from specific cache storage.
+     * @param string $key a unique key identifying the cached value
+     * @return string|bool the value stored in cache, false if the value is not in the cache or expired.
+     */
+    protected function getValue($key)
+    {
+        $query = new Query;
+        $row = $query->select(['data'])
+            ->from($this->cacheCollection)
+            ->where([
+                'id' => $key,
+                '$or' => [
+                    [
+                        'expire' => 0
+                    ],
+                    [
+                        'expire' => ['$gt' => time()]
+                    ],
+                ],
+            ])
+            ->one($this->db);
+
+        if (empty($row)) {
+            return false;
+        }
+        return $row['data'];
+    }
+
+    /**
+     * Stores a value identified by a key in cache.
+     * This method should be implemented by child classes to store the data
+     * in specific cache storage.
+     * @param string $key the key identifying the value to be cached
+     * @param string $value the value to be cached
+     * @param int $expire the number of seconds in which the cached value will expire. 0 means never expire.
+     * @return bool true if the value is successfully stored into cache, false otherwise
+     */
+    protected function setValue($key, $value, $expire)
+    {
+        $result = $this->db->getCollection($this->cacheCollection)
+            ->update(['id' => $key], [
+                'expire' => $expire > 0 ? $expire + time() : 0,
+                'data' => $value,
+            ]);
+
+        if ($result) {
+            $this->gc();
+            return true;
+        }
+        return $this->addValue($key, $value, $expire);
+    }
+
+    /**
+     * Stores a value identified by a key into cache if the cache does not contain this key.
+     * This method should be implemented by child classes to store the data
+     * in specific cache storage.
+     * @param string $key the key identifying the value to be cached
+     * @param string $value the value to be cached
+     * @param int $expire the number of seconds in which the cached value will expire. 0 means never expire.
+     * @return bool true if the value is successfully stored into cache, false otherwise
+     */
+    protected function addValue($key, $value, $expire)
+    {
+        $this->gc();
+
+        if ($expire > 0) {
+            $expire += time();
+        } else {
+            $expire = 0;
+        }
+
+        try {
+            $this->db->getCollection($this->cacheCollection)
+                ->insert([
+                    'id' => $key,
+                    'expire' => $expire,
+                    'data' => $value,
+                ]);
+
+            return true;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * Deletes a value with the specified key from cache
+     * This method should be implemented by child classes to delete the data from actual cache storage.
+     * @param string $key the key of the value to be deleted
+     * @return bool if no error happens during deletion
+     */
+    protected function deleteValue($key)
+    {
+        $this->db->getCollection($this->cacheCollection)->remove(['id' => $key]);
+        return true;
+    }
+
+    /**
+     * Deletes all values from cache.
+     * Child classes may implement this method to realize the flush operation.
+     * @return bool whether the flush operation was successful.
+     */
+    protected function flushValues()
+    {
+        $this->db->getCollection($this->cacheCollection)->remove();
+        return true;
+    }
+
+    /**
+     * Removes the expired data values.
+     * @param bool $force whether to enforce the garbage collection regardless of [[gcProbability]].
+     * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]].
+     */
+    public function gc($force = false)
+    {
+        if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
+            $this->db->getCollection($this->cacheCollection)
+                ->remove([
+                    'expire' => [
+                        '$gt' => 0,
+                        '$lt' => time(),
+                    ]
+                ]);
+        }
+    }
+}

+ 436 - 0
vendor/yiisoft/yii2-mongodb/src/Collection.php

@@ -0,0 +1,436 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use MongoDB\BSON\ObjectID;
+use yii\base\BaseObject;
+use Yii;
+
+/**
+ * Collection represents the Mongo collection information.
+ *
+ * A collection object is usually created by calling [[Database::getCollection()]] or [[Connection::getCollection()]].
+ *
+ * Collection provides the basic interface for the Mongo queries, mostly: insert, update, delete operations.
+ * For example:
+ *
+ * ```php
+ * $collection = Yii::$app->mongodb->getCollection('customer');
+ * $collection->insert(['name' => 'John Smith', 'status' => 1]);
+ * ```
+ *
+ * Collection also provides shortcut for [[Command]] methods, such as [[group()]], [[mapReduce()]] and so on.
+ *
+ * To perform "find" queries, please use [[Query]] instead.
+ *
+ * @property string $fullName Full name of this collection, including database name. This property is
+ * read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Collection extends BaseObject
+{
+    /**
+     * @var Database MongoDB database instance.
+     */
+    public $database;
+    /**
+     * @var string name of this collection.
+     */
+    public $name;
+
+
+    /**
+     * @return string full name of this collection, including database name.
+     */
+    public function getFullName()
+    {
+        return $this->database->name . '.' . $this->name;
+    }
+
+    /**
+     * Drops this collection.
+     * @throws Exception on failure.
+     * @return bool whether the operation successful.
+     */
+    public function drop()
+    {
+        return $this->database->dropCollection($this->name);
+    }
+
+    /**
+     * Returns the list of defined indexes.
+     * @return array list of indexes info.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @since 2.1
+     */
+    public function listIndexes($options = [])
+    {
+        return $this->database->createCommand()->listIndexes($this->name, $options);
+    }
+
+    /**
+     * Creates several indexes at once.
+     * Example:
+     *
+     * ```php
+     * $collection = Yii::$app->mongo->getCollection('customer');
+     * $collection->createIndexes([
+     *     [
+     *         'key' => ['name'],
+     *     ],
+     *     [
+     *         'key' => [
+     *             'email' => 1,
+     *             'address' => -1,
+     *         ],
+     *         'name' => 'my_index'
+     *     ],
+     * ]);
+     * ```
+     *
+     * @param array $indexes indexes specification, each index should be specified as an array.
+     * @param array[] $indexes indexes specification. Each specification should be an array in format: optionName => value
+     * The main options are:
+     *
+     * - keys: array, column names with sort order, to be indexed. This option is mandatory.
+     * - unique: bool, whether to create unique index.
+     * - name: string, the name of the index, if not set it will be generated automatically.
+     * - background: bool, whether to bind index in the background.
+     * - sparse: bool, whether index should reference only documents with the specified field.
+     *
+     * See [[https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options-for-all-index-types]]
+     * for the full list of options.
+     * @return bool whether operation was successful.
+     * @since 2.1
+     */
+    public function createIndexes($indexes)
+    {
+        return $this->database->createCommand()->createIndexes($this->name, $indexes);
+    }
+
+    /**
+     * Drops collection indexes by name.
+     * @param string $indexes wildcard for name of the indexes to be dropped.
+     * You can use `*` to drop all indexes.
+     * @return int count of dropped indexes.
+     */
+    public function dropIndexes($indexes)
+    {
+        $result = $this->database->createCommand()->dropIndexes($this->name, $indexes);
+        return $result['nIndexesWas'];
+    }
+
+    /**
+     * Creates an index on the collection and the specified fields.
+     * @param array|string $columns column name or list of column names.
+     * If array is given, each element in the array has as key the field name, and as
+     * value either 1 for ascending sort, or -1 for descending sort.
+     * You can specify field using native numeric key with the field name as a value,
+     * in this case ascending sort will be used.
+     * For example:
+     *
+     * ```php
+     * [
+     *     'name',
+     *     'status' => -1,
+     * ]
+     * ```
+     *
+     * @param array $options list of options in format: optionName => optionValue.
+     * @throws Exception on failure.
+     * @return bool whether the operation successful.
+     */
+    public function createIndex($columns, $options = [])
+    {
+        $index = array_merge(['key' => $columns], $options);
+        return $this->database->createCommand()->createIndexes($this->name, [$index]);
+    }
+
+    /**
+     * Drop indexes for specified column(s).
+     * @param string|array $columns column name or list of column names.
+     * If array is given, each element in the array has as key the field name, and as
+     * value either 1 for ascending sort, or -1 for descending sort.
+     * Use value 'text' to specify text index.
+     * You can specify field using native numeric key with the field name as a value,
+     * in this case ascending sort will be used.
+     * For example:
+     *
+     * ```php
+     * [
+     *     'name',
+     *     'status' => -1,
+     *     'description' => 'text',
+     * ]
+     * ```
+     *
+     * @throws Exception on failure.
+     * @return bool whether the operation successful.
+     */
+    public function dropIndex($columns)
+    {
+        $existingIndexes = $this->listIndexes();
+
+        $indexKey = $this->database->connection->getQueryBuilder()->buildSortFields($columns);
+        foreach ($existingIndexes as $index) {
+            if ($index['key'] == $indexKey) {
+                $this->database->createCommand()->dropIndexes($this->name, $index['name']);
+                return true;
+            }
+        }
+
+        // Index plugin usage such as 'text' may cause unpredictable index 'key' structure, thus index name should be used
+        $indexName = $this->database->connection->getQueryBuilder()->generateIndexName($indexKey);
+        foreach ($existingIndexes as $index) {
+            if ($index['name'] === $indexName) {
+                $this->database->createCommand()->dropIndexes($this->name, $index['name']);
+                return true;
+            }
+        }
+
+        throw new Exception('Index to be dropped does not exist.');
+    }
+
+    /**
+     * Drops all indexes for this collection.
+     * @throws Exception on failure.
+     * @return int count of dropped indexes.
+     */
+    public function dropAllIndexes()
+    {
+        $result = $this->database->createCommand()->dropIndexes($this->name, '*');
+        return $result['nIndexesWas'];
+    }
+
+    /**
+     * Returns a cursor for the search results.
+     * In order to perform "find" queries use [[Query]] class.
+     * @param array $condition query condition
+     * @param array $fields fields to be selected
+     * @param array $options query options (available since 2.1).
+     * @return \MongoDB\Driver\Cursor cursor for the search results
+     * @see Query
+     */
+    public function find($condition = [], $fields = [], $options = [])
+    {
+        if (!empty($fields)) {
+            $options['projection'] = $fields;
+        }
+        return $this->database->createCommand()->find($this->name, $condition, $options);
+    }
+
+    /**
+     * Returns a single document.
+     * @param array $condition query condition
+     * @param array $fields fields to be selected
+     * @param array $options query options (available since 2.1).
+     * @return array|null the single document. Null is returned if the query results in nothing.
+     */
+    public function findOne($condition = [], $fields = [], $options = [])
+    {
+        $options['limit'] = 1;
+        $cursor = $this->find($condition, $fields, $options);
+        $rows = $cursor->toArray();
+        return empty($rows) ? null : current($rows);
+    }
+
+    /**
+     * Updates a document and returns it.
+     * @param array $condition query condition
+     * @param array $update update criteria
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array|null the original document, or the modified document when $options['new'] is set.
+     * @throws Exception on failure.
+     */
+    public function findAndModify($condition, $update, $options = [])
+    {
+        return $this->database->createCommand()->findAndModify($this->name, $condition, $update, $options);
+    }
+
+    /**
+     * Inserts new data into collection.
+     * @param array|object $data data to be inserted.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return \MongoDB\BSON\ObjectID new record ID instance.
+     * @throws Exception on failure.
+     */
+    public function insert($data, $options = [])
+    {
+        return $this->database->createCommand()->insert($this->name, $data, $options);
+    }
+
+    /**
+     * Inserts several new rows into collection.
+     * @param array $rows array of arrays or objects to be inserted.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array inserted data, each row will have "_id" key assigned to it.
+     * @throws Exception on failure.
+     */
+    public function batchInsert($rows, $options = [])
+    {
+        $insertedIds = $this->database->createCommand()->batchInsert($this->name, $rows, $options);
+        foreach ($rows as $key => $row) {
+            $rows[$key]['_id'] = $insertedIds[$key];
+        }
+        return $rows;
+    }
+
+    /**
+     * Updates the rows, which matches given criteria by given data.
+     * Note: for "multi" mode Mongo requires explicit strategy "$set" or "$inc"
+     * to be specified for the "newData". If no strategy is passed "$set" will be used.
+     * @param array $condition description of the objects to update.
+     * @param array $newData the object with which to update the matching records.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int|bool number of updated documents or whether operation was successful.
+     * @throws Exception on failure.
+     */
+    public function update($condition, $newData, $options = [])
+    {
+        $writeResult = $this->database->createCommand()->update($this->name, $condition, $newData, $options);
+        return $writeResult->getModifiedCount() + $writeResult->getUpsertedCount();
+    }
+
+    /**
+     * Update the existing database data, otherwise insert this data
+     * @param array|object $data data to be updated/inserted.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return \MongoId updated/new record id instance.
+     * @throws Exception on failure.
+     */
+    public function save($data, $options = [])
+    {
+        if (empty($data['_id'])) {
+            return $this->insert($data, $options);
+        }
+        $id = $data['_id'];
+        unset($data['_id']);
+        $this->update(['_id' => $id], ['$set' => $data], ['upsert' => true]);
+
+        return is_object($id) ? $id : new ObjectID($id);
+    }
+
+    /**
+     * Removes data from the collection.
+     * @param array $condition description of records to remove.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int|bool number of updated documents or whether operation was successful.
+     * @throws Exception on failure.
+     */
+    public function remove($condition = [], $options = [])
+    {
+        $options = array_merge(['limit' => 0], $options);
+        $writeResult = $this->database->createCommand()->delete($this->name, $condition, $options);
+        return $writeResult->getDeletedCount();
+    }
+
+    /**
+     * Counts records in this collection.
+     * @param array $condition query condition
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int records count.
+     * @since 2.1
+     */
+    public function count($condition = [], $options = [])
+    {
+        return $this->database->createCommand()->count($this->name, $condition, $options);
+    }
+
+    /**
+     * Returns a list of distinct values for the given column across a collection.
+     * @param string $column column to use.
+     * @param array $condition query parameters.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array|bool array of distinct values, or "false" on failure.
+     * @throws Exception on failure.
+     */
+    public function distinct($column, $condition = [], $options = [])
+    {
+        return $this->database->createCommand()->distinct($this->name, $column, $condition, $options);
+    }
+
+    /**
+     * Performs aggregation using Mongo Aggregation Framework.
+     * In case 'cursor' option is specified [[\MongoDB\Driver\Cursor]] instance is returned,
+     * otherwise - an array of aggregation results.
+     * @param array $pipelines list of pipeline operators.
+     * @param array $options optional parameters.
+     * @return array|\MongoDB\Driver\Cursor the result of the aggregation.
+     * @throws Exception on failure.
+     */
+    public function aggregate($pipelines, $options = [])
+    {
+        return $this->database->createCommand()->aggregate($this->name, $pipelines, $options);
+    }
+
+    /**
+     * Performs aggregation using Mongo "group" command.
+     * @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. If instance of [[\MongoDB\BSON\Javascript]] passed,
+     * it will be treated as a function that returns the key to group by.
+     * @param array $initial Initial value of the aggregation counter object.
+     * @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the current
+     * document and the aggregation to this point) and does the aggregation.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param array $options optional parameters to the group command. Valid options include:
+     *  - condition - criteria for including a document in the aggregation.
+     *  - finalize - function called once per unique key that takes the final output of the reduce function.
+     * @return array the result of the aggregation.
+     * @throws Exception on failure.
+     */
+    public function group($keys, $initial, $reduce, $options = [])
+    {
+        return $this->database->createCommand()->group($this->name, $keys, $initial, $reduce, $options);
+    }
+
+    /**
+     * Performs aggregation using MongoDB "map-reduce" mechanism.
+     * Note: this function will not return the aggregation result, instead it will
+     * write it inside the another Mongo collection specified by "out" parameter.
+     * For example:
+     *
+     * ```php
+     * $customerCollection = Yii::$app->mongo->getCollection('customer');
+     * $resultCollectionName = $customerCollection->mapReduce(
+     *     'function () {emit(this.status, this.amount)}',
+     *     'function (key, values) {return Array.sum(values)}',
+     *     'mapReduceOut',
+     *     ['status' => 3]
+     * );
+     * $query = new Query();
+     * $results = $query->from($resultCollectionName)->all();
+     * ```
+     *
+     * @param \MongoDB\BSON\Javascript|string $map function, which emits map data from collection.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the map key
+     * and the map values) and does the aggregation.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param string|array $out output collection name. It could be a string for simple output
+     * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']).
+     * You can pass ['inline' => true] to fetch the result at once without temporary collection usage.
+     * @param array $condition criteria for including a document in the aggregation.
+     * @param array $options additional optional parameters to the mapReduce command. Valid options include:
+     *
+     * - sort: array, key to sort the input documents. The sort key must be in an existing index for this collection.
+     * - limit: int, the maximum number of documents to return in the collection.
+     * - finalize: \MongoDB\BSON\Javascript|string, function, which follows the reduce method and modifies the output.
+     * - scope: array, specifies global variables that are accessible in the map, reduce and finalize functions.
+     * - jsMode: bool, specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
+     * - verbose: bool, specifies whether to include the timing information in the result information.
+     *
+     * @return string|array the map reduce output collection name or output results.
+     * @throws Exception on failure.
+     */
+    public function mapReduce($map, $reduce, $out, $condition = [], $options = [])
+    {
+        return $this->database->createCommand()->mapReduce($this->name, $map, $reduce, $out, $condition, $options);
+    }
+}

+ 842 - 0
vendor/yiisoft/yii2-mongodb/src/Command.php

@@ -0,0 +1,842 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use MongoDB\BSON\ObjectID;
+use MongoDB\Driver\BulkWrite;
+use MongoDB\Driver\Exception\RuntimeException;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\WriteResult;
+use Yii;
+use yii\base\InvalidConfigException;
+use yii\base\BaseObject;
+
+/**
+ * Command represents MongoDB statement such as command or query.
+ *
+ * A command object is usually created by calling [[Connection::createCommand()]] or [[Database::createCommand()]].
+ * The statement it represents can be set via the [[document]] property.
+ *
+ * To execute a non-query command, such as 'listIndexes', 'count', 'distinct' and so on, call [[execute()]].
+ * For example:
+ *
+ * ```php
+ * $result = Yii::$app->mongodb->createCommand(['listIndexes' => 'some_collection'])->execute();
+ * ```
+ *
+ * To execute a 'find' command, which return cursor, call [[query()]].
+ * For example:
+ *
+ * ```php
+ * $cursor = Yii::$app->mongodb->createCommand(['projection' => ['name' => true]])->query('some_collection');
+ * ```
+ *
+ * To execute batch (bulk) operations, call [[executeBatch()]].
+ * For example:
+ *
+ * ```php
+ * Yii::$app->mongodb->createCommand()
+ *     ->addInsert(['name' => 'new'])
+ *     ->addUpdate(['name' => 'existing'], ['name' => 'updated'])
+ *     ->addDelete(['name' => 'old'])
+ *     ->executeBatch('some_collection');
+ * ```
+ *
+ * @property ReadConcern|string $readConcern Read concern to be used in this command.
+ * @property ReadPreference $readPreference Read preference. Note that the type of this property differs in
+ * getter and setter. See [[getReadPreference()]] and [[setReadPreference()]] for details.
+ * @property WriteConcern|null $writeConcern Write concern to be used in this command. Note that the type of
+ * this property differs in getter and setter. See [[getWriteConcern()]] and [[setWriteConcern()]] for details.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class Command extends BaseObject
+{
+    /**
+     * @var Connection the MongoDB connection that this command is associated with.
+     */
+    public $db;
+    /**
+     * @var string name of the database that this command is associated with.
+     */
+    public $databaseName;
+    /**
+     * @var array command document contents.
+     */
+    public $document = [];
+
+    /**
+     * @var ReadPreference|int|string|null command read preference.
+     */
+    private $_readPreference;
+    /**
+     * @var WriteConcern|int|string|null write concern to be used by this command.
+     */
+    private $_writeConcern;
+    /**
+     * @var ReadConcern|string read concern to be used by this command
+     */
+    private $_readConcern;
+
+
+    /**
+     * Returns read preference for this command.
+     * @return ReadPreference read preference.
+     */
+    public function getReadPreference()
+    {
+        if (!is_object($this->_readPreference)) {
+            if ($this->_readPreference === null) {
+                $this->_readPreference = $this->db->manager->getReadPreference();
+            } elseif (is_scalar($this->_readPreference)) {
+                $this->_readPreference = new ReadPreference($this->_readPreference);
+            }
+        }
+        return $this->_readPreference;
+    }
+
+    /**
+     * Sets read preference for this command.
+     * @param ReadPreference|int|string|null $readPreference read reference, it can be specified as
+     * instance of [[ReadPreference]] or scalar mode value, for example: `ReadPreference::RP_PRIMARY`.
+     * @return $this self reference.
+     */
+    public function setReadPreference($readPreference)
+    {
+        $this->_readPreference = $readPreference;
+        return $this;
+    }
+
+    /**
+     * Returns write concern for this command.
+     * @return WriteConcern|null write concern to be used in this command.
+     */
+    public function getWriteConcern()
+    {
+        if ($this->_writeConcern !== null) {
+            if (is_scalar($this->_writeConcern)) {
+                $this->_writeConcern = new WriteConcern($this->_writeConcern);
+            }
+        }
+        return $this->_writeConcern;
+    }
+
+    /**
+     * Sets write concern for this command.
+     * @param WriteConcern|int|string|null $writeConcern write concern, it can be an instance of [[WriteConcern]]
+     * or its scalar mode value, for example: `majority`.
+     * @return $this self reference
+     */
+    public function setWriteConcern($writeConcern)
+    {
+        $this->_writeConcern = $writeConcern;
+        return $this;
+    }
+
+    /**
+     * Retuns read concern for this command.
+     * @return ReadConcern|string read concern to be used in this command.
+     */
+    public function getReadConcern()
+    {
+        if ($this->_readConcern !== null) {
+            if (is_scalar($this->_readConcern)) {
+                $this->_readConcern = new ReadConcern($this->_readConcern);
+            }
+        }
+        return $this->_readConcern;
+    }
+
+    /**
+     * Sets read concern for this command.
+     * @param ReadConcern|string $readConcern read concern, it can be an instance of [[ReadConcern]] or
+     * scalar level value, for example: 'local'.
+     * @return $this self reference
+     */
+    public function setReadConcern($readConcern)
+    {
+        $this->_readConcern = $readConcern;
+        return $this;
+    }
+
+    /**
+     * Executes this command.
+     * @return \MongoDB\Driver\Cursor result cursor.
+     * @throws Exception on failure.
+     */
+    public function execute()
+    {
+        $databaseName = $this->databaseName === null ? $this->db->defaultDatabaseName : $this->databaseName;
+
+        $token = $this->log([$databaseName, 'command'], $this->document, __METHOD__);
+
+        try {
+            $this->beginProfile($token, __METHOD__);
+
+            $this->db->open();
+            $mongoCommand = new \MongoDB\Driver\Command($this->document);
+            $cursor = $this->db->manager->executeCommand($databaseName, $mongoCommand, $this->getReadPreference());
+            $cursor->setTypeMap($this->db->typeMap);
+
+            $this->endProfile($token, __METHOD__);
+        } catch (RuntimeException $e) {
+            $this->endProfile($token, __METHOD__);
+            throw new Exception($e->getMessage(), $e->getCode(), $e);
+        }
+
+        return $cursor;
+    }
+
+    /**
+     * Execute commands batch (bulk).
+     * @param string $collectionName collection name.
+     * @param array $options batch options.
+     * @return array array of 2 elements:
+     *
+     * - 'insertedIds' - contains inserted IDs.
+     * - 'result' - [[\MongoDB\Driver\WriteResult]] instance.
+     *
+     * @throws Exception on failure.
+     * @throws InvalidConfigException on invalid [[document]] format.
+     */
+    public function executeBatch($collectionName, $options = [])
+    {
+        $databaseName = $this->databaseName === null ? $this->db->defaultDatabaseName : $this->databaseName;
+
+        $token = $this->log([$databaseName, $collectionName, 'bulkWrite'], $this->document, __METHOD__);
+
+        try {
+            $this->beginProfile($token, __METHOD__);
+
+            $batch = new BulkWrite($options);
+
+            $insertedIds = [];
+            foreach ($this->document as $key => $operation) {
+                switch ($operation['type']) {
+                    case 'insert':
+                        $insertedIds[$key] = $batch->insert($operation['document']);
+                        break;
+                    case 'update':
+                        $batch->update($operation['condition'], $operation['document'], $operation['options']);
+                        break;
+                    case 'delete':
+                        $batch->delete($operation['condition'], isset($operation['options']) ? $operation['options'] : []);
+                        break;
+                    default:
+                        throw new InvalidConfigException("Unsupported batch operation type '{$operation['type']}'");
+                }
+            }
+
+            $this->db->open();
+            $writeResult = $this->db->manager->executeBulkWrite($databaseName . '.' . $collectionName, $batch, $this->getWriteConcern());
+
+            $this->endProfile($token, __METHOD__);
+        } catch (RuntimeException $e) {
+            $this->endProfile($token, __METHOD__);
+            throw new Exception($e->getMessage(), $e->getCode(), $e);
+        }
+
+        return [
+            'insertedIds' => $insertedIds,
+            'result' => $writeResult,
+        ];
+    }
+
+    /**
+     * Executes this command as a mongo query
+     * @param string $collectionName collection name
+     * @param array $options query options.
+     * @return \MongoDB\Driver\Cursor result cursor.
+     * @throws Exception on failure
+     */
+    public function query($collectionName, $options = [])
+    {
+        $databaseName = $this->databaseName === null ? $this->db->defaultDatabaseName : $this->databaseName;
+
+        $token = $this->log(
+            'find',
+            array_merge(
+                [
+                    'ns' => $databaseName . '.' . $collectionName,
+                    'filter' => $this->document,
+                ],
+                $options
+            ),
+            __METHOD__
+        );
+
+        $readConcern = $this->getReadConcern();
+        if ($readConcern !== null) {
+            $options['readConcern'] = $readConcern;
+        }
+
+        try {
+            $this->beginProfile($token, __METHOD__);
+
+            $query = new \MongoDB\Driver\Query($this->document, $options);
+            $this->db->open();
+            $cursor = $this->db->manager->executeQuery($databaseName . '.' . $collectionName, $query, $this->getReadPreference());
+            $cursor->setTypeMap($this->db->typeMap);
+
+            $this->endProfile($token, __METHOD__);
+        } catch (RuntimeException $e) {
+            $this->endProfile($token, __METHOD__);
+            throw new Exception($e->getMessage(), $e->getCode(), $e);
+        }
+
+        return $cursor;
+    }
+
+    /**
+     * Drops database associated with this command.
+     * @return bool whether operation was successful.
+     */
+    public function dropDatabase()
+    {
+        $this->document = $this->db->getQueryBuilder()->dropDatabase();
+
+        $result = current($this->execute()->toArray());
+        return $result['ok'] > 0;
+    }
+
+    /**
+     * Creates new collection in database associated with this command.s
+     * @param string $collectionName collection name
+     * @param array $options collection options in format: "name" => "value"
+     * @return bool whether operation was successful.
+     */
+    public function createCollection($collectionName, array $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->createCollection($collectionName, $options);
+
+        $result = current($this->execute()->toArray());
+        return $result['ok'] > 0;
+    }
+
+    /**
+     * Drops specified collection.
+     * @param string $collectionName name of the collection to be dropped.
+     * @return bool whether operation was successful.
+     */
+    public function dropCollection($collectionName)
+    {
+        $this->document = $this->db->getQueryBuilder()->dropCollection($collectionName);
+
+        $result = current($this->execute()->toArray());
+        return $result['ok'] > 0;
+    }
+
+    /**
+     * Creates indexes in the collection.
+     * @param string $collectionName collection name.
+     * @param array[] $indexes indexes specification. Each specification should be an array in format: optionName => value
+     * The main options are:
+     *
+     * - keys: array, column names with sort order, to be indexed. This option is mandatory.
+     * - unique: bool, whether to create unique index.
+     * - name: string, the name of the index, if not set it will be generated automatically.
+     * - background: bool, whether to bind index in the background.
+     * - sparse: bool, whether index should reference only documents with the specified field.
+     *
+     * See [[https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options-for-all-index-types]]
+     * for the full list of options.
+     * @return bool whether operation was successful.
+     */
+    public function createIndexes($collectionName, $indexes)
+    {
+        $this->document = $this->db->getQueryBuilder()->createIndexes($this->databaseName, $collectionName, $indexes);
+
+        $result = current($this->execute()->toArray());
+        return $result['ok'] > 0;
+    }
+
+    /**
+     * Drops collection indexes by name.
+     * @param string $collectionName collection name.
+     * @param string $indexes wildcard for name of the indexes to be dropped.
+     * @return array result data.
+     */
+    public function dropIndexes($collectionName, $indexes)
+    {
+        $this->document = $this->db->getQueryBuilder()->dropIndexes($collectionName, $indexes);
+
+        return current($this->execute()->toArray());
+    }
+
+    /**
+     * Returns information about current collection indexes.
+     * @param string $collectionName collection name
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array list of indexes info.
+     * @throws Exception on failure.
+     */
+    public function listIndexes($collectionName, $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->listIndexes($collectionName, $options);
+
+        try {
+            $cursor = $this->execute();
+        } catch (Exception $e) {
+            // The server may return an error if the collection does not exist.
+            $notFoundCodes = [
+                26, // namespace not found
+                60 // database not found
+            ];
+            if (in_array($e->getCode(), $notFoundCodes, true)) {
+                return [];
+            }
+
+            throw $e;
+        }
+
+        return $cursor->toArray();
+    }
+
+    /**
+     * Counts records in specified collection.
+     * @param string $collectionName collection name
+     * @param array $condition filter condition
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int records count
+     */
+    public function count($collectionName, $condition = [], $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->count($collectionName, $condition, $options);
+
+        $result = current($this->execute()->toArray());
+        return $result['n'];
+    }
+
+    /**
+     * Adds the insert operation to the batch command.
+     * @param array $document document to be inserted
+     * @return $this self reference.
+     * @see executeBatch()
+     */
+    public function addInsert($document)
+    {
+        $this->document[] = [
+            'type' => 'insert',
+            'document' => $document,
+        ];
+        return $this;
+    }
+
+    /**
+     * Adds the update operation to the batch command.
+     * @param array $condition filter condition
+     * @param array $document data to be updated
+     * @param array $options update options.
+     * @return $this self reference.
+     * @see executeBatch()
+     */
+    public function addUpdate($condition, $document, $options = [])
+    {
+        $options = array_merge(
+            [
+                'multi' => true,
+                'upsert' => false,
+            ],
+            $options
+        );
+
+        if ($options['multi']) {
+            $keys = array_keys($document);
+            if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) {
+                $document = ['$set' => $document];
+            }
+        }
+
+        $this->document[] = [
+            'type' => 'update',
+            'condition' => $this->db->getQueryBuilder()->buildCondition($condition),
+            'document' => $document,
+            'options' => $options,
+        ];
+        return $this;
+    }
+
+    /**
+     * Adds the delete operation to the batch command.
+     * @param array $condition filter condition.
+     * @param array $options delete options.
+     * @return $this self reference.
+     * @see executeBatch()
+     */
+    public function addDelete($condition, $options = [])
+    {
+        $this->document[] = [
+            'type' => 'delete',
+            'condition' => $this->db->getQueryBuilder()->buildCondition($condition),
+            'options' => $options,
+        ];
+        return $this;
+    }
+
+    /**
+     * Inserts new document into collection.
+     * @param string $collectionName collection name
+     * @param array $document document content
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return ObjectID|bool inserted record ID, `false` - on failure.
+     */
+    public function insert($collectionName, $document, $options = [])
+    {
+        $this->document = [];
+        $this->addInsert($document);
+        $result = $this->executeBatch($collectionName, $options);
+
+        if ($result['result']->getInsertedCount() < 1) {
+            return false;
+        }
+
+        return reset($result['insertedIds']);
+    }
+
+    /**
+     * Inserts batch of new documents into collection.
+     * @param string $collectionName collection name
+     * @param array[] $documents documents list
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array|false list of inserted IDs, `false` on failure.
+     */
+    public function batchInsert($collectionName, $documents, $options = [])
+    {
+        $this->document = [];
+        foreach ($documents as $key => $document) {
+            $this->document[$key] = [
+                'type' => 'insert',
+                'document' => $document
+            ];
+        }
+
+        $result = $this->executeBatch($collectionName, $options);
+
+        if ($result['result']->getInsertedCount() < 1) {
+            return false;
+        }
+
+        return $result['insertedIds'];
+    }
+
+    /**
+     * Update existing documents in the collection.
+     * @param string $collectionName collection name
+     * @param array $condition filter condition
+     * @param array $document data to be updated.
+     * @param array $options update options.
+     * @return WriteResult write result.
+     */
+    public function update($collectionName, $condition, $document, $options = [])
+    {
+        $batchOptions = [];
+        foreach (['bypassDocumentValidation'] as $name) {
+            if (isset($options[$name])) {
+                $batchOptions[$name] = $options[$name];
+                unset($options[$name]);
+            }
+        }
+
+        $this->document = [];
+        $this->addUpdate($condition, $document, $options);
+        $result = $this->executeBatch($collectionName, $batchOptions);
+
+        return $result['result'];
+    }
+
+    /**
+     * Removes documents from the collection.
+     * @param string $collectionName collection name.
+     * @param array $condition filter condition.
+     * @param array $options delete options.
+     * @return WriteResult write result.
+     */
+    public function delete($collectionName, $condition, $options = [])
+    {
+        $batchOptions = [];
+        foreach (['bypassDocumentValidation'] as $name) {
+            if (isset($options[$name])) {
+                $batchOptions[$name] = $options[$name];
+                unset($options[$name]);
+            }
+        }
+
+        $this->document = [];
+        $this->addDelete($condition, $options);
+        $result = $this->executeBatch($collectionName, $batchOptions);
+
+        return $result['result'];
+    }
+
+    /**
+     * Performs find query.
+     * @param string $collectionName collection name
+     * @param array $condition filter condition
+     * @param array $options query options.
+     * @return \MongoDB\Driver\Cursor result cursor.
+     */
+    public function find($collectionName, $condition, $options = [])
+    {
+        $queryBuilder = $this->db->getQueryBuilder();
+
+        $this->document = $queryBuilder->buildCondition($condition);
+
+        if (isset($options['projection'])) {
+            $options['projection'] = $queryBuilder->buildSelectFields($options['projection']);
+        }
+
+        if (isset($options['sort'])) {
+            $options['sort'] = $queryBuilder->buildSortFields($options['sort']);
+        }
+
+        if (array_key_exists('limit', $options)) {
+            if ($options['limit'] === null || !ctype_digit((string) $options['limit'])) {
+                unset($options['limit']);
+            } else {
+                $options['limit'] = (int)$options['limit'];
+            }
+        }
+        if (array_key_exists('skip', $options)) {
+            if ($options['skip'] === null || !ctype_digit((string) $options['skip'])) {
+                unset($options['skip']);
+            } else {
+                $options['skip'] = (int)$options['skip'];
+            }
+        }
+
+        return $this->query($collectionName, $options);
+    }
+
+    /**
+     * Updates a document and returns it.
+     * @param $collectionName
+     * @param array $condition query condition
+     * @param array $update update criteria
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array|null the original document, or the modified document when $options['new'] is set.
+     */
+    public function findAndModify($collectionName, $condition = [], $update = [], $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->findAndModify($collectionName, $condition, $update, $options);
+        $cursor = $this->execute();
+
+        $result = current($cursor->toArray());
+
+        if (!isset($result['value'])) {
+            return null;
+        }
+
+        return $result['value'];
+    }
+
+    /**
+     * Returns a list of distinct values for the given column across a collection.
+     * @param string $collectionName collection name.
+     * @param string $fieldName field name to use.
+     * @param array $condition query parameters.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array array of distinct values, or "false" on failure.
+     */
+    public function distinct($collectionName, $fieldName, $condition = [], $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->distinct($collectionName, $fieldName, $condition, $options);
+        $cursor = $this->execute();
+
+        $result = current($cursor->toArray());
+
+        if (!isset($result['values']) || !is_array($result['values'])) {
+            return false;
+        }
+
+        return $result['values'];
+    }
+
+    /**
+     * Performs aggregation using MongoDB "group" command.
+     * @param string $collectionName collection name.
+     * @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. If instance of [[\MongoDB\BSON\Javascript]] passed,
+     * it will be treated as a function that returns the key to group by.
+     * @param array $initial Initial value of the aggregation counter object.
+     * @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the current
+     * document and the aggregation to this point) and does the aggregation.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param array $options optional parameters to the group command. Valid options include:
+     *  - condition - criteria for including a document in the aggregation.
+     *  - finalize - function called once per unique key that takes the final output of the reduce function.
+     * @return array the result of the aggregation.
+     */
+    public function group($collectionName, $keys, $initial, $reduce, $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->group($collectionName, $keys, $initial, $reduce, $options);
+        $cursor = $this->execute();
+
+        $result = current($cursor->toArray());
+
+        return $result['retval'];
+    }
+
+    /**
+     * Performs MongoDB "map-reduce" command.
+     * @param string $collectionName collection name.
+     * @param \MongoDB\BSON\Javascript|string $map function, which emits map data from collection.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the map key
+     * and the map values) and does the aggregation.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param string|array $out output collection name. It could be a string for simple output
+     * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']).
+     * You can pass ['inline' => true] to fetch the result at once without temporary collection usage.
+     * @param array $condition filter condition for including a document in the aggregation.
+     * @param array $options additional optional parameters to the mapReduce command. Valid options include:
+     *
+     *  - sort: array, key to sort the input documents. The sort key must be in an existing index for this collection.
+     *  - limit: int, the maximum number of documents to return in the collection.
+     *  - finalize: \MongoDB\BSON\Javascript|string, function, which follows the reduce method and modifies the output.
+     *  - scope: array, specifies global variables that are accessible in the map, reduce and finalize functions.
+     *  - jsMode: bool, specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
+     *  - verbose: bool, specifies whether to include the timing information in the result information.
+     *
+     * @return string|array the map reduce output collection name or output results.
+     */
+    public function mapReduce($collectionName, $map, $reduce, $out, $condition = [], $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->mapReduce($collectionName, $map, $reduce, $out, $condition, $options);
+        $cursor = $this->execute();
+
+        $result = current($cursor->toArray());
+
+        return array_key_exists('results', $result) ? $result['results'] : $result['result'];
+    }
+
+    /**
+     * Performs aggregation using MongoDB Aggregation Framework.
+     * In case 'cursor' option is specified [[\MongoDB\Driver\Cursor]] instance is returned,
+     * otherwise - an array of aggregation results.
+     * @param string $collectionName collection name
+     * @param array $pipelines list of pipeline operators.
+     * @param array $options optional parameters.
+     * @return array|\MongoDB\Driver\Cursor aggregation result.
+     */
+    public function aggregate($collectionName, $pipelines, $options = [])
+    {
+        if (empty($options['cursor'])) {
+            $returnCursor = false;
+            $options['cursor'] = new \stdClass();
+        } else {
+            $returnCursor = true;
+        }
+
+        $this->document = $this->db->getQueryBuilder()->aggregate($collectionName, $pipelines, $options);
+        $cursor = $this->execute();
+
+        if ($returnCursor) {
+            return $cursor;
+        }
+
+        return $cursor->toArray();
+    }
+
+    /**
+     * Return an explanation of the query, often useful for optimization and debugging.
+     * @param string $collectionName collection name
+     * @param array $query query document.
+     * @return array explanation of the query.
+     */
+    public function explain($collectionName, $query)
+    {
+        $this->document = $this->db->getQueryBuilder()->explain($collectionName, $query);
+        $cursor = $this->execute();
+
+        return current($cursor->toArray());
+    }
+
+    /**
+     * Returns the list of available databases.
+     * @param array $condition filter condition.
+     * @param array $options options list.
+     * @return array database information
+     */
+    public function listDatabases($condition = [], $options = [])
+    {
+        if ($this->databaseName === null) {
+            $this->databaseName = 'admin';
+        }
+        $this->document = $this->db->getQueryBuilder()->listDatabases($condition, $options);
+
+        $cursor = $this->execute();
+        $result = current($cursor->toArray());
+
+        if (empty($result['databases'])) {
+            return [];
+        }
+        return $result['databases'];
+    }
+
+    /**
+     * Returns the list of available collections.
+     * @param array $condition filter condition.
+     * @param array $options options list.
+     * @return array collections information.
+     */
+    public function listCollections($condition = [], $options = [])
+    {
+        $this->document = $this->db->getQueryBuilder()->listCollections($condition, $options);
+        $cursor = $this->execute();
+
+        return $cursor->toArray();
+    }
+
+    // Logging :
+
+    /**
+     * Logs the command data if logging is enabled at [[db]].
+     * @param array|string $namespace command namespace.
+     * @param array $data command data.
+     * @param string $category log category
+     * @return string|false log token, `false` if log is not enabled.
+     */
+    protected function log($namespace, $data, $category)
+    {
+        if ($this->db->enableLogging) {
+            $token = $this->db->getLogBuilder()->generateToken($namespace, $data);
+            Yii::info($token, $category);
+            return $token;
+        }
+        return false;
+    }
+
+    /**
+     * Marks the beginning of a code block for profiling.
+     * @param string $token token for the code block
+     * @param string $category the category of this log message
+     * @see endProfile()
+     */
+    protected function beginProfile($token, $category)
+    {
+        if ($token !== false && $this->db->enableProfiling) {
+            Yii::beginProfile($token, $category);
+        }
+    }
+
+    /**
+     * Marks the end of a code block for profiling.
+     * @param string $token token for the code block
+     * @param string $category the category of this log message
+     * @see beginProfile()
+     */
+    protected function endProfile($token, $category)
+    {
+        if ($token !== false && $this->db->enableProfiling) {
+            Yii::endProfile($token, $category);
+        }
+    }
+}

+ 435 - 0
vendor/yiisoft/yii2-mongodb/src/Connection.php

@@ -0,0 +1,435 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use MongoDB\Driver\Manager;
+use yii\base\Component;
+use yii\base\InvalidConfigException;
+use Yii;
+
+/**
+ * Connection represents a connection to a MongoDb server.
+ *
+ * Connection works together with [[Database]] and [[Collection]] to provide data access
+ * to the Mongo database. They are wrappers of the [[MongoDB PHP extension]](http://us1.php.net/manual/en/book.mongo.php).
+ *
+ * To establish a DB connection, set [[dsn]] and then call [[open()]] to be true.
+ *
+ * The following example shows how to create a Connection instance and establish
+ * the DB connection:
+ *
+ * ```php
+ * $connection = new \yii\mongodb\Connection([
+ *     'dsn' => $dsn,
+ * ]);
+ * $connection->open();
+ * ```
+ *
+ * After the Mongo connection is established, one can access Mongo databases and collections:
+ *
+ * ```php
+ * $database = $connection->getDatabase('my_mongo_db');
+ * $collection = $database->getCollection('customer');
+ * $collection->insert(['name' => 'John Smith', 'status' => 1]);
+ * ```
+ *
+ * You can work with several different databases at the same server using this class.
+ * However, while it is unlikely your application will actually need it, the Connection class
+ * provides ability to use [[defaultDatabaseName]] as well as a shortcut method [[getCollection()]]
+ * to retrieve a particular collection instance:
+ *
+ * ```php
+ * // get collection 'customer' from default database:
+ * $collection = $connection->getCollection('customer');
+ * // get collection 'customer' from database 'mydatabase':
+ * $collection = $connection->getCollection(['mydatabase', 'customer']);
+ * ```
+ *
+ * Connection is often used as an application component and configured in the application
+ * configuration like the following:
+ *
+ * ```php
+ * [
+ *      'components' => [
+ *          'mongodb' => [
+ *              'class' => '\yii\mongodb\Connection',
+ *              'dsn' => 'mongodb://developer:password@localhost:27017/mydatabase',
+ *          ],
+ *      ],
+ * ]
+ * ```
+ *
+ * @property Database $database Database instance. This property is read-only.
+ * @property string $defaultDatabaseName Default database name.
+ * @property file\Collection $fileCollection Mongo GridFS collection instance. This property is read-only.
+ * @property bool $isActive Whether the Mongo connection is established. This property is read-only.
+ * @property LogBuilder $logBuilder The log builder for this connection. Note that the type of this property
+ * differs in getter and setter. See [[getLogBuilder()]] and [[setLogBuilder()]] for details.
+ * @property QueryBuilder $queryBuilder The query builder for the this MongoDB connection. Note that the type
+ * of this property differs in getter and setter. See [[getQueryBuilder()]] and [[setQueryBuilder()]] for
+ * details.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Connection extends Component
+{
+    /**
+     * @event Event an event that is triggered after a DB connection is established
+     */
+    const EVENT_AFTER_OPEN = 'afterOpen';
+
+    /**
+     * @var string host:port
+     *
+     * Correct syntax is:
+     * mongodb://[username:password@]host1[:port1][,host2[:port2:],...][/dbname]
+     * For example:
+     * mongodb://localhost:27017
+     * mongodb://developer:password@localhost:27017
+     * mongodb://developer:password@localhost:27017/mydatabase
+     */
+    public $dsn;
+    /**
+     * @var array connection options.
+     * For example:
+     *
+     * ```php
+     * [
+     *     'socketTimeoutMS' => 1000, // how long a send or receive on a socket can take before timing out
+     *     'ssl' => true // initiate the connection with TLS/SSL
+     * ]
+     * ```
+     *
+     * @see https://docs.mongodb.com/manual/reference/connection-string/#connections-connection-options
+     */
+    public $options = [];
+    /**
+     * @var array options for the MongoDB driver.
+     * Any driver-specific options not included in MongoDB connection string specification.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-manager.construct.php
+     */
+    public $driverOptions = [];
+    /**
+     * @var Manager MongoDB driver manager.
+     * @since 2.1
+     */
+    public $manager;
+    /**
+     * @var array type map to use for BSON unserialization.
+     * Note: default type map will be automatically merged into this field, possibly overriding user-defined values.
+     * @see http://php.net/manual/en/mongodb-driver-cursor.settypemap.php
+     * @since 2.1
+     */
+    public $typeMap = [];
+    /**
+     * @var bool whether to log command and query executions.
+     * When enabled this option may reduce performance. MongoDB commands may contain large data,
+     * consuming both CPU and memory.
+     * It makes sense to disable this option in the production environment.
+     * @since 2.1
+     */
+    public $enableLogging = true;
+    /**
+     * @var bool whether to enable profiling the commands and queries being executed.
+     * This option will have no effect in case [[enableLogging]] is disabled.
+     * @since 2.1
+     */
+    public $enableProfiling = true;
+    /**
+     * @var string name of the protocol, which should be used for the GridFS stream wrapper.
+     * Only alphanumeric values are allowed: do not use any URL special characters, such as '/', '&', ':' etc.
+     * @see \yii\mongodb\file\StreamWrapper
+     * @since 2.1
+     */
+    public $fileStreamProtocol = 'gridfs';
+    /**
+     * @var string name of the class, which should serve as a stream wrapper for [[fileStreamProtocol]] protocol.
+     * @since 2.1
+     */
+    public $fileStreamWrapperClass = 'yii\mongodb\file\StreamWrapper';
+
+    /**
+     * @var string name of the MongoDB database to use by default.
+     * If this field left blank, connection instance will attempt to determine it from
+     * [[dsn]] automatically, if needed.
+     */
+    private $_defaultDatabaseName;
+    /**
+     * @var Database[] list of Mongo databases
+     */
+    private $_databases = [];
+    /**
+     * @var QueryBuilder|array|string the query builder for this connection
+     * @since 2.1
+     */
+    private $_queryBuilder = 'yii\mongodb\QueryBuilder';
+    /**
+     * @var LogBuilder|array|string log entries builder used for this connecton.
+     * @since 2.1
+     */
+    private $_logBuilder = 'yii\mongodb\LogBuilder';
+    /**
+     * @var bool whether GridFS stream wrapper has been already registered.
+     * @since 2.1
+     */
+    private $_fileStreamWrapperRegistered = false;
+
+
+    /**
+     * Sets default database name.
+     * @param string $name default database name.
+     */
+    public function setDefaultDatabaseName($name)
+    {
+        $this->_defaultDatabaseName = $name;
+    }
+
+    /**
+     * Returns default database name, if it is not set,
+     * attempts to determine it from [[dsn]] value.
+     * @return string default database name
+     * @throws \yii\base\InvalidConfigException if unable to determine default database name.
+     */
+    public function getDefaultDatabaseName()
+    {
+        if ($this->_defaultDatabaseName === null) {
+            if (preg_match('/^mongodb:\\/\\/.+\\/([^?&]+)/s', $this->dsn, $matches)) {
+                $this->_defaultDatabaseName = $matches[1];
+            } else {
+                throw new InvalidConfigException("Unable to determine default database name from dsn.");
+            }
+        }
+
+        return $this->_defaultDatabaseName;
+    }
+
+    /**
+     * Returns the query builder for the this MongoDB connection.
+     * @return QueryBuilder the query builder for the this MongoDB connection.
+     * @since 2.1
+     */
+    public function getQueryBuilder()
+    {
+        if (!is_object($this->_queryBuilder)) {
+            $this->_queryBuilder = Yii::createObject($this->_queryBuilder, [$this]);
+        }
+        return $this->_queryBuilder;
+    }
+
+    /**
+     * Sets the query builder for the this MongoDB connection.
+     * @param QueryBuilder|array|string|null $queryBuilder the query builder for this MongoDB connection.
+     * @since 2.1
+     */
+    public function setQueryBuilder($queryBuilder)
+    {
+        $this->_queryBuilder = $queryBuilder;
+    }
+
+    /**
+     * Returns log builder for this connection.
+     * @return LogBuilder the log builder for this connection.
+     * @since 2.1
+     */
+    public function getLogBuilder()
+    {
+        if (!is_object($this->_logBuilder)) {
+            $this->_logBuilder = Yii::createObject($this->_logBuilder);
+        }
+        return $this->_logBuilder;
+    }
+
+    /**
+     * Sets log builder used for this connection.
+     * @param array|string|LogBuilder $logBuilder the log builder for this connection.
+     * @since 2.1
+     */
+    public function setLogBuilder($logBuilder)
+    {
+        $this->_logBuilder = $logBuilder;
+    }
+
+    /**
+     * Returns the MongoDB database with the given name.
+     * @param string|null $name database name, if null default one will be used.
+     * @param bool $refresh whether to reestablish the database connection even, if it is found in the cache.
+     * @return Database database instance.
+     */
+    public function getDatabase($name = null, $refresh = false)
+    {
+        if ($name === null) {
+            $name = $this->getDefaultDatabaseName();
+        }
+        if ($refresh || !array_key_exists($name, $this->_databases)) {
+            $this->_databases[$name] = $this->selectDatabase($name);
+        }
+
+        return $this->_databases[$name];
+    }
+
+    /**
+     * Selects the database with given name.
+     * @param string $name database name.
+     * @return Database database instance.
+     */
+    protected function selectDatabase($name)
+    {
+        return Yii::createObject([
+            'class' => 'yii\mongodb\Database',
+            'name' => $name,
+            'connection' => $this,
+        ]);
+    }
+
+    /**
+     * Returns the MongoDB collection with the given name.
+     * @param string|array $name collection name. If string considered as the name of the collection
+     * inside the default database. If array - first element considered as the name of the database,
+     * second - as name of collection inside that database
+     * @param bool $refresh whether to reload the collection instance even if it is found in the cache.
+     * @return Collection Mongo collection instance.
+     */
+    public function getCollection($name, $refresh = false)
+    {
+        if (is_array($name)) {
+            list ($dbName, $collectionName) = $name;
+            return $this->getDatabase($dbName)->getCollection($collectionName, $refresh);
+        }
+        return $this->getDatabase()->getCollection($name, $refresh);
+    }
+
+    /**
+     * Returns the MongoDB GridFS collection.
+     * @param string|array $prefix collection prefix. If string considered as the prefix of the GridFS
+     * collection inside the default database. If array - first element considered as the name of the database,
+     * second - as prefix of the GridFS collection inside that database, if no second element present
+     * default "fs" prefix will be used.
+     * @param bool $refresh whether to reload the collection instance even if it is found in the cache.
+     * @return file\Collection Mongo GridFS collection instance.
+     */
+    public function getFileCollection($prefix = 'fs', $refresh = false)
+    {
+        if (is_array($prefix)) {
+            list ($dbName, $collectionPrefix) = $prefix;
+            if (!isset($collectionPrefix)) {
+                $collectionPrefix = 'fs';
+            }
+
+            return $this->getDatabase($dbName)->getFileCollection($collectionPrefix, $refresh);
+        }
+        return $this->getDatabase()->getFileCollection($prefix, $refresh);
+    }
+
+    /**
+     * Returns a value indicating whether the Mongo connection is established.
+     * @return bool whether the Mongo connection is established
+     */
+    public function getIsActive()
+    {
+        return is_object($this->manager) && $this->manager->getServers() !== [];
+    }
+
+    /**
+     * Establishes a Mongo connection.
+     * It does nothing if a MongoDB connection has already been established.
+     * @throws Exception if connection fails
+     */
+    public function open()
+    {
+        if ($this->manager === null) {
+            if (empty($this->dsn)) {
+                throw new InvalidConfigException($this->className() . '::dsn cannot be empty.');
+            }
+            $token = 'Opening MongoDB connection: ' . $this->dsn;
+            try {
+                Yii::trace($token, __METHOD__);
+                Yii::beginProfile($token, __METHOD__);
+                $options = $this->options;
+
+                $this->manager = new Manager($this->dsn, $options, $this->driverOptions);
+                $this->manager->selectServer($this->manager->getReadPreference());
+
+                $this->initConnection();
+                Yii::endProfile($token, __METHOD__);
+            } catch (\Exception $e) {
+                Yii::endProfile($token, __METHOD__);
+                throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
+            }
+
+            $this->typeMap = array_merge(
+                $this->typeMap,
+                [
+                    'root' => 'array',
+                    'document' => 'array'
+                ]
+            );
+        }
+    }
+
+    /**
+     * Closes the currently active DB connection.
+     * It does nothing if the connection is already closed.
+     */
+    public function close()
+    {
+        if ($this->manager !== null) {
+            Yii::trace('Closing MongoDB connection: ' . $this->dsn, __METHOD__);
+            $this->manager = null;
+            foreach ($this->_databases as $database) {
+                $database->clearCollections();
+            }
+            $this->_databases = [];
+        }
+    }
+
+    /**
+     * Initializes the DB connection.
+     * This method is invoked right after the DB connection is established.
+     * The default implementation triggers an [[EVENT_AFTER_OPEN]] event.
+     */
+    protected function initConnection()
+    {
+        $this->trigger(self::EVENT_AFTER_OPEN);
+    }
+
+    /**
+     * Creates MongoDB command.
+     * @param array $document command document contents.
+     * @param string|null $databaseName database name, if not set [[defaultDatabaseName]] will be used.
+     * @return Command command instance.
+     * @since 2.1
+     */
+    public function createCommand($document = [], $databaseName = null)
+    {
+        return new Command([
+            'db' => $this,
+            'databaseName' => $databaseName,
+            'document' => $document,
+        ]);
+    }
+
+    /**
+     * Registers GridFS stream wrapper for the [[fileStreamProtocol]] protocol.
+     * @param bool $force whether to enforce registration even wrapper has been already registered.
+     * @return string registered stream protocol name.
+     */
+    public function registerFileStreamWrapper($force = false)
+    {
+        if ($force || !$this->_fileStreamWrapperRegistered) {
+            /* @var $class \yii\mongodb\file\StreamWrapper */
+            $class = $this->fileStreamWrapperClass;
+            $class::register($this->fileStreamProtocol, $force);
+
+            $this->_fileStreamWrapperRegistered = true;
+        }
+
+        return $this->fileStreamProtocol;
+    }
+}

+ 158 - 0
vendor/yiisoft/yii2-mongodb/src/Database.php

@@ -0,0 +1,158 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use yii\base\BaseObject;
+use Yii;
+
+/**
+ * Database represents the MongoDB database information.
+ *
+ * @property file\Collection $fileCollection Mongo GridFS collection. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Database extends BaseObject
+{
+    /**
+     * @var Connection MongoDB connection.
+     */
+    public $connection;
+    /**
+     * @var string name of this database.
+     */
+    public $name;
+
+    /**
+     * @var Collection[] list of collections.
+     */
+    private $_collections = [];
+    /**
+     * @var file\Collection[] list of GridFS collections.
+     */
+    private $_fileCollections = [];
+
+
+    /**
+     * Returns the Mongo collection with the given name.
+     * @param string $name collection name
+     * @param bool $refresh whether to reload the collection instance even if it is found in the cache.
+     * @return Collection Mongo collection instance.
+     */
+    public function getCollection($name, $refresh = false)
+    {
+        if ($refresh || !array_key_exists($name, $this->_collections)) {
+            $this->_collections[$name] = $this->selectCollection($name);
+        }
+
+        return $this->_collections[$name];
+    }
+
+    /**
+     * Returns Mongo GridFS collection with given prefix.
+     * @param string $prefix collection prefix.
+     * @param bool $refresh whether to reload the collection instance even if it is found in the cache.
+     * @return file\Collection Mongo GridFS collection.
+     */
+    public function getFileCollection($prefix = 'fs', $refresh = false)
+    {
+        if ($refresh || !array_key_exists($prefix, $this->_fileCollections)) {
+            $this->_fileCollections[$prefix] = $this->selectFileCollection($prefix);
+        }
+
+        return $this->_fileCollections[$prefix];
+    }
+
+    /**
+     * Selects collection with given name.
+     * @param string $name collection name.
+     * @return Collection collection instance.
+     */
+    protected function selectCollection($name)
+    {
+        return Yii::createObject([
+            'class' => 'yii\mongodb\Collection',
+            'database' => $this,
+            'name' => $name,
+        ]);
+    }
+
+    /**
+     * Selects GridFS collection with given prefix.
+     * @param string $prefix file collection prefix.
+     * @return file\Collection file collection instance.
+     */
+    protected function selectFileCollection($prefix)
+    {
+        return Yii::createObject([
+            'class' => 'yii\mongodb\file\Collection',
+            'database' => $this,
+            'prefix' => $prefix,
+        ]);
+    }
+
+    /**
+     * Creates MongoDB command associated with this database.
+     * @param array $document command document contents.
+     * @return Command command instance.
+     * @since 2.1
+     */
+    public function createCommand($document = [])
+    {
+        return $this->connection->createCommand($document, $this->name);
+    }
+
+    /**
+     * Creates new collection.
+     * Note: Mongo creates new collections automatically on the first demand,
+     * this method makes sense only for the migration script or for the case
+     * you need to create collection with the specific options.
+     * @param string $name name of the collection
+     * @param array $options collection options in format: "name" => "value"
+     * @return bool whether operation was successful.
+     * @throws Exception on failure.
+     */
+    public function createCollection($name, $options = [])
+    {
+        return $this->createCommand()->createCollection($name, $options);
+    }
+
+    /**
+     * Drops specified collection.
+     * @param string $name name of the collection
+     * @return bool whether operation was successful.
+     * @since 2.1
+     */
+    public function dropCollection($name)
+    {
+        return $this->createCommand()->dropCollection($name);
+    }
+
+    /**
+     * Returns the list of available collections in this database.
+     * @param array $condition filter condition.
+     * @param array $options options list.
+     * @return array collections information.
+     * @since 2.1.1
+     */
+    public function listCollections($condition = [], $options = [])
+    {
+        return $this->createCommand()->listCollections($condition, $options);
+    }
+
+    /**
+     * Clears internal collection lists.
+     * This method can be used to break cycle references between [[Database]] and [[Collection]] instances.
+     */
+    public function clearCollections()
+    {
+        $this->_collections = [];
+        $this->_fileCollections = [];
+    }
+}

+ 25 - 0
vendor/yiisoft/yii2-mongodb/src/Exception.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+/**
+ * Exception represents an exception that is caused by some Mongo-related operations.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Exception extends \yii\base\Exception
+{
+    /**
+     * @return string the user-friendly name of this exception
+     */
+    public function getName()
+    {
+        return 'MongoDB Exception';
+    }
+}

+ 131 - 0
vendor/yiisoft/yii2-mongodb/src/LogBuilder.php

@@ -0,0 +1,131 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use MongoDB\BSON\Binary;
+use MongoDB\BSON\Javascript;
+use MongoDB\BSON\MaxKey;
+use MongoDB\BSON\MinKey;
+use MongoDB\BSON\ObjectID;
+use MongoDB\BSON\Regex;
+use MongoDB\BSON\Timestamp;
+use MongoDB\BSON\Type;
+use MongoDB\BSON\UTCDatetime;
+use yii\base\BaseObject;
+
+/**
+ * LogBuilder allows composition and escaping of the log entries.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class LogBuilder extends BaseObject
+{
+    /**
+     * Generate log/profile token.
+     * @param string|array $namespace command namespace
+     * @param array $data command data.
+     * @return string token.
+     */
+    public function generateToken($namespace, $data = [])
+    {
+        if (is_array($namespace)) {
+            $namespace = implode('.', $namespace);
+        }
+        return $namespace . '(' . $this->encodeData($data) . ')';
+    }
+
+    /**
+     * Encodes complex log data into JSON format string.
+     * @param mixed $data raw data.
+     * @return string encoded data string.
+     */
+    public function encodeData($data)
+    {
+        return json_encode($this->processData($data));
+    }
+
+    /**
+     * Pre-processes the log data before sending it to `json_encode()`.
+     * @param mixed $data raw data.
+     * @return mixed the processed data.
+     */
+    protected function processData($data)
+    {
+        if (is_object($data)) {
+            if ($data instanceof ObjectID ||
+                $data instanceof Regex ||
+                $data instanceof UTCDateTime ||
+                $data instanceof Timestamp
+            ) {
+                $data = get_class($data) . '(' . $data->__toString() . ')';
+            } elseif ($data instanceof Javascript) {
+                $data = $this->processJavascript($data);
+            } elseif ($data instanceof MinKey || $data instanceof MaxKey) {
+                $data = get_class($data);
+            } elseif ($data instanceof Binary) {
+                if (in_array($data->getType(), [Binary::TYPE_MD5, Binary::TYPE_UUID, Binary::TYPE_OLD_UUID], true)) {
+                    $data = $data->getData();
+                } else {
+                    $data = get_class($data) . '(...)';
+                }
+            } elseif ($data instanceof Type) {
+                // Covers 'Binary', 'DBRef' and others
+                $data = get_class($data) . '(...)';
+            } else {
+                $result = [];
+                foreach ($data as $name => $value) {
+                    $result[$name] = $value;
+                }
+                $data = $result;
+            }
+
+            if ($data === []) {
+                return new \stdClass();
+            }
+        }
+
+        if (is_array($data)) {
+            foreach ($data as $key => $value) {
+                if (is_array($value) || is_object($value)) {
+                    $data[$key] = $this->processData($value);
+                }
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Processes [[Javascript]] composing recoverable value.
+     * @param Javascript $javascript javascript BSON object.
+     * @return string processed javascript.
+     */
+    private function processJavascript(Javascript $javascript)
+    {
+        $dump = print_r($javascript, true);
+        $beginPos = strpos($dump, '[javascript] => ');
+        if ($beginPos === false) {
+            $beginPos = strpos($dump, '[code] => ');
+            if ($beginPos === false) {
+                return $dump;
+            }
+            $beginPos += strlen('[code] => ');
+        } else {
+            $beginPos += strlen('[javascript] => ');
+        }
+
+        $endPos = strrpos($dump, '[scope] => ');
+        if ($endPos === false || $beginPos > $endPos) {
+            return $dump;
+        }
+        $content = substr($dump, $beginPos, $endPos - $beginPos);
+
+        return get_class($javascript) . '(' . trim($content, " \n\t") . ')';
+    }
+}

+ 293 - 0
vendor/yiisoft/yii2-mongodb/src/Migration.php

@@ -0,0 +1,293 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use yii\base\Component;
+use yii\db\MigrationInterface;
+use yii\di\Instance;
+use yii\helpers\Json;
+
+/**
+ * Migration is the base class for representing a MongoDB migration.
+ *
+ * Each child class of Migration represents an individual MongoDB migration which
+ * is identified by the child class name.
+ *
+ * Within each migration, the [[up()]] method should be overridden to contain the logic
+ * for "upgrading" the database; while the [[down()]] method for the "downgrading"
+ * logic.
+ *
+ * Migration provides a set of convenient methods for manipulating MongoDB data and schema.
+ * For example, the [[createIndex()]] method can be used to create a collection index.
+ * Compared with the same methods in [[Collection]], these methods will display extra
+ * information showing the method parameters and execution time, which may be useful when
+ * applying migrations.
+ *
+ * @author Klimov Paul <klimov@zfort.com>
+ * @since 2.0
+ */
+abstract class Migration extends Component implements MigrationInterface
+{
+    /**
+     * @var Connection|array|string the MongoDB connection object or the application component ID of the MongoDB connection
+     * that this migration should work with.
+     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
+     */
+    public $db = 'mongodb';
+    /**
+     * @var bool indicates whether the log output should be compacted.
+     * If this is set to true, the individual commands ran within the migration will not be output to the console log.
+     * Default is `false`, in other words the output is fully verbose by default.
+     * @since 2.1.5
+     */
+    public $compact = false;
+
+
+    /**
+     * Initializes the migration.
+     * This method will set [[db]] to be the 'db' application component, if it is null.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->db = Instance::ensure($this->db, Connection::className());
+    }
+
+    /**
+     * Creates new collection with the specified options.
+     * @param string|array $collection name of the collection
+     * @param array $options collection options in format: "name" => "value"
+     */
+    public function createCollection($collection, $options = [])
+    {
+        if (is_array($collection)) {
+            list($database, $collectionName) = $collection;
+        } else {
+            $database = null;
+            $collectionName = $collection;
+        }
+        $this->beginProfile($token = "    > create collection " . $this->composeCollectionLogName($collection) . " ...");
+        $this->db->getDatabase($database)->createCollection($collectionName, $options);
+        $this->endProfile($token);
+    }
+
+    /**
+     * Drops existing collection.
+     * @param string|array $collection name of the collection
+     */
+    public function dropCollection($collection)
+    {
+        $this->beginProfile($token = "    > drop collection " . $this->composeCollectionLogName($collection) . " ...");
+        $this->db->getCollection($collection)->drop();
+        $this->endProfile($token);
+    }
+
+    /**
+     * Creates indexes in the collection.
+     * @param string|array $collection name of the collection
+     * @param array $indexes indexes specifications.
+     * @since 2.1
+     */
+    public function createIndexes($collection, $indexes)
+    {
+        $this->beginProfile($token = "    > create indexes on " . $this->composeCollectionLogName($collection) . " (" . Json::encode($indexes) . ") ...");
+        $this->db->getCollection($collection)->createIndexes($indexes);
+        $this->endProfile($token);
+    }
+
+    /**
+     * Drops collection indexes by name.
+     * @param string|array $collection name of the collection
+     * @param string $indexes wildcard for name of the indexes to be dropped.
+     * @since 2.1
+     */
+    public function dropIndexes($collection, $indexes)
+    {
+        $this->beginProfile($token = "    > drop indexes '{$indexes}' on " . $this->composeCollectionLogName($collection) . ") ...");
+        $this->db->getCollection($collection)->dropIndexes($indexes);
+        $this->endProfile($token);
+    }
+
+    /**
+     * Creates an index on the collection and the specified fields.
+     * @param string|array $collection name of the collection
+     * @param array|string $columns column name or list of column names.
+     * @param array $options list of options in format: optionName => optionValue.
+     */
+    public function createIndex($collection, $columns, $options = [])
+    {
+        $this->beginProfile($token = "    > create index on " . $this->composeCollectionLogName($collection) . " (" . Json::encode((array) $columns) . empty($options) ? "" : ", " . Json::encode($options) . ") ...");
+        $this->db->getCollection($collection)->createIndex($columns, $options);
+        $this->endProfile($token);
+    }
+
+    /**
+     * Drop indexes for specified column(s).
+     * @param string|array $collection name of the collection
+     * @param string|array $columns column name or list of column names.
+     */
+    public function dropIndex($collection, $columns)
+    {
+        $this->beginProfile($token = "    > drop index on " . $this->composeCollectionLogName($collection) . " (" . Json::encode((array) $columns) . ") ...");
+        $this->db->getCollection($collection)->dropIndex($columns);
+        $this->endProfile($token);
+    }
+
+    /**
+     * Drops all indexes for specified collection.
+     * @param string|array $collection name of the collection.
+     */
+    public function dropAllIndexes($collection)
+    {
+        $this->beginProfile($token = "    > drop all indexes on " . $this->composeCollectionLogName($collection) . ") ...");
+        $this->db->getCollection($collection)->dropAllIndexes();
+        $this->endProfile($token);
+    }
+
+    /**
+     * Inserts new data into collection.
+     * @param array|string $collection collection name.
+     * @param array|object $data data to be inserted.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return \MongoDB\BSON\ObjectID new record id instance.
+     */
+    public function insert($collection, $data, $options = [])
+    {
+        $this->beginProfile($token = "    > insert into " . $this->composeCollectionLogName($collection) . ") ...");
+        $id = $this->db->getCollection($collection)->insert($data, $options);
+        $this->endProfile($token);
+        return $id;
+    }
+
+    /**
+     * Inserts several new rows into collection.
+     * @param array|string $collection collection name.
+     * @param array $rows array of arrays or objects to be inserted.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array inserted data, each row will have "_id" key assigned to it.
+     */
+    public function batchInsert($collection, $rows, $options = [])
+    {
+        $this->beginProfile($token = "    > insert into " . $this->composeCollectionLogName($collection) . ") ...");
+        $rows = $this->db->getCollection($collection)->batchInsert($rows, $options);
+        $this->endProfile($token);
+        return $rows;
+    }
+
+    /**
+     * Updates the rows, which matches given criteria by given data.
+     * Note: for "multiple" mode Mongo requires explicit strategy "$set" or "$inc"
+     * to be specified for the "newData". If no strategy is passed "$set" will be used.
+     * @param array|string $collection collection name.
+     * @param array $condition description of the objects to update.
+     * @param array $newData the object with which to update the matching records.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int|bool number of updated documents or whether operation was successful.
+     */
+    public function update($collection, $condition, $newData, $options = [])
+    {
+        $this->beginProfile($token = "    > update " . $this->composeCollectionLogName($collection) . ") ...");
+        $result = $this->db->getCollection($collection)->update($condition, $newData, $options);
+        $this->endProfile($token);
+        return $result;
+    }
+
+    /**
+     * Update the existing database data, otherwise insert this data
+     * @param array|string $collection collection name.
+     * @param array|object $data data to be updated/inserted.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return \MongoDB\BSON\ObjectID updated/new record id instance.
+     */
+    public function save($collection, $data, $options = [])
+    {
+        $this->beginProfile($token = "    > save " . $this->composeCollectionLogName($collection) . ") ...");
+        $id = $this->db->getCollection($collection)->save($data, $options);
+        $this->endProfile($token);
+        return $id;
+    }
+
+    /**
+     * Removes data from the collection.
+     * @param array|string $collection collection name.
+     * @param array $condition description of records to remove.
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return int|bool number of updated documents or whether operation was successful.
+     */
+    public function remove($collection, $condition = [], $options = [])
+    {
+        $this->beginProfile($token = "    > remove " . $this->composeCollectionLogName($collection) . ") ...");
+        $result = $this->db->getCollection($collection)->remove($condition, $options);
+        $this->endProfile($token);
+        return $result;
+    }
+
+    /**
+     * Composes string representing collection name.
+     * @param array|string $collection collection name.
+     * @return string collection name.
+     */
+    protected function composeCollectionLogName($collection)
+    {
+        if (is_array($collection)) {
+            list($database, $collection) = $collection;
+            return $database . '.' . $collection;
+        }
+        return $collection;
+    }
+
+    /**
+     * @var array opened profile tokens.
+     * @since 2.1.1
+     */
+    private $profileTokens = [];
+
+    /**
+     * Logs the incoming message.
+     * By default this method sends message to 'stdout'.
+     * @param string $string message to be logged.
+     * @since 2.1.1
+     */
+    protected function log($string)
+    {
+        echo $string;
+    }
+
+    /**
+     * Marks the beginning of a code block for profiling.
+     * @param string $token token for the code block.
+     * @since 2.1.1
+     */
+    protected function beginProfile($token)
+    {
+        $this->profileTokens[$token] = microtime(true);
+
+        if (!$this->compact) {
+            $this->log($token);
+        }
+    }
+
+    /**
+     * Marks the end of a code block for profiling.
+     * @param string $token token for the code block.
+     * @since 2.1.1
+     */
+    protected function endProfile($token)
+    {
+        if (isset($this->profileTokens[$token])) {
+            $time = microtime(true) - $this->profileTokens[$token];
+            unset($this->profileTokens[$token]);
+        } else {
+            $time = 0;
+        }
+
+        if (!$this->compact) {
+            $this->log(" done (time: " . sprintf('%.3f', $time) . "s)\n");
+        }
+    }
+}

+ 635 - 0
vendor/yiisoft/yii2-mongodb/src/Query.php

@@ -0,0 +1,635 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use yii\base\Component;
+use yii\db\QueryInterface;
+use yii\db\QueryTrait;
+use Yii;
+use yii\helpers\ArrayHelper;
+
+/**
+ * Query represents Mongo "find" operation.
+ *
+ * Query provides a set of methods to facilitate the specification of "find" command.
+ * These methods can be chained together.
+ *
+ * For example,
+ *
+ * ```php
+ * $query = new Query();
+ * // compose the query
+ * $query->select(['name', 'status'])
+ *     ->from('customer')
+ *     ->limit(10);
+ * // execute the query
+ * $rows = $query->all();
+ * ```
+ *
+ * @property Collection $collection Collection instance. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Query extends Component implements QueryInterface
+{
+    use QueryTrait;
+
+    /**
+     * @var array the fields of the results to return. For example: `['name', 'group_id']`, `['name' => true, '_id' => false]`.
+     * Unless directly excluded, the "_id" field is always returned. If not set, it means selecting all columns.
+     * @see select()
+     */
+    public $select = [];
+    /**
+     * @var string|array the collection to be selected from. If string considered as the name of the collection
+     * inside the default database. If array - first element considered as the name of the database,
+     * second - as name of collection inside that database
+     * @see from()
+     */
+    public $from;
+    /**
+     * @var array cursor options in format: optionKey => optionValue
+     * @see \MongoDB\Driver\Cursor::addOption()
+     * @see options()
+     */
+    public $options = [];
+
+
+    /**
+     * Returns the Mongo collection for this query.
+     * @param Connection $db Mongo connection.
+     * @return Collection collection instance.
+     */
+    public function getCollection($db = null)
+    {
+        if ($db === null) {
+            $db = Yii::$app->get('mongodb');
+        }
+
+        return $db->getCollection($this->from);
+    }
+
+    /**
+     * Sets the list of fields of the results to return.
+     * @param array $fields fields of the results to return.
+     * @return $this the query object itself.
+     */
+    public function select(array $fields)
+    {
+        $this->select = $fields;
+
+        return $this;
+    }
+
+    /**
+     * Sets the collection to be selected from.
+     * @param string|array the collection to be selected from. If string considered as the name of the collection
+     * inside the default database. If array - first element considered as the name of the database,
+     * second - as name of collection inside that database
+     * @return $this the query object itself.
+     */
+    public function from($collection)
+    {
+        $this->from = $collection;
+
+        return $this;
+    }
+
+    /**
+     * Sets the cursor options.
+     * @param array $options cursor options in format: optionName => optionValue
+     * @return $this the query object itself
+     * @see addOptions()
+     */
+    public function options($options)
+    {
+        $this->options = $options;
+
+        return $this;
+    }
+
+    /**
+     * Adds additional cursor options.
+     * @param array $options cursor options in format: optionName => optionValue
+     * @return $this the query object itself
+     * @see options()
+     */
+    public function addOptions($options)
+    {
+        if (is_array($this->options)) {
+            $this->options = array_merge($this->options, $options);
+        } else {
+            $this->options = $options;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Helper method for easy querying on values containing some common operators.
+     *
+     * The comparison operator is intelligently determined based on the first few characters in the given value and
+     * internally translated to a MongoDB operator.
+     * In particular, it recognizes the following operators if they appear as the leading characters in the given value:
+     * <: the column must be less than the given value ($lt).
+     * >: the column must be greater than the given value ($gt).
+     * <=: the column must be less than or equal to the given value ($lte).
+     * >=: the column must be greater than or equal to the given value ($gte).
+     * <>: the column must not be the same as the given value ($ne). Note that when $partialMatch is true, this would mean the value must not be a substring of the column.
+     * =: the column must be equal to the given value ($eq).
+     * none of the above: use the $defaultOperator
+     *
+     * Note that when the value is empty, no comparison expression will be added to the search condition.
+     *
+     * @param string $name column name
+     * @param string $value column value
+     * @param string $defaultOperator Defaults to =, performing an exact match.
+     * For example: use 'LIKE' or 'REGEX' for partial cq regex matching
+     * @see Collection::buildCondition()
+     * @return $this the query object itself.
+     * @since 2.0.5
+     */
+    public function andFilterCompare($name, $value, $defaultOperator = '=')
+    {
+        $matches = [];
+        if (preg_match('/^(<>|>=|>|<=|<|=)/', $value, $matches)) {
+            $op = $matches[1];
+            $value = substr($value, strlen($op));
+        } else {
+            $op = $defaultOperator;
+        }
+
+        return $this->andFilterWhere([$op, $name, $value]);
+    }
+
+    /**
+     * Prepares for query building.
+     * This method is called before actual query composition, e.g. building cursor, count etc.
+     * You may override this method to do some final preparation work before query execution.
+     * @return $this a prepared query instance.
+     * @since 2.1.3
+     */
+    public function prepare()
+    {
+        return $this;
+    }
+
+    /**
+     * Builds the MongoDB cursor for this query.
+     * @param Connection $db the MongoDB connection used to execute the query.
+     * @return \MongoDB\Driver\Cursor mongo cursor instance.
+     */
+    public function buildCursor($db = null)
+    {
+        $this->prepare();
+
+        $options = $this->options;
+        if (!empty($this->orderBy)) {
+            $options['sort'] = $this->orderBy;
+        }
+        $options['limit'] = $this->limit;
+        $options['skip'] = $this->offset;
+
+        $cursor = $this->getCollection($db)->find($this->composeCondition(), $this->select, $options);
+
+        return $cursor;
+    }
+
+    /**
+     * Fetches rows from the given Mongo cursor.
+     * @param \MongoDB\Driver\Cursor $cursor Mongo cursor instance to fetch data from.
+     * @param bool $all whether to fetch all rows or only first one.
+     * @param string|callable $indexBy the column name or PHP callback,
+     * by which the query results should be indexed by.
+     * @throws Exception on failure.
+     * @return array|bool result.
+     */
+    protected function fetchRows($cursor, $all = true, $indexBy = null)
+    {
+        $token = 'fetch cursor id = ' . $cursor->getId();
+        Yii::info($token, __METHOD__);
+        try {
+            Yii::beginProfile($token, __METHOD__);
+            $result = $this->fetchRowsInternal($cursor, $all);
+            Yii::endProfile($token, __METHOD__);
+
+            return $result;
+        } catch (\Exception $e) {
+            Yii::endProfile($token, __METHOD__);
+            throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
+        }
+    }
+
+    /**
+     * @param \MongoDB\Driver\Cursor $cursor Mongo cursor instance to fetch data from.
+     * @param bool $all whether to fetch all rows or only first one.
+     * @return array|bool result.
+     * @see Query::fetchRows()
+     */
+    protected function fetchRowsInternal($cursor, $all)
+    {
+        $result = [];
+        if ($all) {
+            foreach ($cursor as $row) {
+                $result[] = $row;
+            }
+        } else {
+            if ($row = current($cursor->toArray())) {
+                $result = $row;
+            } else {
+                $result = false;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Starts a batch query.
+     *
+     * A batch query supports fetching data in batches, which can keep the memory usage under a limit.
+     * This method will return a [[BatchQueryResult]] object which implements the `Iterator` interface
+     * and can be traversed to retrieve the data in batches.
+     *
+     * For example,
+     *
+     * ```php
+     * $query = (new Query)->from('user');
+     * foreach ($query->batch() as $rows) {
+     *     // $rows is an array of 10 or fewer rows from user collection
+     * }
+     * ```
+     *
+     * @param int $batchSize the number of records to be fetched in each batch.
+     * @param Connection $db the MongoDB connection. If not set, the "mongodb" application component will be used.
+     * @return BatchQueryResult the batch query result. It implements the `Iterator` interface
+     * and can be traversed to retrieve the data in batches.
+     * @since 2.1
+     */
+    public function batch($batchSize = 100, $db = null)
+    {
+        return Yii::createObject([
+            'class' => BatchQueryResult::className(),
+            'query' => $this,
+            'batchSize' => $batchSize,
+            'db' => $db,
+            'each' => false,
+        ]);
+    }
+
+    /**
+     * Starts a batch query and retrieves data row by row.
+     * This method is similar to [[batch()]] except that in each iteration of the result,
+     * only one row of data is returned. For example,
+     *
+     * ```php
+     * $query = (new Query)->from('user');
+     * foreach ($query->each() as $row) {
+     * }
+     * ```
+     *
+     * @param int $batchSize the number of records to be fetched in each batch.
+     * @param Connection $db the MongoDB connection. If not set, the "mongodb" application component will be used.
+     * @return BatchQueryResult the batch query result. It implements the `Iterator` interface
+     * and can be traversed to retrieve the data in batches.
+     * @since 2.1
+     */
+    public function each($batchSize = 100, $db = null)
+    {
+        return Yii::createObject([
+            'class' => BatchQueryResult::className(),
+            'query' => $this,
+            'batchSize' => $batchSize,
+            'db' => $db,
+            'each' => true,
+        ]);
+    }
+
+    /**
+     * Executes the query and returns all results as an array.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return array the query results. If the query results in nothing, an empty array will be returned.
+     */
+    public function all($db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return [];
+        }
+        $cursor = $this->buildCursor($db);
+        $rows = $this->fetchRows($cursor, true, $this->indexBy);
+        return $this->populate($rows);
+    }
+
+    /**
+     * Converts the raw query results into the format as specified by this query.
+     * This method is internally used to convert the data fetched from database
+     * into the format as required by this query.
+     * @param array $rows the raw query result from database
+     * @return array the converted query result
+     */
+    public function populate($rows)
+    {
+        if ($this->indexBy === null) {
+            return $rows;
+        }
+        $result = [];
+        foreach ($rows as $row) {
+            $result[ArrayHelper::getValue($row, $this->indexBy)] = $row;
+        }
+        return $result;
+    }
+
+    /**
+     * Executes the query and returns a single row of result.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return array|false the first row (in terms of an array) of the query result. `false` is returned if the query
+     * results in nothing.
+     */
+    public function one($db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return false;
+        }
+        $cursor = $this->buildCursor($db);
+        return $this->fetchRows($cursor, false);
+    }
+
+    /**
+     * Returns the query result as a scalar value.
+     * The value returned will be the first column in the first row of the query results.
+     * Column `_id` will be automatically excluded from select fields, if [[select]] is not empty and
+     * `_id` is not selected explicitly.
+     * @param Connection $db the MongoDB connection used to generate the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return string|null|false the value of the first column in the first row of the query result.
+     * `false` is returned if the query result is empty.
+     * @since 2.1.2
+     */
+    public function scalar($db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return null;
+        }
+
+        $originSelect = (array)$this->select;
+        if (!isset($originSelect['_id']) && array_search('_id', $originSelect, true) === false) {
+            $this->select['_id'] = false;
+        }
+
+        $cursor = $this->buildCursor($db);
+        $row = $this->fetchRows($cursor, false);
+
+        if (empty($row)) {
+            return false;
+        }
+
+        return reset($row);
+    }
+
+    /**
+     * Executes the query and returns the first column of the result.
+     * Column `_id` will be automatically excluded from select fields, if [[select]] is not empty and
+     * `_id` is not selected explicitly.
+     * @param Connection $db the MongoDB connection used to generate the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return array the first column of the query result. An empty array is returned if the query results in nothing.
+     * @since 2.1.2
+     */
+    public function column($db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return [];
+        }
+
+        $originSelect = (array)$this->select;
+        if (!isset($originSelect['_id']) && array_search('_id', $originSelect, true) === false) {
+            $this->select['_id'] = false;
+        }
+        if (is_string($this->indexBy) && $originSelect && count($originSelect) === 1) {
+            $this->select[] = $this->indexBy;
+        }
+
+        $cursor = $this->buildCursor($db);
+        $rows = $this->fetchRows($cursor, true);
+
+        if (empty($rows)) {
+            return [];
+        }
+
+        $results = [];
+        foreach ($rows as $row) {
+            $value = reset($row);
+
+            if ($this->indexBy === null) {
+                $results[] = $value;
+            } else {
+                if ($this->indexBy instanceof \Closure) {
+                    $results[call_user_func($this->indexBy, $row)] = $value;
+                } else {
+                    $results[$row[$this->indexBy]] = $value;
+                }
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Performs 'findAndModify' query and returns a single row of result.
+     * @param array $update update criteria
+     * @param array $options list of options in format: optionName => optionValue.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * @return array|null the original document, or the modified document when $options['new'] is set.
+     */
+    public function modify($update, $options = [], $db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return null;
+        }
+
+        $this->prepare();
+
+        $collection = $this->getCollection($db);
+        if (!empty($this->orderBy)) {
+            $options['sort'] = $this->orderBy;
+        }
+
+        $options['fields'] = $this->select;
+        return $collection->findAndModify($this->composeCondition(), $update, $options);
+    }
+
+    /**
+     * Returns the number of records.
+     * @param string $q kept to match [[QueryInterface]], its value is ignored.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return int number of records
+     * @throws Exception on failure.
+     */
+    public function count($q = '*', $db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return 0;
+        }
+        $this->prepare();
+        $collection = $this->getCollection($db);
+        return $collection->count($this->where, $this->options);
+    }
+
+    /**
+     * Returns a value indicating whether the query result contains any row of data.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return bool whether the query result contains any row of data.
+     */
+    public function exists($db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return false;
+        }
+        $cursor = $this->buildCursor($db);
+        foreach ($cursor as $row) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the sum of the specified column values.
+     * @param string $q the column name.
+     * Make sure you properly quote column names in the expression.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return int the sum of the specified column values
+     */
+    public function sum($q, $db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return 0;
+        }
+        return $this->aggregate($q, 'sum', $db);
+    }
+
+    /**
+     * Returns the average of the specified column values.
+     * @param string $q the column name.
+     * Make sure you properly quote column names in the expression.
+     * @param Connection $db the Mongo connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return int the average of the specified column values.
+     */
+    public function average($q, $db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return 0;
+        }
+        return $this->aggregate($q, 'avg', $db);
+    }
+
+    /**
+     * Returns the minimum of the specified column values.
+     * @param string $q the column name.
+     * Make sure you properly quote column names in the expression.
+     * @param Connection $db the MongoDB connection used to execute the query.
+     * If this parameter is not given, the `db` application component will be used.
+     * @return int the minimum of the specified column values.
+     */
+    public function min($q, $db = null)
+    {
+        return $this->aggregate($q, 'min', $db);
+    }
+
+    /**
+     * Returns the maximum of the specified column values.
+     * @param string $q the column name.
+     * Make sure you properly quote column names in the expression.
+     * @param Connection $db the MongoDB connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return int the maximum of the specified column values.
+     */
+    public function max($q, $db = null)
+    {
+        return $this->aggregate($q, 'max', $db);
+    }
+
+    /**
+     * Performs the aggregation for the given column.
+     * @param string $column column name.
+     * @param string $operator aggregation operator.
+     * @param Connection $db the database connection used to execute the query.
+     * @return int aggregation result.
+     */
+    protected function aggregate($column, $operator, $db)
+    {
+        if (!empty($this->emulateExecution)) {
+            return null;
+        }
+
+        $this->prepare();
+        $collection = $this->getCollection($db);
+        $pipelines = [];
+        if ($this->where !== null) {
+            $pipelines[] = ['$match' => $this->where];
+        }
+        $pipelines[] = [
+            '$group' => [
+                '_id' => '1',
+                'total' => [
+                    '$' . $operator => '$' . $column
+                ],
+            ]
+        ];
+        $result = $collection->aggregate($pipelines);
+        if (array_key_exists(0, $result)) {
+            return $result[0]['total'];
+        }
+        return null;
+    }
+
+    /**
+     * Returns a list of distinct values for the given column across a collection.
+     * @param string $q column to use.
+     * @param Connection $db the MongoDB connection used to execute the query.
+     * If this parameter is not given, the `mongodb` application component will be used.
+     * @return array array of distinct values
+     */
+    public function distinct($q, $db = null)
+    {
+        if (!empty($this->emulateExecution)) {
+            return [];
+        }
+
+        $this->prepare();
+        $collection = $this->getCollection($db);
+        if ($this->where !== null) {
+            $condition = $this->where;
+        } else {
+            $condition = [];
+        }
+        $result = $collection->distinct($q, $condition);
+        if ($result === false) {
+            return [];
+        }
+        return $result;
+    }
+
+    /**
+     * Composes condition from raw [[where]] value.
+     * @return array conditions.
+     */
+    private function composeCondition()
+    {
+        if ($this->where === null) {
+            return [];
+        }
+        return $this->where;
+    }
+}

+ 916 - 0
vendor/yiisoft/yii2-mongodb/src/QueryBuilder.php

@@ -0,0 +1,916 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use MongoDB\BSON\Javascript;
+use MongoDB\BSON\ObjectID;
+use MongoDB\BSON\Regex;
+use MongoDB\Driver\Exception\InvalidArgumentException;
+use yii\base\InvalidParamException;
+use yii\base\BaseObject;
+use yii\helpers\ArrayHelper;
+
+/**
+ * QueryBuilder builds a MongoDB command statements.
+ * It is used by [[Command]] for particular commands and queries composition.
+ *
+ * MongoDB uses JSON format to specify query conditions with quite specific syntax.
+ * However [[buildCondition()]] method provides the ability of "translating" common condition format used "yii\db\*"
+ * into MongoDB condition.
+ * For example:
+ *
+ * ```php
+ * $condition = [
+ *     [
+ *         'OR',
+ *         ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']],
+ *         ['status' => [1, 2, 3]]
+ *     ],
+ * ];
+ * print_r(Yii::$app->mongodb->getQueryBuilder()->buildCondition($condition));
+ * // outputs :
+ * [
+ *     '$or' => [
+ *         [
+ *             'first_name' => 'John',
+ *             'last_name' => 'John',
+ *         ],
+ *         [
+ *             'status' => ['$in' => [1, 2, 3]],
+ *         ]
+ *     ]
+ * ]
+ * ```
+ *
+ * Note: condition values for the key '_id' will be automatically cast to [[\MongoDB\BSON\ObjectID]] instance,
+ * even if they are plain strings. However, if you have other columns, containing [[\MongoDB\BSON\ObjectID]], you
+ * should take care of possible typecast on your own.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class QueryBuilder extends BaseObject
+{
+    /**
+     * @var Connection the MongoDB connection.
+     */
+    public $db;
+
+
+    /**
+     * Constructor.
+     * @param Connection $connection the database connection.
+     * @param array $config name-value pairs that will be used to initialize the object properties
+     */
+    public function __construct($connection, $config = [])
+    {
+        $this->db = $connection;
+        parent::__construct($config);
+    }
+
+    // Commands :
+
+    /**
+     * Generates 'create collection' command.
+     * https://docs.mongodb.com/manual/reference/method/db.createCollection/
+     * @param string $collectionName collection name.
+     * @param array $options collection options in format: "name" => "value"
+     * @return array command document.
+     */
+    public function createCollection($collectionName, array $options = [])
+    {
+        $document = array_merge(['create' => $collectionName], $options);
+
+        if (isset($document['indexOptionDefaults'])) {
+            $document['indexOptionDefaults'] = (object) $document['indexOptionDefaults'];
+        }
+        if (isset($document['storageEngine'])) {
+            $document['storageEngine'] = (object) $document['storageEngine'];
+        }
+        if (isset($document['validator'])) {
+            $document['validator'] = (object) $document['validator'];
+        }
+
+        return $document;
+    }
+
+    /**
+     * Generates drop database command.
+     * https://docs.mongodb.com/manual/reference/method/db.dropDatabase/
+     * @return array command document.
+     */
+    public function dropDatabase()
+    {
+        return ['dropDatabase' => 1];
+    }
+
+    /**
+     * Generates drop collection command.
+     * https://docs.mongodb.com/manual/reference/method/db.collection.drop/
+     * @param string $collectionName name of the collection to be dropped.
+     * @return array command document.
+     */
+    public function dropCollection($collectionName)
+    {
+        return ['drop' => $collectionName];
+    }
+
+    /**
+     * Generates create indexes command.
+     * @see https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/
+     * @param string|null $databaseName database name.
+     * @param string $collectionName collection name.
+     * @param array[] $indexes indexes specification. Each specification should be an array in format: optionName => value
+     * The main options are:
+     *
+     * - keys: array, column names with sort order, to be indexed. This option is mandatory.
+     * - unique: bool, whether to create unique index.
+     * - name: string, the name of the index, if not set it will be generated automatically.
+     * - background: bool, whether to bind index in the background.
+     * - sparse: bool, whether index should reference only documents with the specified field.
+     *
+     * See [[https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options-for-all-index-types]]
+     * for the full list of options.
+     * @return array command document.
+     */
+    public function createIndexes($databaseName, $collectionName, $indexes)
+    {
+        $normalizedIndexes = [];
+
+        foreach ($indexes as $index) {
+            if (!isset($index['key'])) {
+                throw new InvalidParamException('"key" is required for index specification');
+            }
+
+            $index['key'] = $this->buildSortFields($index['key']);
+
+            if (!isset($index['ns'])) {
+                if ($databaseName === null) {
+                    $databaseName = $this->db->getDefaultDatabaseName();
+                }
+                $index['ns'] = $databaseName . '.' . $collectionName;
+            }
+
+            if (!isset($index['name'])) {
+                $index['name'] = $this->generateIndexName($index['key']);
+            }
+
+            $normalizedIndexes[] = $index;
+        }
+
+        return [
+            'createIndexes' => $collectionName,
+            'indexes' => $normalizedIndexes,
+        ];
+    }
+
+    /**
+     * Generates index name for the given column orders.
+     * Columns should be normalized using [[buildSortFields()]] before being passed to this method.
+     * @param array $columns columns with sort order.
+     * @return string index name.
+     */
+    public function generateIndexName($columns)
+    {
+        $parts = [];
+        foreach ($columns as $column => $order) {
+            $parts[] = $column . '_' . $order;
+        }
+        return implode('_', $parts);
+    }
+
+    /**
+     * Generates drop indexes command.
+     * @param string $collectionName collection name
+     * @param string $index index name or pattern, use `*` in order to drop all indexes.
+     * @return array command document.
+     */
+    public function dropIndexes($collectionName, $index)
+    {
+        return [
+            'dropIndexes' => $collectionName,
+            'index' => $index,
+        ];
+    }
+
+    /**
+     * Generates list indexes command.
+     * @param string $collectionName collection name
+     * @param array $options command options.
+     * Available options are:
+     *
+     * - maxTimeMS: int, max execution time in ms.
+     *
+     * @return array command document.
+     */
+    public function listIndexes($collectionName, $options = [])
+    {
+        return array_merge(['listIndexes' => $collectionName], $options);
+    }
+
+    /**
+     * Generates count command
+     * @param string $collectionName
+     * @param array $condition
+     * @param array $options
+     * @return array command document.
+     */
+    public function count($collectionName, $condition = [], $options = [])
+    {
+        $document = ['count' => $collectionName];
+
+        if (!empty($condition)) {
+            $document['query'] = (object) $this->buildCondition($condition);
+        }
+
+        return array_merge($document, $options);
+    }
+
+    /**
+     * Generates 'find and modify' command.
+     * @param string $collectionName collection name
+     * @param array $condition filter condition
+     * @param array $update update criteria
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array command document.
+     */
+    public function findAndModify($collectionName, $condition = [], $update = [], $options = [])
+    {
+        $document = array_merge(['findAndModify' => $collectionName], $options);
+
+        if (!empty($condition)) {
+            $options['query'] = $this->buildCondition($condition);
+        }
+
+        if (!empty($update)) {
+            $options['update'] = $update;
+        }
+
+        if (isset($options['fields'])) {
+            $options['fields'] = $this->buildSelectFields($options['fields']);
+        }
+
+        if (isset($options['sort'])) {
+            $options['sort'] = $this->buildSortFields($options['sort']);
+        }
+
+        foreach (['fields', 'query', 'sort', 'update'] as $name) {
+            if (isset($options[$name])) {
+                $document[$name] = (object) $options[$name];
+            }
+        }
+
+        return $document;
+    }
+
+    /**
+     * Generates 'distinct' command.
+     * @param string $collectionName collection name.
+     * @param string $fieldName target field name.
+     * @param array $condition filter condition
+     * @param array $options list of options in format: optionName => optionValue.
+     * @return array command document.
+     */
+    public function distinct($collectionName, $fieldName, $condition = [], $options = [])
+    {
+        $document = array_merge(
+            [
+                'distinct' => $collectionName,
+                'key' => $fieldName,
+            ],
+            $options
+        );
+
+        if (!empty($condition)) {
+            $document['query'] = $this->buildCondition($condition);
+        }
+
+        return $document;
+    }
+
+    /**
+     * Generates 'group' command.
+     * @param string $collectionName
+     * @@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. If instance of [[Javascript]] passed,
+     * it will be treated as a function that returns the key to group by.
+     * @param array $initial Initial value of the aggregation counter object.
+     * @param Javascript|string $reduce function that takes two arguments (the current
+     * document and the aggregation to this point) and does the aggregation.
+     * Argument will be automatically cast to [[Javascript]].
+     * @param array $options optional parameters to the group command. Valid options include:
+     *  - condition - criteria for including a document in the aggregation.
+     *  - finalize - function called once per unique key that takes the final output of the reduce function.
+     * @return array command document.
+     */
+    public function group($collectionName, $keys, $initial, $reduce, $options = [])
+    {
+        if (!($reduce instanceof Javascript)) {
+            $reduce = new Javascript((string) $reduce);
+        }
+
+        if (isset($options['condition'])) {
+            $options['cond'] = $this->buildCondition($options['condition']);
+            unset($options['condition']);
+        }
+
+        if (isset($options['finalize'])) {
+            if (!($options['finalize'] instanceof Javascript)) {
+                $options['finalize'] = new Javascript((string) $options['finalize']);
+            }
+        }
+
+        if (isset($options['keyf'])) {
+            $options['$keyf'] = $options['keyf'];
+            unset($options['keyf']);
+        }
+        if (isset($options['$keyf'])) {
+            if (!($options['$keyf'] instanceof Javascript)) {
+                $options['$keyf'] = new Javascript((string) $options['$keyf']);
+            }
+        }
+
+        $document = [
+            'group' => array_merge(
+                [
+                    'ns' => $collectionName,
+                    'key' => $keys,
+                    'initial' => $initial,
+                    '$reduce' => $reduce,
+                ],
+                $options
+            )
+        ];
+
+        return $document;
+    }
+
+    /**
+     * Generates 'map-reduce' command.
+     * @see https://docs.mongodb.com/manual/core/map-reduce/
+     * @param string $collectionName collection name.
+     * @param \MongoDB\BSON\Javascript|string $map function, which emits map data from collection.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the map key
+     * and the map values) and does the aggregation.
+     * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
+     * @param string|array $out output collection name. It could be a string for simple output
+     * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']).
+     * You can pass ['inline' => true] to fetch the result at once without temporary collection usage.
+     * @param array $condition filter condition for including a document in the aggregation.
+     * @param array $options additional optional parameters to the mapReduce command. Valid options include:
+     *
+     *  - sort: array, key to sort the input documents. The sort key must be in an existing index for this collection.
+     *  - limit: int, the maximum number of documents to return in the collection.
+     *  - finalize: \MongoDB\BSON\Javascript|string, function, which follows the reduce method and modifies the output.
+     *  - scope: array, specifies global variables that are accessible in the map, reduce and finalize functions.
+     *  - jsMode: bool, specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
+     *  - verbose: bool, specifies whether to include the timing information in the result information.
+     *
+     * @return array command document.
+     */
+    public function mapReduce($collectionName, $map, $reduce, $out, $condition = [], $options = [])
+    {
+        if (!($map instanceof Javascript)) {
+            $map = new Javascript((string) $map);
+        }
+        if (!($reduce instanceof Javascript)) {
+            $reduce = new Javascript((string) $reduce);
+        }
+
+        $document = [
+            'mapReduce' => $collectionName,
+            'map' => $map,
+            'reduce' => $reduce,
+            'out' => $out
+        ];
+
+        if (!empty($condition)) {
+            $document['query'] = $this->buildCondition($condition);
+        }
+
+        if (!empty($options)) {
+            $document = array_merge($document, $options);
+        }
+
+        return $document;
+    }
+
+    /**
+     * Generates 'aggregate' command.
+     * @param string $collectionName collection name
+     * @param array $pipelines list of pipeline operators.
+     * @param array $options optional parameters.
+     * @return array command document.
+     */
+    public function aggregate($collectionName, $pipelines, $options = [])
+    {
+        foreach ($pipelines as $key => $pipeline) {
+            if (isset($pipeline['$match'])) {
+                $pipelines[$key]['$match'] = $this->buildCondition($pipeline['$match']);
+            }
+        }
+
+        $document = array_merge(
+            [
+                'aggregate' => $collectionName,
+                'pipeline' => $pipelines,
+                'allowDiskUse' => false,
+            ],
+            $options
+        );
+
+        return $document;
+    }
+
+    /**
+     * Generates 'explain' command.
+     * @param string $collectionName collection name.
+     * @param array $query query options.
+     * @return array command document.
+     */
+    public function explain($collectionName, $query)
+    {
+        $query = array_merge(
+            ['find' => $collectionName],
+            $query
+        );
+
+        if (isset($query['filter'])) {
+            $query['filter'] = (object) $this->buildCondition($query['filter']);
+        }
+        if (isset($query['projection'])) {
+            $query['projection'] = $this->buildSelectFields($query['projection']);
+        }
+        if (isset($query['sort'])) {
+            $query['sort'] = $this->buildSortFields($query['sort']);
+        }
+
+        return [
+            'explain' => $query,
+        ];
+    }
+
+    /**
+     * Generates 'listDatabases' command.
+     * @param array $condition filter condition.
+     * @param array $options command options.
+     * @return array command document.
+     */
+    public function listDatabases($condition = [], $options = [])
+    {
+        $document = array_merge(['listDatabases' => 1], $options);
+        if (!empty($condition)) {
+            $document['filter'] = (object)$this->buildCondition($condition);
+        }
+        return $document;
+    }
+
+    /**
+     * Generates 'listCollections' command.
+     * @param array $condition filter condition.
+     * @param array $options command options.
+     * @return array command document.
+     */
+    public function listCollections($condition = [], $options = [])
+    {
+        $document = array_merge(['listCollections' => 1], $options);
+        if (!empty($condition)) {
+            $document['filter'] = (object)$this->buildCondition($condition);
+        }
+        return $document;
+    }
+
+    // Service :
+
+    /**
+     * Normalizes fields list for the MongoDB select composition.
+     * @param array|string $fields raw fields.
+     * @return array normalized select fields.
+     */
+    public function buildSelectFields($fields)
+    {
+        $selectFields = [];
+        foreach ((array)$fields as $key => $value) {
+            if (is_int($key)) {
+                $selectFields[$value] = true;
+            } else {
+                $selectFields[$key] = is_scalar($value) ? (bool)$value : $value;
+            }
+        }
+        return $selectFields;
+    }
+
+    /**
+     * Normalizes fields list for the MongoDB sort composition.
+     * @param array|string $fields raw fields.
+     * @return array normalized sort fields.
+     */
+    public function buildSortFields($fields)
+    {
+        $sortFields = [];
+        foreach ((array)$fields as $key => $value) {
+            if (is_int($key)) {
+                $sortFields[$value] = +1;
+            } else {
+                if ($value === SORT_ASC) {
+                    $value = +1;
+                } elseif ($value === SORT_DESC) {
+                    $value = -1;
+                }
+                $sortFields[$key] = $value;
+            }
+        }
+        return $sortFields;
+    }
+
+    /**
+     * Converts "\yii\db\*" quick condition keyword into actual Mongo condition keyword.
+     * @param string $key raw condition key.
+     * @return string actual key.
+     */
+    protected function normalizeConditionKeyword($key)
+    {
+        static $map = [
+            'AND' => '$and',
+            'OR' => '$or',
+            'IN' => '$in',
+            'NOT IN' => '$nin',
+        ];
+        $matchKey = strtoupper($key);
+        if (array_key_exists($matchKey, $map)) {
+            return $map[$matchKey];
+        }
+        return $key;
+    }
+
+    /**
+     * Converts given value into [[ObjectID]] instance.
+     * If array given, each element of it will be processed.
+     * @param mixed $rawId raw id(s).
+     * @return array|ObjectID normalized id(s).
+     */
+    protected function ensureMongoId($rawId)
+    {
+        if (is_array($rawId)) {
+            $result = [];
+            foreach ($rawId as $key => $value) {
+                $result[$key] = $this->ensureMongoId($value);
+            }
+
+            return $result;
+        } elseif (is_object($rawId)) {
+            if ($rawId instanceof ObjectID) {
+                return $rawId;
+            } else {
+                $rawId = (string) $rawId;
+            }
+        }
+        try {
+            $mongoId = new ObjectID($rawId);
+        } catch (InvalidArgumentException $e) {
+            // invalid id format
+            $mongoId = $rawId;
+        }
+
+        return $mongoId;
+    }
+
+    /**
+     * Parses the condition specification and generates the corresponding Mongo condition.
+     * @param array $condition the condition specification. Please refer to [[Query::where()]]
+     * on how to specify a condition.
+     * @return array the generated Mongo condition
+     * @throws InvalidParamException if the condition is in bad format
+     */
+    public function buildCondition($condition)
+    {
+        static $builders = [
+            'NOT' => 'buildNotCondition',
+            'AND' => 'buildAndCondition',
+            'OR' => 'buildOrCondition',
+            'BETWEEN' => 'buildBetweenCondition',
+            'NOT BETWEEN' => 'buildBetweenCondition',
+            'IN' => 'buildInCondition',
+            'NOT IN' => 'buildInCondition',
+            'REGEX' => 'buildRegexCondition',
+            'LIKE' => 'buildLikeCondition',
+        ];
+
+        if (!is_array($condition)) {
+            throw new InvalidParamException('Condition should be an array.');
+        } elseif (empty($condition)) {
+            return [];
+        }
+        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
+            $operator = strtoupper($condition[0]);
+            if (isset($builders[$operator])) {
+                $method = $builders[$operator];
+            } else {
+                $operator = $condition[0];
+                $method = 'buildSimpleCondition';
+            }
+            array_shift($condition);
+            return $this->$method($operator, $condition);
+        }
+        // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
+        return $this->buildHashCondition($condition);
+    }
+
+    /**
+     * Creates a condition based on column-value pairs.
+     * @param array $condition the condition specification.
+     * @return array the generated Mongo condition.
+     */
+    public function buildHashCondition($condition)
+    {
+        $result = [];
+        foreach ($condition as $name => $value) {
+            if (strncmp('$', $name, 1) === 0) {
+                // Native Mongo condition:
+                $result[$name] = $value;
+            } else {
+                if (is_array($value)) {
+                    if (ArrayHelper::isIndexed($value)) {
+                        // Quick IN condition:
+                        $result = array_merge($result, $this->buildInCondition('IN', [$name, $value]));
+                    } else {
+                        // Mongo complex condition:
+                        $result[$name] = $value;
+                    }
+                } else {
+                    // Direct match:
+                    if ($name == '_id') {
+                        $value = $this->ensureMongoId($value);
+                    }
+                    $result[$name] = $value;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Composes `NOT` condition.
+     * @param string $operator the operator to use for connecting the given operands
+     * @param array $operands the Mongo conditions to connect.
+     * @return array the generated Mongo condition.
+     * @throws InvalidParamException if wrong number of operands have been given.
+     */
+    public function buildNotCondition($operator, $operands)
+    {
+        if (count($operands) !== 2) {
+            throw new InvalidParamException("Operator '$operator' requires two operands.");
+        }
+
+        list($name, $value) = $operands;
+
+        $result = [];
+        if (is_array($value)) {
+            $result[$name] = ['$not' => $this->buildCondition($value)];
+        } else {
+            if ($name == '_id') {
+                $value = $this->ensureMongoId($value);
+            }
+            $result[$name] = ['$ne' => $value];
+        }
+
+        return $result;
+    }
+
+    /**
+     * Connects two or more conditions with the `AND` operator.
+     * @param string $operator the operator to use for connecting the given operands
+     * @param array $operands the Mongo conditions to connect.
+     * @return array the generated Mongo condition.
+     */
+    public function buildAndCondition($operator, $operands)
+    {
+        $operator = $this->normalizeConditionKeyword($operator);
+        $parts = [];
+        foreach ($operands as $operand) {
+            $parts[] = $this->buildCondition($operand);
+        }
+
+        return [$operator => $parts];
+    }
+
+    /**
+     * Connects two or more conditions with the `OR` operator.
+     * @param string $operator the operator to use for connecting the given operands
+     * @param array $operands the Mongo conditions to connect.
+     * @return array the generated Mongo condition.
+     */
+    public function buildOrCondition($operator, $operands)
+    {
+        $operator = $this->normalizeConditionKeyword($operator);
+        $parts = [];
+        foreach ($operands as $operand) {
+            $parts[] = $this->buildCondition($operand);
+        }
+
+        return [$operator => $parts];
+    }
+
+    /**
+     * Creates an Mongo condition, which emulates the `BETWEEN` operator.
+     * @param string $operator the operator to use
+     * @param array $operands the first operand is the column name. The second and third operands
+     * describe the interval that column value should be in.
+     * @return array the generated Mongo condition.
+     * @throws InvalidParamException if wrong number of operands have been given.
+     */
+    public function buildBetweenCondition($operator, $operands)
+    {
+        if (!isset($operands[0], $operands[1], $operands[2])) {
+            throw new InvalidParamException("Operator '$operator' requires three operands.");
+        }
+        list($column, $value1, $value2) = $operands;
+
+        if (strncmp('NOT', $operator, 3) === 0) {
+            return [
+                $column => [
+                    '$lt' => $value1,
+                    '$gt' => $value2,
+                ]
+            ];
+        }
+        return [
+            $column => [
+                '$gte' => $value1,
+                '$lte' => $value2,
+            ]
+        ];
+    }
+
+    /**
+     * Creates an Mongo condition with the `IN` operator.
+     * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
+     * @param array $operands the first operand is the column name. If it is an array
+     * a composite IN condition will be generated.
+     * The second operand is an array of values that column value should be among.
+     * @return array the generated Mongo condition.
+     * @throws InvalidParamException if wrong number of operands have been given.
+     */
+    public function buildInCondition($operator, $operands)
+    {
+        if (!isset($operands[0], $operands[1])) {
+            throw new InvalidParamException("Operator '$operator' requires two operands.");
+        }
+
+        list($column, $values) = $operands;
+
+        $values = (array) $values;
+        $operator = $this->normalizeConditionKeyword($operator);
+
+        if (!is_array($column)) {
+            $columns = [$column];
+            $values = [$column => $values];
+        } elseif (count($column) > 1) {
+            return $this->buildCompositeInCondition($operator, $column, $values);
+        } else {
+            $columns = $column;
+            $values = [$column[0] => $values];
+        }
+
+        $result = [];
+        foreach ($columns as $column) {
+            if ($column == '_id') {
+                $inValues = $this->ensureMongoId($values[$column]);
+            } else {
+                $inValues = $values[$column];
+            }
+
+            $inValues = array_values($inValues);
+            if (count($inValues) === 1 && $operator === '$in') {
+                $result[$column] = $inValues[0];
+            } else {
+                $result[$column][$operator] = $inValues;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param string $operator MongoDB the operator to use (`$in` OR `$nin`)
+     * @param array $columns list of compare columns
+     * @param array $values compare values in format: columnName => [values]
+     * @return array the generated Mongo condition.
+     */
+    private function buildCompositeInCondition($operator, $columns, $values)
+    {
+        $result = [];
+
+        $inValues = [];
+        foreach ($values as $columnValues) {
+            foreach ($columnValues as $column => $value) {
+                if ($column == '_id') {
+                    $value = $this->ensureMongoId($value);
+                }
+                $inValues[$column][] = $value;
+            }
+        }
+
+        foreach ($columns as $column) {
+            $columnInValues = array_values($inValues[$column]);
+            if (count($columnInValues) === 1 && $operator === '$in') {
+                $result[$column] = $columnInValues[0];
+            } else {
+                $result[$column][$operator] = $columnInValues;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Creates a Mongo regular expression condition.
+     * @param string $operator the operator to use
+     * @param array $operands the first operand is the column name.
+     * The second operand is a single value that column value should be compared with.
+     * @return array the generated Mongo condition.
+     * @throws InvalidParamException if wrong number of operands have been given.
+     */
+    public function buildRegexCondition($operator, $operands)
+    {
+        if (!isset($operands[0], $operands[1])) {
+            throw new InvalidParamException("Operator '$operator' requires two operands.");
+        }
+        list($column, $value) = $operands;
+        if (!($value instanceof Regex)) {
+            if (preg_match('~\/(.+)\/(.*)~', $value, $matches)) {
+                $value = new Regex($matches[1], $matches[2]);
+            } else {
+                $value = new Regex($value, '');
+            }
+        }
+
+        return [$column => $value];
+    }
+
+    /**
+     * Creates a Mongo condition, which emulates the `LIKE` operator.
+     * @param string $operator the operator to use
+     * @param array $operands the first operand is the column name.
+     * The second operand is a single value that column value should be compared with.
+     * @return array the generated Mongo condition.
+     * @throws InvalidParamException if wrong number of operands have been given.
+     */
+    public function buildLikeCondition($operator, $operands)
+    {
+        if (!isset($operands[0], $operands[1])) {
+            throw new InvalidParamException("Operator '$operator' requires two operands.");
+        }
+        list($column, $value) = $operands;
+        if (!($value instanceof Regex)) {
+            $value = new Regex(preg_quote($value), 'i');
+        }
+
+        return [$column => $value];
+    }
+
+    /**
+     * Creates an Mongo condition like `{$operator:{field:value}}`.
+     * @param string $operator the operator to use. Besides regular MongoDB operators, aliases like `>`, `<=`,
+     * and so on, can be used here.
+     * @param array $operands the first operand is the column name.
+     * The second operand is a single value that column value should be compared with.
+     * @return string the generated Mongo condition.
+     * @throws InvalidParamException if wrong number of operands have been given.
+     */
+    public function buildSimpleCondition($operator, $operands)
+    {
+        if (count($operands) !== 2) {
+            throw new InvalidParamException("Operator '$operator' requires two operands.");
+        }
+
+        list($column, $value) = $operands;
+
+        if (strncmp('$', $operator, 1) !== 0) {
+            static $operatorMap = [
+                '>' => '$gt',
+                '<' => '$lt',
+                '>=' => '$gte',
+                '<=' => '$lte',
+                '!=' => '$ne',
+                '<>' => '$ne',
+                '=' => '$eq',
+                '==' => '$eq',
+            ];
+            if (isset($operatorMap[$operator])) {
+                $operator = $operatorMap[$operator];
+            } else {
+                throw new InvalidParamException("Unsupported operator '{$operator}'");
+            }
+        }
+
+        return [$column => [$operator => $value]];
+    }
+}

+ 184 - 0
vendor/yiisoft/yii2-mongodb/src/Session.php

@@ -0,0 +1,184 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb;
+
+use Yii;
+use yii\base\ErrorHandler;
+use yii\base\InvalidConfigException;
+use yii\di\Instance;
+use yii\web\MultiFieldSession;
+
+/**
+ * Session extends [[\yii\web\Session]] by using MongoDB as session data storage.
+ *
+ * By default, Session stores session data in a collection named 'session' inside the default database.
+ * This collection is better to be pre-created with fields 'id' and 'expire' indexed.
+ * The collection name can be changed by setting [[sessionCollection]].
+ *
+ * The following example shows how you can configure the application to use Session:
+ * Add the following to your application config under `components`:
+ *
+ * ```php
+ * 'session' => [
+ *     'class' => 'yii\mongodb\Session',
+ *     // 'db' => 'mymongodb',
+ *     // 'sessionCollection' => 'my_session',
+ * ]
+ * ```
+ *
+ * Session extends [[MultiFieldSession]], thus it allows saving extra fields into the [[sessionCollection]].
+ * Refer to [[MultiFieldSession]] for more details.
+ *
+ * Tip: you can use MongoDB [TTL index](https://docs.mongodb.com/manual/tutorial/expire-data/) for the session garbage
+ * collection for performance saving, in this case you should set [[Session::gCProbability]] to `0`.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Session extends MultiFieldSession
+{
+    /**
+     * @var Connection|array|string the MongoDB connection object or the application component ID of the MongoDB connection.
+     * After the Session object is created, if you want to change this property, you should only assign it
+     * with a MongoDB connection object.
+     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
+     */
+    public $db = 'mongodb';
+    /**
+     * @var string|array the name of the MongoDB collection that stores the session data.
+     * Please refer to [[Connection::getCollection()]] on how to specify this parameter.
+     * This collection is better to be pre-created with fields 'id' and 'expire' indexed.
+     */
+    public $sessionCollection = 'session';
+
+
+    /**
+     * Initializes the Session component.
+     * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection.
+     * @throws InvalidConfigException if [[db]] is invalid.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->db = Instance::ensure($this->db, Connection::className());
+    }
+
+    /**
+     * Updates the current session ID with a newly generated one.
+     * Please refer to <http://php.net/session_regenerate_id> for more details.
+     * @param bool $deleteOldSession Whether to delete the old associated session file or not.
+     */
+    public function regenerateID($deleteOldSession = false)
+    {
+        $oldID = session_id();
+
+        // if no session is started, there is nothing to regenerate
+        if (empty($oldID)) {
+            return;
+        }
+
+        parent::regenerateID(false);
+        $newID = session_id();
+
+        $collection = $this->db->getCollection($this->sessionCollection);
+        $row = $collection->findOne(['id' => $oldID]);
+        if ($row !== null) {
+            if ($deleteOldSession) {
+                $collection->update(['id' => $oldID], ['id' => $newID]);
+            } else {
+                unset($row['_id']);
+                $row['id'] = $newID;
+                $collection->insert($row);
+            }
+        } else {
+            // shouldn't reach here normally
+            $collection->insert($this->composeFields($newID, ''));
+        }
+    }
+
+    /**
+     * Session read handler.
+     * Do not call this method directly.
+     * @param string $id session ID
+     * @return string the session data
+     */
+    public function readSession($id)
+    {
+        $collection = $this->db->getCollection($this->sessionCollection);
+        $condition = [
+            'id' => $id,
+            'expire' => ['$gt' => time()],
+        ];
+
+        if (isset($this->readCallback)) {
+            $doc = $collection->findOne($condition);
+            return $doc === null ? '' : $this->extractData($doc);
+        }
+
+        $doc = $collection->findOne(
+            $condition,
+            ['data' => 1, '_id' => 0]
+        );
+        return isset($doc['data']) ? $doc['data'] : '';
+    }
+
+    /**
+     * Session write handler.
+     * Do not call this method directly.
+     * @param string $id session ID
+     * @param string $data session data
+     * @return bool whether session write is successful
+     */
+    public function writeSession($id, $data)
+    {
+        // exception must be caught in session write handler
+        // http://us.php.net/manual/en/function.session-set-save-handler.php
+        try {
+            $this->db->getCollection($this->sessionCollection)->update(
+                ['id' => $id],
+                $this->composeFields($id, $data),
+                ['upsert' => true]
+            );
+        } catch (\Exception $e) {
+            Yii::$app->errorHandler->handleException($e);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Session destroy handler.
+     * Do not call this method directly.
+     * @param string $id session ID
+     * @return bool whether session is destroyed successfully
+     */
+    public function destroySession($id)
+    {
+        $this->db->getCollection($this->sessionCollection)->remove(
+            ['id' => $id],
+            ['justOne' => true]
+        );
+
+        return true;
+    }
+
+    /**
+     * Session GC (garbage collection) handler.
+     * Do not call this method directly.
+     * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up.
+     * @return bool whether session is GCed successfully
+     */
+    public function gcSession($maxLifetime)
+    {
+        $this->db->getCollection($this->sessionCollection)
+            ->remove(['expire' => ['$lt' => time()]]);
+
+        return true;
+    }
+}

+ 271 - 0
vendor/yiisoft/yii2-mongodb/src/console/controllers/MigrateController.php

@@ -0,0 +1,271 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\console\controllers;
+
+use Yii;
+use yii\console\controllers\BaseMigrateController;
+use yii\console\Exception;
+use yii\mongodb\Connection;
+use yii\mongodb\Query;
+use yii\helpers\ArrayHelper;
+
+/**
+ * Manages application MongoDB migrations.
+ *
+ * This is an analog of [[\yii\console\controllers\MigrateController]] for MongoDB.
+ *
+ * This command provides support for tracking the migration history, upgrading
+ * or downloading with migrations, and creating new migration skeletons.
+ *
+ * The migration history is stored in a MongoDB collection named
+ * as [[migrationCollection]]. This collection will be automatically created the first time
+ * this command is executed, if it does not exist.
+ *
+ * In order to enable this command you should adjust the configuration of your console application:
+ *
+ * ```php
+ * return [
+ *     // ...
+ *     'controllerMap' => [
+ *         'mongodb-migrate' => 'yii\mongodb\console\controllers\MigrateController'
+ *     ],
+ * ];
+ * ```
+ *
+ * Below are some common usages of this command:
+ *
+ * ```php
+ * # creates a new migration named 'create_user_collection'
+ * yii mongodb-migrate/create create_user_collection
+ *
+ * # applies ALL new migrations
+ * yii mongodb-migrate
+ *
+ * # reverts the last applied migration
+ * yii mongodb-migrate/down
+ * ```
+ *
+ * Since 2.1.2, in case of usage Yii version >= 2.0.10, you can use namespaced migrations. In order to enable this
+ * feature you should configure [[migrationNamespaces]] property for the controller at application configuration:
+ *
+ * ```php
+ * return [
+ *     'controllerMap' => [
+ *         'mongodb-migrate' => [
+ *             'class' => 'yii\mongodb\console\controllers\MigrateController',
+ *             'migrationNamespaces' => [
+ *                 'app\migrations',
+ *                 'some\extension\migrations',
+ *             ],
+ *             //'migrationPath' => null, // allows to disable not namespaced migration completely
+ *         ],
+ *     ],
+ * ];
+ * ```
+ *
+ * @author Klimov Paul <klimov@zfort.com>
+ * @since 2.0
+ */
+class MigrateController extends BaseMigrateController
+{
+    /**
+     * @var string|array the name of the collection for keeping applied migration information.
+     */
+    public $migrationCollection = 'migration';
+    /**
+     * {@inheritdoc}
+     */
+    public $templateFile = '@yii/mongodb/views/migration.php';
+    /**
+     * @var Connection|string the DB connection object or the application
+     * component ID of the DB connection.
+     */
+    public $db = 'mongodb';
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function options($actionID)
+    {
+        return array_merge(
+            parent::options($actionID),
+            ['migrationCollection', 'db'] // global for all actions
+        );
+    }
+
+    /**
+     * This method is invoked right before an action is to be executed (after all possible filters.)
+     * It checks the existence of the [[migrationPath]].
+     * @param \yii\base\Action $action the action to be executed.
+     * @throws Exception if db component isn't configured
+     * @return bool whether the action should continue to be executed.
+     */
+    public function beforeAction($action)
+    {
+        if (parent::beforeAction($action)) {
+            if ($action->id !== 'create') {
+                if (is_string($this->db)) {
+                    $this->db = Yii::$app->get($this->db);
+                }
+                if (!$this->db instanceof Connection) {
+                    throw new Exception("The 'db' option must refer to the application component ID of a MongoDB connection.");
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Creates a new migration instance.
+     * @param string $class the migration class name
+     * @return \yii\mongodb\Migration the migration instance
+     */
+    protected function createMigration($class)
+    {
+        // since Yii 2.0.12 includeMigrationFile() exists, which replaced the code below
+        // remove this construct when composer requirement raises above 2.0.12
+        if (method_exists($this, 'includeMigrationFile')) {
+            $this->includeMigrationFile($class);
+        } else {
+            $class = trim($class, '\\');
+            if (strpos($class, '\\') === false) {
+                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
+                require_once($file);
+            }
+        }
+
+        return new $class(['db' => $this->db, 'compact' => isset($this->compact) ? $this->compact : false]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getMigrationHistory($limit)
+    {
+        $this->ensureBaseMigrationHistory();
+
+        $query = (new Query())
+            ->select(['version', 'apply_time'])
+            ->from($this->migrationCollection)
+            ->orderBy(['apply_time' => SORT_DESC, 'version' => SORT_DESC]);
+
+        if (empty($this->migrationNamespaces)) {
+            $query->limit($limit);
+            $rows = $query->all($this->db);
+            $history = ArrayHelper::map($rows, 'version', 'apply_time');
+            unset($history[self::BASE_MIGRATION]);
+            return $history;
+        }
+
+        $rows = $query->all($this->db);
+
+        $history = [];
+        foreach ($rows as $key => $row) {
+            if ($row['version'] === self::BASE_MIGRATION) {
+                continue;
+            }
+            if (preg_match('/m?(\d{6}_?\d{6})(\D.*)?$/is', $row['version'], $matches)) {
+                $time = str_replace('_', '', $matches[1]);
+                $row['canonicalVersion'] = $time;
+            } else {
+                $row['canonicalVersion'] = $row['version'];
+            }
+            $row['apply_time'] = (int)$row['apply_time'];
+            $history[] = $row;
+        }
+
+        usort($history, function ($a, $b) {
+            if ($a['apply_time'] === $b['apply_time']) {
+                if (($compareResult = strcasecmp($b['canonicalVersion'], $a['canonicalVersion'])) !== 0) {
+                    return $compareResult;
+                }
+                return strcasecmp($b['version'], $a['version']);
+            }
+            return ($a['apply_time'] > $b['apply_time']) ? -1 : +1;
+        });
+
+        $history = array_slice($history, 0, $limit);
+
+        $history = ArrayHelper::map($history, 'version', 'apply_time');
+
+        return $history;
+    }
+
+    private $baseMigrationEnsured = false;
+
+    /**
+     * Ensures migration history contains at least base migration entry.
+     */
+    protected function ensureBaseMigrationHistory()
+    {
+        if (!$this->baseMigrationEnsured) {
+            $query = new Query;
+            $row = $query->select(['version'])
+                ->from($this->migrationCollection)
+                ->andWhere(['version' => self::BASE_MIGRATION])
+                ->limit(1)
+                ->one($this->db);
+            if (empty($row)) {
+                $this->addMigrationHistory(self::BASE_MIGRATION);
+            }
+            $this->baseMigrationEnsured = true;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function addMigrationHistory($version)
+    {
+        $this->db->getCollection($this->migrationCollection)->insert([
+            'version' => $version,
+            'apply_time' => time(),
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function removeMigrationHistory($version)
+    {
+        $this->db->getCollection($this->migrationCollection)->remove([
+            'version' => $version,
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     * @since 2.1.5
+     */
+    protected function truncateDatabase()
+    {
+        $collections = $this->db->getDatabase()->createCommand()->listCollections();
+
+        foreach ($collections as $collection) {
+            if (in_array($collection['name'], ['system.roles', 'system.users', 'system.indexes'])) {
+                // prevent deleting database auth data
+                // access to 'system.indexes' is more likely to be restricted, thus indexes will be dropped manually per collection
+                $this->stdout("System collection {$collection['name']} skipped.\n");
+                continue;
+            }
+
+            if (in_array($collection['name'], ['system.profile', 'system.js'])) {
+                // dropping of system collection is unlikely to be permitted, attempt to clear them out instead
+                $this->db->getDatabase()->createCommand()->delete($collection['name'], []);
+                $this->stdout("System collection {$collection['name']} truncated.\n");
+                continue;
+            }
+
+            $this->db->getDatabase()->createCommand()->dropIndexes($collection['name'], '*');
+            $this->db->getDatabase()->dropCollection($collection['name']);
+            $this->stdout("Collection {$collection['name']} dropped.\n");
+        }
+    }
+}

+ 115 - 0
vendor/yiisoft/yii2-mongodb/src/debug/ExplainAction.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\debug;
+
+use yii\base\Action;
+use yii\helpers\Json;
+use yii\web\HttpException;
+
+/**
+ * ExplainAction provides EXPLAIN information for MongoDB queries
+ *
+ * @author Sergey Smirnov <webdevsega@yandex.ru>
+ * @author Klimov Paul <klimov@zfort.com>
+ * @since 2.0.5
+ */
+class ExplainAction extends Action
+{
+    /**
+     * @var MongoDbPanel related debug toolbar panel
+     */
+    public $panel;
+
+
+    /**
+     * Runs the explain action
+     * @param int $seq
+     * @param string $tag
+     * @return string explain result content
+     * @throws HttpException if requested log not found
+     */
+    public function run($seq, $tag)
+    {
+        $this->controller->loadData($tag);
+
+        $timings = $this->panel->calculateTimings();
+
+        if (!isset($timings[$seq])) {
+            throw new HttpException(404, 'Log message not found.');
+        }
+
+        $query = $timings[$seq]['info'];
+
+        if (strpos($query, 'find({') !== 0) {
+            return '';
+        }
+
+        $query = substr($query, strlen('find('), -1);
+        $result = $this->explainQuery($query);
+        if (!$result) {
+            return '';
+        }
+
+        return Json::encode($result, JSON_PRETTY_PRINT);
+    }
+
+    /**
+     * Runs explain command over the query
+     *
+     * @param string $queryString query log string.
+     * @return array|false explain results, `false` on failure.
+     */
+    protected function explainQuery($queryString)
+    {
+        /* @var $connection \yii\mongodb\Connection */
+        $connection = $this->panel->getDb();
+
+        $queryInfo = Json::decode($queryString);
+        if (!isset($queryInfo['ns'])) {
+            return false;
+        }
+
+        list($databaseName, $collectionName) = explode('.', $queryInfo['ns'], 2);
+        unset($queryInfo['ns']);
+
+        if (!empty($queryInfo['filer'])) {
+            $queryInfo['filer'] = $this->prepareQueryFiler($queryInfo['filer']);
+        }
+
+        return $connection->createCommand($databaseName)->explain($collectionName, $queryInfo);
+    }
+
+    /**
+     * Prepare query filer for explain.
+     * Converts BSON object log entries into actual objects.
+     *
+     * @param array $query raw query filter.
+     * @return array|string prepared query
+     */
+    private function prepareQueryFiler($query)
+    {
+        $result = [];
+        foreach ($query as $key => $value) {
+            if (is_array($value)) {
+                $result[$key] = $this->prepareQueryFiler($value);
+            } elseif (is_string($value) && preg_match('#^(MongoDB\\\\BSON\\\\[A-Za-z]+)\\((.*)\\)$#s', $value, $matches)) {
+                $class = $matches[1];
+                $objectValue = $matches[1];
+
+                try {
+                    $result[$key] = new $class($objectValue);
+                } catch (\Exception $e) {
+                    $result[$key] = $value;
+                }
+            } else {
+                $result[$key] = $value;
+            }
+        }
+        return $result;
+    }
+}

+ 121 - 0
vendor/yiisoft/yii2-mongodb/src/debug/MongoDbPanel.php

@@ -0,0 +1,121 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\debug;
+
+use Yii;
+use yii\debug\models\search\Db;
+use yii\debug\panels\DbPanel;
+use yii\log\Logger;
+
+/**
+ * MongoDbPanel panel that collects and displays MongoDB queries performed.
+ *
+ * @property array $profileLogs This property is read-only.
+ *
+ * @author Klimov Paul <klimov@zfort.com>
+ * @since 2.0.1
+ */
+class MongoDbPanel extends DbPanel
+{
+    /**
+     * {@inheritdoc}
+     */
+    public $db = 'mongodb';
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function init()
+    {
+        $this->actions['mongodb-explain'] = [
+            'class' => 'yii\\mongodb\\debug\\ExplainAction',
+            'panel' => $this,
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'MongoDB';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSummaryName()
+    {
+        return 'MongoDB';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDetail()
+    {
+        $searchModel = new Db();
+
+        if (!$searchModel->load(Yii::$app->request->getQueryParams())) {
+            $searchModel->load($this->defaultFilter, '');
+        }
+
+        $dataProvider = $searchModel->search($this->getModels());
+        $dataProvider->getSort()->defaultOrder = $this->defaultOrder;
+
+        return Yii::$app->view->render('@yii/mongodb/debug/views/detail', [
+            'panel' => $this,
+            'dataProvider' => $dataProvider,
+            'searchModel' => $searchModel,
+        ]);
+    }
+
+    /**
+     * Returns all profile logs of the current request for this panel.
+     * @return array
+     */
+    public function getProfileLogs()
+    {
+        $target = $this->module->logTarget;
+
+        return $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, [
+            'yii\mongodb\Command::*',
+            'yii\mongodb\Query::*',
+            'yii\mongodb\BatchQueryResult::*',
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function hasExplain()
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getQueryType($timing)
+    {
+        $timing = ltrim($timing);
+        $timing = mb_substr($timing, 0, mb_strpos($timing, '('), 'utf8');
+        $matches = explode('.', $timing);
+
+        return count($matches) ? array_pop($matches) : '';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function canBeExplained($type)
+    {
+        return $type === 'find';
+    }
+}

+ 116 - 0
vendor/yiisoft/yii2-mongodb/src/debug/views/detail.php

@@ -0,0 +1,116 @@
+<?php
+/* @var $panel yii\mongodb\debug\MongoDbPanel */
+/* @var $searchModel yii\debug\models\search\Db */
+/* @var $dataProvider yii\data\ArrayDataProvider */
+
+use yii\helpers\Html;
+use yii\grid\GridView;
+use yii\web\View;
+
+echo Html::tag('h1', $panel->getName() . ' Queries');
+
+echo GridView::widget([
+    'dataProvider' => $dataProvider,
+    'id' => 'db-panel-detailed-grid',
+    'options' => ['class' => 'detail-grid-view table-responsive'],
+    'filterModel' => $searchModel,
+    'filterUrl' => $panel->getUrl(),
+    'columns' => [
+        [
+            'attribute' => 'seq',
+            'label' => 'Time',
+            'value' => function ($data) {
+                $timeInSeconds = $data['timestamp'] / 1000;
+                $millisecondsDiff = (int) (($timeInSeconds - (int) $timeInSeconds) * 1000);
+
+                return date('H:i:s.', $timeInSeconds) . sprintf('%03d', $millisecondsDiff);
+            },
+            'headerOptions' => [
+                'class' => 'sort-numerical'
+            ]
+        ],
+        [
+            'attribute' => 'duration',
+            'value' => function ($data) {
+                return sprintf('%.1f ms', $data['duration']);
+            },
+            'options' => [
+                'width' => '10%',
+            ],
+            'headerOptions' => [
+                'class' => 'sort-numerical'
+            ]
+        ],
+        [
+            'attribute' => 'type',
+            'value' => function ($data) {
+                return Html::encode($data['type']);
+            },
+            'filter' => $panel->getTypes(),
+        ],
+        [
+            'attribute' => 'query',
+            'value' => function ($data) use ($panel) {
+                $query = Html::encode($data['query']);
+
+                if (!empty($data['trace'])) {
+                    $query .= Html::ul($data['trace'], [
+                        'class' => 'trace',
+                        'item' => function ($trace) use ($panel) {
+                            return '<li>' . $panel->getTraceLine($trace) . '</li>';
+                        },
+                    ]);
+                }
+
+                if ($panel->canBeExplained($data['type'])) {
+                    $query .= Html::tag('p', '', ['class' => 'db-explain-text']);
+
+                    $query .= Html::tag(
+                        'div',
+                        Html::a('[+] Explain', (['mongodb-explain', 'seq' => $data['seq'], 'tag' => Yii::$app->controller->summary['tag']])),
+                        ['class' => 'db-explain']
+                    );
+                }
+
+                return $query;
+            },
+            'format' => 'raw',
+            'options' => [
+                'width' => '60%',
+            ],
+        ]
+    ],
+]);
+
+echo Html::tag(
+    'div',
+    Html::a('[+] Explain all', '#'),
+    ['id' => 'db-explain-all']
+);
+
+$this->registerJs('debug_db_detail();', View::POS_READY);
+?>
+
+<script>
+    function debug_db_detail() {
+        $('.db-explain a').on('click', function(e) {
+            e.preventDefault();
+
+            var $explain = $('.db-explain-text', $(this).parent().parent());
+
+            if ($explain.is(':visible')) {
+                $explain.hide();
+                $(this).text('[+] Explain');
+            } else {
+                $explain.load($(this).attr('href')).show();
+                $(this).text('[-] Explain');
+            }
+        });
+
+        $('#db-explain-all a').on('click', function(e) {
+            e.preventDefault();
+
+            $('.db-explain a').click();
+        });
+    }
+</script>

+ 183 - 0
vendor/yiisoft/yii2-mongodb/src/file/ActiveQuery.php

@@ -0,0 +1,183 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use yii\db\ActiveQueryInterface;
+use yii\db\ActiveQueryTrait;
+use yii\db\ActiveRelationTrait;
+
+/**
+ * ActiveQuery represents a Mongo query associated with an file Active Record class.
+ *
+ * ActiveQuery instances are usually created by [[ActiveRecord::find()]].
+ *
+ * Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
+ * [[orderBy()]] to customize the query options.
+ *
+ * ActiveQuery also provides the following additional query options:
+ *
+ * - [[with()]]: list of relations that this query should be performed with.
+ * - [[asArray()]]: whether to return each record as an array.
+ *
+ * These options can be configured using methods of the same name. For example:
+ *
+ * ```php
+ * $images = ImageFile::find()->with('tags')->asArray()->all();
+ * ```
+ *
+ * @property Collection $collection Collection instance. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class ActiveQuery extends Query implements ActiveQueryInterface
+{
+    use ActiveQueryTrait;
+    use ActiveRelationTrait;
+
+    /**
+     * @event Event an event that is triggered when the query is initialized via [[init()]].
+     */
+    const EVENT_INIT = 'init';
+
+
+    /**
+     * Constructor.
+     * @param array $modelClass the model class associated with this query
+     * @param array $config configurations to be applied to the newly created query object
+     */
+    public function __construct($modelClass, $config = [])
+    {
+        $this->modelClass = $modelClass;
+        parent::__construct($config);
+    }
+
+    /**
+     * Initializes the object.
+     * This method is called at the end of the constructor. The default implementation will trigger
+     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
+     * to ensure triggering of the event.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->trigger(self::EVENT_INIT);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function buildCursor($db = null)
+    {
+        if ($this->primaryModel !== null) {
+            // lazy loading
+            if ($this->via instanceof self) {
+                // via pivot collection
+                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
+                $this->filterByModels($viaModels);
+            } elseif (is_array($this->via)) {
+                // via relation
+                /* @var $viaQuery ActiveQuery */
+                list($viaName, $viaQuery) = $this->via;
+                if ($viaQuery->multiple) {
+                    $viaModels = $viaQuery->all();
+                    $this->primaryModel->populateRelation($viaName, $viaModels);
+                } else {
+                    $model = $viaQuery->one();
+                    $this->primaryModel->populateRelation($viaName, $model);
+                    $viaModels = $model === null ? [] : [$model];
+                }
+                $this->filterByModels($viaModels);
+            } else {
+                $this->filterByModels([$this->primaryModel]);
+            }
+        }
+
+        return parent::buildCursor($db);
+    }
+
+    /**
+     * Executes query and returns all results as an array.
+     * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query.
+     * If null, the Mongo connection returned by [[modelClass]] will be used.
+     * @return array|ActiveRecord the query results. If the query results in nothing, an empty array will be returned.
+     */
+    public function all($db = null)
+    {
+        return parent::all($db);
+    }
+
+    /**
+     * Executes query and returns a single row of result.
+     * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query.
+     * If null, the Mongo connection returned by [[modelClass]] will be used.
+     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
+     * the query result may be either an array or an ActiveRecord object. Null will be returned
+     * if the query results in nothing.
+     */
+    public function one($db = null)
+    {
+        $row = parent::one($db);
+        if ($row !== false) {
+            $models = $this->populate([$row]);
+            return reset($models) ?: null;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the Mongo collection for this query.
+     * @param \yii\mongodb\Connection $db Mongo connection.
+     * @return Collection collection instance.
+     */
+    public function getCollection($db = null)
+    {
+        /* @var $modelClass ActiveRecord */
+        $modelClass = $this->modelClass;
+        if ($db === null) {
+            $db = $modelClass::getDb();
+        }
+        if ($this->from === null) {
+            $this->from = $modelClass::collectionName();
+        }
+
+        return $db->getFileCollection($this->from);
+    }
+
+    /**
+     * Converts the raw query results into the format as specified by this query.
+     * This method is internally used to convert the data fetched from MongoDB
+     * into the format as required by this query.
+     * @param array $rows the raw query result from MongoDB
+     * @return array the converted query result
+     */
+    public function populate($rows)
+    {
+        if (empty($rows)) {
+            return [];
+        }
+
+        $indexBy = $this->indexBy;
+        $this->indexBy = null;
+        $rows = parent::populate($rows);
+        $this->indexBy = $indexBy;
+
+        $models = $this->createModels($rows);
+
+        if (!empty($this->with)) {
+            $this->findWith($this->with, $models);
+        }
+        if (!$this->asArray) {
+            foreach ($models as $model) {
+                $model->afterFind();
+            }
+        }
+
+        return parent::populate($models);
+    }
+}

+ 335 - 0
vendor/yiisoft/yii2-mongodb/src/file/ActiveRecord.php

@@ -0,0 +1,335 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use Yii;
+use yii\base\InvalidParamException;
+use yii\db\StaleObjectException;
+use yii\web\UploadedFile;
+
+/**
+ * ActiveRecord is the base class for classes representing Mongo GridFS files in terms of objects.
+ *
+ * To specify source file use the [[file]] attribute. It can be specified in one of the following ways:
+ *  - string - full name of the file, which content should be stored in GridFS
+ *  - \yii\web\UploadedFile - uploaded file instance, which content should be stored in GridFS
+ *
+ * For example:
+ *
+ * ```php
+ * $record = new ImageFile();
+ * $record->file = '/path/to/some/file.jpg';
+ * $record->save();
+ * ```
+ *
+ * You can also specify file content via [[newFileContent]] attribute:
+ *
+ * ```php
+ * $record = new ImageFile();
+ * $record->newFileContent = 'New file content';
+ * $record->save();
+ * ```
+ *
+ * Note: [[newFileContent]] always takes precedence over [[file]].
+ *
+ * @property null|string $fileContent File content. This property is read-only.
+ * @property resource $fileResource File stream resource. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+abstract class ActiveRecord extends \yii\mongodb\ActiveRecord
+{
+    /**
+     * {@inheritdoc}
+     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
+     */
+    public static function find()
+    {
+        return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
+    }
+
+    /**
+     * Return the Mongo GridFS collection instance for this AR class.
+     * @return Collection collection instance.
+     */
+    public static function getCollection()
+    {
+        return static::getDb()->getFileCollection(static::collectionName());
+    }
+
+    /**
+     * Returns the list of all attribute names of the model.
+     * This method could be overridden by child classes to define available attributes.
+     * Note: all attributes defined in base Active Record class should be always present
+     * in returned array.
+     * For example:
+     *
+     * ```php
+     * public function attributes()
+     * {
+     *     return array_merge(
+     *         parent::attributes(),
+     *         ['tags', 'status']
+     *     );
+     * }
+     * ```
+     *
+     * @return array list of attribute names.
+     */
+    public function attributes()
+    {
+        return [
+            '_id',
+            'filename',
+            'uploadDate',
+            'length',
+            'chunkSize',
+            'md5',
+            'file',
+            'newFileContent'
+        ];
+    }
+
+    /**
+     * @see ActiveRecord::insert()
+     */
+    protected function insertInternal($attributes = null)
+    {
+        if (!$this->beforeSave(true)) {
+            return false;
+        }
+        $values = $this->getDirtyAttributes($attributes);
+        if (empty($values)) {
+            $currentAttributes = $this->getAttributes();
+            foreach ($this->primaryKey() as $key) {
+                $values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null;
+            }
+        }
+        $collection = static::getCollection();
+        if (isset($values['newFileContent'])) {
+            $newFileContent = $values['newFileContent'];
+            unset($values['newFileContent']);
+        }
+        if (isset($values['file'])) {
+            $newFile = $values['file'];
+            unset($values['file']);
+        }
+        if (isset($newFileContent)) {
+            $newId = $collection->insertFileContent($newFileContent, $values);
+        } elseif (isset($newFile)) {
+            $fileName = $this->extractFileName($newFile);
+            $newId = $collection->insertFile($fileName, $values);
+        } else {
+            $newId = $collection->insert($values);
+        }
+        if ($newId !== null) {
+            $this->setAttribute('_id', $newId);
+            $values['_id'] = $newId;
+        }
+
+        $changedAttributes = array_fill_keys(array_keys($values), null);
+        $this->setOldAttributes($values);
+        $this->afterSave(true, $changedAttributes);
+
+        return true;
+    }
+
+    /**
+     * @see ActiveRecord::update()
+     * @throws StaleObjectException
+     */
+    protected function updateInternal($attributes = null)
+    {
+        if (!$this->beforeSave(false)) {
+            return false;
+        }
+        $values = $this->getDirtyAttributes($attributes);
+        if (empty($values)) {
+            $this->afterSave(false, $values);
+            return 0;
+        }
+
+        $collection = static::getCollection();
+        if (isset($values['newFileContent'])) {
+            $newFileContent = $values['newFileContent'];
+            unset($values['newFileContent']);
+        }
+        if (isset($values['file'])) {
+            $newFile = $values['file'];
+            unset($values['file']);
+        }
+        if (isset($newFileContent) || isset($newFile)) {
+            $fileAssociatedAttributeNames = [
+                'filename',
+                'uploadDate',
+                'length',
+                'chunkSize',
+                'md5',
+                'file',
+                'newFileContent'
+            ];
+            $values = array_merge($this->getAttributes(null, $fileAssociatedAttributeNames), $values);
+            $rows = $this->deleteInternal();
+            $insertValues = $values;
+            $insertValues['_id'] = $this->getAttribute('_id');
+            if (isset($newFileContent)) {
+                $collection->insertFileContent($newFileContent, $insertValues);
+            } else {
+                $fileName = $this->extractFileName($newFile);
+                $collection->insertFile($fileName, $insertValues);
+            }
+            $this->setAttribute('newFileContent', null);
+            $this->setAttribute('file', null);
+        } else {
+            $condition = $this->getOldPrimaryKey(true);
+            $lock = $this->optimisticLock();
+            if ($lock !== null) {
+                if (!isset($values[$lock])) {
+                    $values[$lock] = $this->$lock + 1;
+                }
+                $condition[$lock] = $this->$lock;
+            }
+            // We do not check the return value of update() because it's possible
+            // that it doesn't change anything and thus returns 0.
+            $rows = $collection->update($condition, $values);
+            if ($lock !== null && !$rows) {
+                throw new StaleObjectException('The object being updated is outdated.');
+            }
+        }
+
+        $changedAttributes = [];
+        foreach ($values as $name => $value) {
+            $changedAttributes[$name] = $this->getOldAttribute($name);
+            $this->setOldAttribute($name, $value);
+        }
+        $this->afterSave(false, $changedAttributes);
+
+        return $rows;
+    }
+
+    /**
+     * Extracts filename from given raw file value.
+     * @param mixed $file raw file value.
+     * @return string file name.
+     * @throws \yii\base\InvalidParamException on invalid file value.
+     */
+    protected function extractFileName($file)
+    {
+        if ($file instanceof UploadedFile) {
+            return $file->tempName;
+        } elseif (is_string($file)) {
+            if (file_exists($file)) {
+                return $file;
+            }
+            throw new InvalidParamException("File '{$file}' does not exist.");
+        }
+
+        throw new InvalidParamException('Unsupported type of "file" attribute.');
+    }
+
+    /**
+     * Refreshes the [[file]] attribute from file collection, using current primary key.
+     * @return \MongoGridFSFile|null refreshed file value.
+     */
+    public function refreshFile()
+    {
+        $mongoFile = $this->getCollection()->get($this->getPrimaryKey());
+        $this->setAttribute('file', $mongoFile);
+
+        return $mongoFile;
+    }
+
+    /**
+     * Returns the associated file content.
+     * @return null|string file content.
+     * @throws \yii\base\InvalidParamException on invalid file attribute value.
+     */
+    public function getFileContent()
+    {
+        $file = $this->getAttribute('file');
+        if (empty($file) && !$this->getIsNewRecord()) {
+            $file = $this->refreshFile();
+        }
+
+        if (empty($file)) {
+            return null;
+        } elseif ($file instanceof Download) {
+            $fileSize = $file->getSize();
+            return empty($fileSize) ? null : $file->toString();
+        } elseif ($file instanceof UploadedFile) {
+            return file_get_contents($file->tempName);
+        } elseif (is_string($file)) {
+            if (file_exists($file)) {
+                return file_get_contents($file);
+            }
+            throw new InvalidParamException("File '{$file}' does not exist.");
+        }
+
+        throw new InvalidParamException('Unsupported type of "file" attribute.');
+    }
+
+    /**
+     * Writes the the internal file content into the given filename.
+     * @param string $filename full filename to be written.
+     * @return bool whether the operation was successful.
+     * @throws \yii\base\InvalidParamException on invalid file attribute value.
+     */
+    public function writeFile($filename)
+    {
+        $file = $this->getAttribute('file');
+        if (empty($file) && !$this->getIsNewRecord()) {
+            $file = $this->refreshFile();
+        }
+
+        if (empty($file)) {
+            throw new InvalidParamException('There is no file associated with this object.');
+        } elseif ($file instanceof Download) {
+            return ($file->toFile($filename) == $file->getSize());
+        } elseif ($file instanceof UploadedFile) {
+            return copy($file->tempName, $filename);
+        } elseif (is_string($file)) {
+            if (file_exists($file)) {
+                return copy($file, $filename);
+            }
+            throw new InvalidParamException("File '{$file}' does not exist.");
+        }
+
+        throw new InvalidParamException('Unsupported type of "file" attribute.');
+    }
+
+    /**
+     * This method returns a stream resource that can be used with all file functions in PHP,
+     * which deal with reading files. The contents of the file are pulled out of MongoDB on the fly,
+     * so that the whole file does not have to be loaded into memory first.
+     * @return resource file stream resource.
+     * @throws \yii\base\InvalidParamException on invalid file attribute value.
+     */
+    public function getFileResource()
+    {
+        $file = $this->getAttribute('file');
+        if (empty($file) && !$this->getIsNewRecord()) {
+            $file = $this->refreshFile();
+        }
+
+        if (empty($file)) {
+            throw new InvalidParamException('There is no file associated with this object.');
+        } elseif ($file instanceof Download) {
+            return $file->getResource();
+        } elseif ($file instanceof UploadedFile) {
+            return fopen($file->tempName, 'r');
+        } elseif (is_string($file)) {
+            if (file_exists($file)) {
+                return fopen($file, 'r');
+            }
+            throw new InvalidParamException("File '{$file}' does not exist.");
+        }
+
+        throw new InvalidParamException('Unsupported type of "file" attribute.');
+    }
+}

+ 327 - 0
vendor/yiisoft/yii2-mongodb/src/file/Collection.php

@@ -0,0 +1,327 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use MongoDB\BSON\ObjectID;
+use yii\mongodb\Exception;
+use Yii;
+use yii\web\UploadedFile;
+
+/**
+ * Collection represents the Mongo GridFS collection information.
+ *
+ * A file collection object is usually created by calling [[Database::getFileCollection()]] or [[Connection::getFileCollection()]].
+ *
+ * File collection inherits all interface from regular [[\yii\mongo\Collection]], adding methods to store files.
+ *
+ * @property \yii\mongodb\Collection $chunkCollection Mongo collection instance. This property is read-only.
+ * @property \yii\mongodb\Collection $fileCollection Mongo collection instance. This property is read-only.
+ * @property string $prefix Prefix of this file collection.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Collection extends \yii\mongodb\Collection
+{
+    /**
+     * @var \yii\mongodb\Database MongoDB database instance.
+     */
+    public $database;
+
+    /**
+     * @var string prefix of this file collection.
+     */
+    private $_prefix;
+    /**
+     * @var \yii\mongodb\Collection file chunks MongoDB collection.
+     */
+    private $_chunkCollection;
+    /**
+     * @var \yii\mongodb\Collection files MongoDB collection.
+     */
+    private $_fileCollection;
+    /**
+     * @var bool whether file related fields indexes are ensured for this collection.
+     */
+    private $indexesEnsured = false;
+
+
+    /**
+     * @return string prefix of this file collection.
+     */
+    public function getPrefix()
+    {
+        return $this->_prefix;
+    }
+
+    /**
+     * @param string $prefix prefix of this file collection.
+     */
+    public function setPrefix($prefix)
+    {
+        $this->_prefix = $prefix;
+        $this->name = $prefix . '.files';
+    }
+
+    /**
+     * Creates upload command.
+     * @param array $options upload options.
+     * @return Upload file upload instance.
+     * @since 2.1
+     */
+    public function createUpload($options = [])
+    {
+        $config = $options;
+        $config['collection'] = $this;
+        return new Upload($config);
+    }
+
+    /**
+     * Creates download command.
+     * @param array|ObjectID $document file document ot be downloaded.
+     * @return Download file download instance.
+     * @since 2.1
+     */
+    public function createDownload($document)
+    {
+        return new Download([
+            'collection' => $this,
+            'document' => $document,
+        ]);
+    }
+
+    /**
+     * Returns the MongoDB collection for the file chunks.
+     * @param bool $refresh whether to reload the collection instance even if it is found in the cache.
+     * @return \yii\mongodb\Collection mongo collection instance.
+     */
+    public function getChunkCollection($refresh = false)
+    {
+        if ($refresh || !is_object($this->_chunkCollection)) {
+            $this->_chunkCollection = Yii::createObject([
+                'class' => 'yii\mongodb\Collection',
+                'database' => $this->database,
+                'name' => $this->getPrefix() . '.chunks'
+            ]);
+        }
+
+        return $this->_chunkCollection;
+    }
+
+    /**
+     * Returns the MongoDB collection for the files.
+     * @param bool $refresh whether to reload the collection instance even if it is found in the cache.
+     * @return \yii\mongodb\Collection mongo collection instance.
+     * @since 2.1
+     */
+    public function getFileCollection($refresh = false)
+    {
+        if ($refresh || !is_object($this->_fileCollection)) {
+            $this->_fileCollection = Yii::createObject([
+                'class' => 'yii\mongodb\Collection',
+                'database' => $this->database,
+                'name' => $this->name
+            ]);
+        }
+
+        return $this->_fileCollection;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function drop()
+    {
+        return parent::drop() && $this->database->dropCollection($this->getChunkCollection()->name);
+    }
+
+    /**
+     * {@inheritdoc}
+     * @return Cursor cursor for the search results
+     */
+    public function find($condition = [], $fields = [], $options = [])
+    {
+        return new Cursor($this, parent::find($condition, $fields, $options));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function remove($condition = [], $options = [])
+    {
+        $fileCollection = $this->getFileCollection();
+        $chunkCollection = $this->getChunkCollection();
+
+        if (empty($condition) && empty($options['limit'])) {
+            // truncate :
+            $deleteCount = $fileCollection->remove([], $options);
+            $chunkCollection->remove([], $options);
+            return $deleteCount;
+        }
+
+        $batchSize = 200;
+        $options['batchSize'] = $batchSize;
+        $cursor = $fileCollection->find($condition, ['_id'], $options);
+        unset($options['limit']);
+        $deleteCount = 0;
+        $deleteCallback = function ($ids) use ($fileCollection, $chunkCollection, $options) {
+            $chunkCollection->remove(['files_id' => ['$in' => $ids]], $options);
+            return $fileCollection->remove(['_id' => ['$in' => $ids]], $options);
+        };
+
+        $ids = [];
+        $idsCount = 0;
+        foreach ($cursor as $row) {
+            $ids[] = $row['_id'];
+            $idsCount++;
+            if ($idsCount >= $batchSize) {
+                $deleteCount += $deleteCallback($ids);
+                $ids = [];
+                $idsCount = 0;
+            }
+        }
+
+        if (!empty($ids)) {
+            $deleteCount += $deleteCallback($ids);
+        }
+
+        return $deleteCount;
+    }
+
+    /**
+     * Creates new file in GridFS collection from given local filesystem file.
+     * Additional attributes can be added file document using $metadata.
+     * @param string $filename name of the file to store.
+     * @param array $metadata other metadata fields to include in the file document.
+     * @param array $options list of options in format: optionName => optionValue
+     * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]]
+     * unless an "_id" was explicitly specified in the metadata.
+     * @throws Exception on failure.
+     */
+    public function insertFile($filename, $metadata = [], $options = [])
+    {
+        $options['document'] = $metadata;
+        $document = $this->createUpload($options)->addFile($filename)->complete();
+        return $document['_id'];
+    }
+
+    /**
+     * Creates new file in GridFS collection with specified content.
+     * Additional attributes can be added file document using $metadata.
+     * @param string $bytes string of bytes to store.
+     * @param array $metadata other metadata fields to include in the file document.
+     * @param array $options list of options in format: optionName => optionValue
+     * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]]
+     * unless an "_id" was explicitly specified in the metadata.
+     * @throws Exception on failure.
+     */
+    public function insertFileContent($bytes, $metadata = [], $options = [])
+    {
+        $options['document'] = $metadata;
+        $document = $this->createUpload($options)->addContent($bytes)->complete();
+        return $document['_id'];
+    }
+
+    /**
+     * Creates new file in GridFS collection from uploaded file.
+     * Additional attributes can be added file document using $metadata.
+     * @param string $name name of the uploaded file to store. This should correspond to
+     * the file field's name attribute in the HTML form.
+     * @param array $metadata other metadata fields to include in the file document.
+     * @param array $options list of options in format: optionName => optionValue
+     * @return mixed the "_id" of the saved file document. This will be a generated [[\MongoId]]
+     * unless an "_id" was explicitly specified in the metadata.
+     * @throws Exception on failure.
+     */
+    public function insertUploads($name, $metadata = [], $options = [])
+    {
+        $uploadedFile = UploadedFile::getInstanceByName($name);
+        if ($uploadedFile === null) {
+            throw new Exception("Uploaded file '{$name}' does not exist.");
+        }
+
+        $options['filename'] = $uploadedFile->name;
+        $options['document'] = $metadata;
+        $document = $this->createUpload($options)->addFile($uploadedFile->tempName)->complete();
+        return $document['_id'];
+    }
+
+    /**
+     * Retrieves the file with given _id.
+     * @param mixed $id _id of the file to find.
+     * @return Download|null found file, or null if file does not exist
+     * @throws Exception on failure.
+     */
+    public function get($id)
+    {
+        $document = $this->getFileCollection()->findOne(['_id' => $id]);
+        return empty($document) ? null : $this->createDownload($document);
+    }
+
+    /**
+     * Deletes the file with given _id.
+     * @param mixed $id _id of the file to find.
+     * @return bool whether the operation was successful.
+     * @throws Exception on failure.
+     */
+    public function delete($id)
+    {
+        $this->remove(['_id' => $id], ['limit' => 1]);
+        return true;
+    }
+
+    /**
+     * Makes sure that indexes, which are crucial for the file processing,
+     * exist at this collection and [[chunkCollection]].
+     * The check result is cached per collection instance.
+     * @param bool $force whether to ignore internal collection instance cache.
+     * @return $this self reference.
+     */
+    public function ensureIndexes($force = false)
+    {
+        if (!$force && $this->indexesEnsured) {
+            return $this;
+        }
+
+        $this->ensureFileIndexes();
+        $this->ensureChunkIndexes();
+
+        $this->indexesEnsured = true;
+        return $this;
+    }
+
+    /**
+     * Ensures indexes at file collection.
+     */
+    private function ensureFileIndexes()
+    {
+        $indexKey = ['filename' => 1, 'uploadDate' => 1];
+        foreach ($this->listIndexes() as $index) {
+            if ($index['key'] === $indexKey) {
+                return;
+            }
+        }
+
+        $this->createIndex($indexKey);
+    }
+
+    /**
+     * Ensures indexes at chunk collection.
+     */
+    private function ensureChunkIndexes()
+    {
+        $chunkCollection = $this->getChunkCollection();
+        $indexKey = ['files_id' => 1, 'n' => 1];
+        foreach ($chunkCollection->listIndexes() as $index) {
+            if (!empty($index['unique']) && $index['key'] === $indexKey) {
+                return;
+            }
+        }
+        $chunkCollection->createIndex($indexKey, ['unique' => true]);
+    }
+}

+ 149 - 0
vendor/yiisoft/yii2-mongodb/src/file/Cursor.php

@@ -0,0 +1,149 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+/**
+ * Cursor is a wrapper around [[\MongoDB\Driver\Cursor]], which allows returning of the
+ * record with [[Download]] instance attached.
+ *
+ * @method \MongoDB\Driver\Cursor getInnerIterator()
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class Cursor extends \IteratorIterator implements \Countable
+{
+    /**
+     * @var Collection related GridFS collection instance.
+     */
+    public $collection;
+
+
+    /**
+     * Constructor.
+     * @param Collection $collection
+     * @param \MongoDB\Driver\Cursor $cursor
+     */
+    public function __construct($collection, $cursor)
+    {
+        $this->collection = $collection;
+        parent::__construct($cursor);
+    }
+
+    /**
+     * Return the current element
+     * This method is required by the interface [[\Iterator]].
+     * @return mixed current row
+     */
+    public function current()
+    {
+        $value = parent::current();
+        if (!isset($value['file'])) {
+            $value['file'] = $this->collection->createDownload(array_intersect_key($value, ['_id' => true, 'filename' => true, 'length' => true, 'chunkSize' => true]));
+        }
+        return $value;
+    }
+
+    /**
+     * Count elements of this cursor.
+     * This method is required by the interface [[\Countable]].
+     * @return int elements count.
+     */
+    public function count()
+    {
+        return count($this->cursor);
+    }
+
+    // Mock up original cursor interface :
+
+    /**
+     * Returns an array containing all results for this cursor
+     * @return array containing all results for this cursor.
+     */
+    public function toArray()
+    {
+        $result = [];
+        foreach ($this as $key => $value) {
+            $result[$key] = $value;
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the ID for this cursor.
+     * @return \MongoDB\Driver\CursorId cursor ID.
+     */
+    public function getId()
+    {
+        return $this->getInnerIterator()->getId();
+    }
+
+    /**
+     * Sets a type map to use for BSON unserialization.
+     * @param array $typemap type map.
+     */
+    public function setTypeMap($typemap)
+    {
+        $this->getInnerIterator()->setTypeMap($typemap);
+    }
+
+    /**
+     * PHP magic method, which is invoked on attempt of invocation not existing method.
+     * It redirects method call to inner iterator.
+     * @param string $name method name.
+     * @param array $arguments method arguments
+     * @return mixed method result.
+     */
+    public function __call($name, $arguments)
+    {
+        return call_user_func_array([$this->getInnerIterator(), $name], $arguments);
+    }
+
+    /**
+     * PHP magic method, which is invoked on attempt of setting not existing property.
+     * It passes value to the inner iterator.
+     * @param string $name field name.
+     * @param mixed $value field value.
+     */
+    public function __set($name, $value)
+    {
+        $this->getInnerIterator()->{$name} = $value;
+    }
+
+    /**
+     * PHP magic method, which is invoked on attempt of getting not existing property.
+     * It returns value from the inner iterator.
+     * @param string $name field name.
+     * @return mixed field value.
+     */
+    public function __get($name)
+    {
+        return $this->getInnerIterator()->{$name};
+    }
+
+    /**
+     * PHP magic method, which is invoked on attempt of checking if a property is set.
+     * @param string $name field name.
+     * @return bool whether field exists or not.
+     */
+    public function __isset($name)
+    {
+        $cursor = $this->getInnerIterator();
+        return isset($cursor->$name);
+    }
+
+    /**
+     * PHP magic method, which is invoked on attempt of unsetting of property.
+     * @param string $name field name.
+     */
+    public function __unset($name)
+    {
+        $cursor = $this->getInnerIterator();
+        unset($cursor->$name);
+    }
+}

+ 319 - 0
vendor/yiisoft/yii2-mongodb/src/file/Download.php

@@ -0,0 +1,319 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use MongoDB\BSON\ObjectID;
+use Yii;
+use yii\base\InvalidConfigException;
+use yii\base\BaseObject;
+use yii\helpers\FileHelper;
+use yii\helpers\StringHelper;
+
+/**
+ * Download represents the GridFS download operation.
+ *
+ * A `Download` object is usually created by calling [[Collection::get()]] or [[Collection::createDownload()]].
+ *
+ * Usage example:
+ *
+ * ```php
+ * Yii::$app->mongodb->getFileCollection()->createDownload($document['_id'])->toFile('/path/to/file.dat');
+ * ```
+ *
+ * You can use `Download::substr()` to read a specific part of the file:
+ *
+ * ```php
+ * $filePart = Yii::$app->mongodb->getFileCollection()->createDownload($document['_id'])->substr(256, 1024);
+ * ```
+ *
+ * @property string $bytes File content. This property is read-only.
+ * @property \MongoDB\Driver\Cursor $chunkCursor Chuck list cursor. This property is read-only.
+ * @property \Iterator $chunkIterator Chuck cursor iterator. This property is read-only.
+ * @property array $document Document to be downloaded. Note that the type of this property differs in getter
+ * and setter. See [[getDocument()]] and [[setDocument()]] for details.
+ * @property string|null $filename File name. This property is read-only.
+ * @property resource $resource File stream resource. This property is read-only.
+ * @property int $size File size. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class Download extends BaseObject
+{
+    /**
+     * @var Collection file collection to be used.
+     */
+    public $collection;
+
+    /**
+     * @var array|ObjectID document to be downloaded.
+     */
+    private $_document;
+    /**
+     * @var \MongoDB\Driver\Cursor cursor for the file chunks.
+     */
+    private $_chunkCursor;
+    /**
+     * @var \Iterator iterator for [[chunkCursor]].
+     */
+    private $_chunkIterator;
+    /**
+     * @var resource|null
+     */
+    private $_resource;
+
+
+    /**
+     * @return array document to be downloaded.
+     * @throws InvalidConfigException on invalid document configuration.
+     */
+    public function getDocument()
+    {
+        if (!is_array($this->_document)) {
+            if (is_scalar($this->_document) || $this->_document instanceof ObjectID) {
+                $document = $this->collection->findOne(['_id' => $this->_document]);
+                if (empty($document)) {
+                    throw new InvalidConfigException('Document id=' . $this->_document . ' does not exist at collection "' . $this->collection->getFullName() . '"');
+                }
+                $this->_document = $document;
+            } else {
+                $this->_document = (array)$this->_document;
+            }
+        }
+        return $this->_document;
+    }
+
+    /**
+     * Sets data of the document to be downloaded.
+     * Document can be specified by its ID, in this case its data will be fetched automatically
+     * via extra query.
+     * @param array|ObjectID $document document raw data or document ID.
+     */
+    public function setDocument($document)
+    {
+        $this->_document = $document;
+    }
+
+    /**
+     * Returns the size of the associated file.
+     * @return int file size.
+     */
+    public function getSize()
+    {
+        $document = $this->getDocument();
+        return isset($document['length']) ? $document['length'] : 0;
+    }
+
+    /**
+     * Returns associated file's filename.
+     * @return string|null file name.
+     */
+    public function getFilename()
+    {
+        $document = $this->getDocument();
+        return isset($document['filename']) ? $document['filename'] : null;
+    }
+
+    /**
+     * Returns file chunks read cursor.
+     * @param bool $refresh whether to recreate cursor, if it is already exist.
+     * @return \MongoDB\Driver\Cursor chuck list cursor.
+     * @throws InvalidConfigException
+     */
+    public function getChunkCursor($refresh = false)
+    {
+        if ($refresh || $this->_chunkCursor === null) {
+            $file = $this->getDocument();
+            $this->_chunkCursor = $this->collection->getChunkCollection()->find(
+                ['files_id' => $file['_id']],
+                [],
+                ['sort' => ['n' => 1]]
+            );
+        }
+        return $this->_chunkCursor;
+    }
+
+    /**
+     * Returns iterator for the file chunks cursor.
+     * @param bool $refresh whether to recreate iterator, if it is already exist.
+     * @return \Iterator chuck cursor iterator.
+     */
+    public function getChunkIterator($refresh = false)
+    {
+        if ($refresh || $this->_chunkIterator === null) {
+            $this->_chunkIterator = new \IteratorIterator($this->getChunkCursor($refresh));
+            $this->_chunkIterator->rewind();
+        }
+        return $this->_chunkIterator;
+    }
+
+    /**
+     * Saves file into the given stream.
+     * @param resource $stream stream, which file should be saved to.
+     * @return int number of written bytes.
+     */
+    public function toStream($stream)
+    {
+        $bytesWritten = 0;
+        foreach ($this->getChunkCursor() as $chunk) {
+            $bytesWritten += fwrite($stream, $chunk['data']->getData());
+        }
+        return $bytesWritten;
+    }
+
+    /**
+     * Saves download to the physical file.
+     * @param string $filename name of the physical file.
+     * @return int number of written bytes.
+     */
+    public function toFile($filename)
+    {
+        $filename = Yii::getAlias($filename);
+        FileHelper::createDirectory(dirname($filename));
+        return $this->toStream(fopen($filename, 'w+'));
+    }
+
+    /**
+     * Returns a string of the bytes in the associated file.
+     * @return string file content.
+     */
+    public function toString()
+    {
+        $result = '';
+        foreach ($this->getChunkCursor() as $chunk) {
+            $result .= $chunk['data']->getData();
+        }
+        return $result;
+    }
+
+    /**
+     * Returns an opened stream resource, which can be used to read file.
+     * Note: each invocation of this method will create new file resource.
+     * @return resource stream resource.
+     */
+    public function toResource()
+    {
+        $protocol = $this->collection->database->connection->registerFileStreamWrapper();
+
+        $context = stream_context_create([
+            $protocol => [
+                'download' => $this,
+            ]
+        ]);
+
+        $document = $this->getDocument();
+        $url = "{$protocol}://{$this->collection->database->name}.{$this->collection->prefix}?_id={$document['_id']}";
+        return fopen($url, 'r', false, $context);
+    }
+
+    /**
+     * Return part of a file.
+     * @param int $start reading start position.
+     * If non-negative, the returned string will start at the start'th position in file, counting from zero.
+     * If negative, the returned string will start at the start'th character from the end of file.
+     * @param int $length number of bytes to read.
+     * If given and is positive, the string returned will contain at most length characters beginning from start (depending on the length of file).
+     * If given and is negative, then that many characters will be omitted from the end of file (after the start position has been calculated when a start is negative).
+     * @return string|false the extracted part of file or `false` on failure
+     */
+    public function substr($start, $length)
+    {
+        $document = $this->getDocument();
+
+        if ($start < 0) {
+            $start = max($document['length'] + $start, 0);
+        }
+
+        if ($start > $document['length']) {
+            return false;
+        }
+
+        if ($length < 0) {
+            $length = $document['length'] - $start + $length;
+            if ($length < 0) {
+                return false;
+            }
+        }
+
+        $chunkSize = $document['chunkSize'];
+
+        $startChunkNumber = floor($start / $chunkSize);
+
+        $chunkIterator = $this->getChunkIterator();
+
+        if (!$chunkIterator->valid()) {
+            // invalid iterator state - recreate iterator
+            // unable to use `rewind` due to error "Cursors cannot rewind after starting iteration"
+            $chunkIterator = $this->getChunkIterator(true);
+        }
+
+        if ($chunkIterator->key() > $startChunkNumber) {
+            // unable to go back by iterator
+            // unable to use `rewind` due to error "Cursors cannot rewind after starting iteration"
+            $chunkIterator = $this->getChunkIterator(true);
+        }
+
+        $result = '';
+
+        $chunkDataOffset = $start - $startChunkNumber * $chunkSize;
+        while ($chunkIterator->valid()) {
+            if ($chunkIterator->key() >= $startChunkNumber) {
+                $chunk = $chunkIterator->current();
+                $data = $chunk['data']->getData();
+
+                $readLength = min($chunkSize - $chunkDataOffset, $length);
+
+                $result .= StringHelper::byteSubstr($data, $chunkDataOffset, $readLength);
+
+                $length -= $readLength;
+                if ($length <= 0) {
+                    break;
+                }
+
+                $chunkDataOffset = 0;
+            }
+
+            $chunkIterator->next();
+        }
+
+        return $result;
+    }
+
+    // Compatibility with `MongoGridFSFile` :
+
+    /**
+     * Alias of [[toString()]] method.
+     * @return string file content.
+     */
+    public function getBytes()
+    {
+        return $this->toString();
+    }
+
+    /**
+     * Alias of [[toFile()]] method.
+     * @param string $filename name of the physical file.
+     * @return int number of written bytes.
+     */
+    public function write($filename)
+    {
+        return $this->toFile($filename);
+    }
+
+    /**
+     * Returns persistent stream resource, which can be used to read file.
+     * @return resource file stream resource.
+     */
+    public function getResource()
+    {
+        if ($this->_resource === null) {
+            $this->_resource = $this->toResource();
+        }
+        return $this->_resource;
+    }
+}

+ 39 - 0
vendor/yiisoft/yii2-mongodb/src/file/Query.php

@@ -0,0 +1,39 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use Yii;
+
+/**
+ * Query represents Mongo "find" operation for GridFS collection.
+ *
+ * Query behaves exactly as regular [[\yii\mongodb\Query]].
+ * Found files will be represented as arrays of file document attributes with
+ * additional 'file' key, which stores [[\MongoGridFSFile]] instance.
+ *
+ * @property Collection $collection Collection instance. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Query extends \yii\mongodb\Query
+{
+    /**
+     * Returns the Mongo collection for this query.
+     * @param \yii\mongodb\Connection $db Mongo connection.
+     * @return Collection collection instance.
+     */
+    public function getCollection($db = null)
+    {
+        if ($db === null) {
+            $db = Yii::$app->get('mongodb');
+        }
+
+        return $db->getFileCollection($this->from);
+    }
+}

+ 415 - 0
vendor/yiisoft/yii2-mongodb/src/file/StreamWrapper.php

@@ -0,0 +1,415 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use yii\base\InvalidConfigException;
+use yii\base\BaseObject;
+use yii\di\Instance;
+use yii\helpers\StringHelper;
+use yii\mongodb\Connection;
+
+/**
+ * StreamWrapper provides stream wrapper for MongoDB GridFS, allowing file operations via
+ * regular PHP stream resources.
+ *
+ * Before feature can be used this wrapper should be registered via [[register()]] method.
+ * It is usually performed via [[yii\mongodb\Connection::registerFileStreamWrapper()]].
+ *
+ * Note: do not use this class directly - its instance will be created and maintained by PHP internally
+ * once corresponding stream resource is created.
+ *
+ * Resource path should be specified in following format:
+ *
+ * ```
+ * 'protocol://databaseName.fileCollectionPrefix?file_attribute=value'
+ * ```
+ *
+ * Write example:
+ *
+ * ```php
+ * $resource = fopen('gridfs://mydatabase.fs?filename=new_file.txt', 'w');
+ * fwrite($resource, 'some content');
+ * // ...
+ * fclose($resource);
+ * ```
+ *
+ * Read example:
+ *
+ * ```php
+ * $resource = fopen('gridfs://mydatabase.fs?filename=my_file.txt', 'r');
+ * $fileContent = stream_get_contents($resource);
+ * ```
+ *
+ * @see http://php.net/manual/en/function.stream-wrapper-register.php
+ *
+ * @property array $contextOptions Context options. This property is read-only.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class StreamWrapper extends BaseObject
+{
+    /**
+     * @var resource associated stream resource context.
+     * This property is set automatically by PHP once wrapper is instantiated.
+     */
+    public $context;
+
+    /**
+     * @var array context options associated with [[context]].
+     */
+    private $_contextOptions;
+    /**
+     * @var string protocol associated with stream
+     */
+    private $_protocol;
+    /**
+     * @var string namespace in format 'databaseName.collectionName' associated with stream.
+     */
+    private $_namespace;
+    /**
+     * @var array query parameters passed for the stream.
+     */
+    private $_queryParams = [];
+    /**
+     * @var Upload file upload instance
+     */
+    private $_upload;
+    /**
+     * @var Download file upload instance
+     */
+    private $_download;
+    /**
+     * @var int file pointer offset.
+     */
+    private $_pointerOffset = 0;
+
+
+    /**
+     * Registers this steam wrapper.
+     * @param string $protocol name of the protocol to be used.
+     * @param bool $force whether to register wrapper, even if protocol is already taken.
+     */
+    public static function register($protocol = 'gridfs', $force = false)
+    {
+        if (in_array($protocol, stream_get_wrappers())) {
+            if (!$force) {
+                return;
+            }
+            stream_wrapper_unregister($protocol);
+        }
+
+        stream_wrapper_register($protocol, get_called_class(), STREAM_IS_URL);
+    }
+
+    /**
+     * Returns options associated with [[context]].
+     * @return array context options.
+     */
+    public function getContextOptions()
+    {
+        if ($this->_contextOptions === null) {
+            $this->_contextOptions = stream_context_get_options($this->context);
+        }
+        return $this->_contextOptions;
+    }
+
+    /**
+     * Parses stream open path, initializes internal parameters.
+     * @param string $path stream open path.
+     */
+    private function parsePath($path)
+    {
+        $pathInfo = parse_url($path);
+
+        $this->_protocol = $pathInfo['scheme'];
+        $this->_namespace = $pathInfo['host'];
+        parse_str($pathInfo['query'], $this->_queryParams);
+    }
+
+    /**
+     * Prepares [[Download]] instance for the read operations.
+     * @return bool success.
+     * @throws InvalidConfigException on invalid context configuration.
+     */
+    private function prepareDownload()
+    {
+        $contextOptions = $this->getContextOptions();
+        if (isset($contextOptions[$this->_protocol]['download'])) {
+            $download = $contextOptions[$this->_protocol]['download'];
+            if (!$download instanceof Download) {
+                throw new InvalidConfigException('"download" context option should be an instance of "' . Download::className() . '"');
+            }
+            $this->_download = $download;
+            return true;
+        }
+
+        $collection = $this->fetchCollection();
+        if (empty($this->_queryParams)) {
+            return false;
+        }
+        $file = $collection->findOne($this->_queryParams);
+        if (empty($file)) {
+            throw new InvalidConfigException('Requested file does not exits.');
+        }
+
+        $this->_download = $file['file'];
+        return true;
+    }
+
+    /**
+     * Prepares [[Upload]] instance for the write operations.
+     * @return bool success.
+     * @throws InvalidConfigException on invalid context configuration.
+     */
+    private function prepareUpload()
+    {
+        $contextOptions = $this->getContextOptions();
+        if (isset($contextOptions[$this->_protocol]['upload'])) {
+            $upload = $contextOptions[$this->_protocol]['upload'];
+            if (!$upload instanceof Upload) {
+                throw new InvalidConfigException('"upload" context option should be an instance of "' . Upload::className() . '"');
+            }
+            $this->_upload = $upload;
+            return true;
+        }
+
+        $collection = $this->fetchCollection();
+        $this->_upload = $collection->createUpload(['document' => $this->_queryParams]);
+        return true;
+    }
+
+    /**
+     * Fetches associated file collection from stream options.
+     * @return Collection file collection instance.
+     * @throws InvalidConfigException on invalid stream options.
+     */
+    private function fetchCollection()
+    {
+        $contextOptions = $this->getContextOptions();
+
+        if (isset($contextOptions[$this->_protocol]['collection'])) {
+            $collection = $contextOptions[$this->_protocol]['collection'];
+            if ($collection instanceof Collection) {
+                throw new InvalidConfigException('"collection" context option should be an instance of "' . Collection::className() . '"');
+            }
+
+            return $collection;
+        }
+
+        $connection = isset($contextOptions[$this->_protocol]['db'])
+            ? $contextOptions[$this->_protocol]['db']
+            : 'mongodb';
+
+        /* @var $connection Connection */
+        $connection = Instance::ensure($connection, Connection::className());
+
+        list($databaseName, $collectionPrefix) = explode('.', $this->_namespace, 2);
+        return $connection->getDatabase($databaseName)->getFileCollection($collectionPrefix);
+    }
+
+    /**
+     * Default template for file statistic data set.
+     * @see stat()
+     * @return array statistic information.
+     */
+    private function fileStatisticsTemplate()
+    {
+        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,
+        ];
+    }
+
+    // Stream Interface :
+
+    /**
+     * Closes a resource.
+     * This method is called in response to `fclose()`.
+     * @see fclose()
+     */
+    public function stream_close()
+    {
+        if ($this->_upload !== null) {
+            $this->_upload->complete();
+            $this->_upload = null;
+        }
+        if ($this->_download !== null) {
+            $this->_download = null;
+        }
+    }
+
+    /**
+     * Tests for end-of-file on a file pointer.
+     * This method is called in response to `feof()`.
+     * @see feof()
+     * @return bool `true` if the read/write position is at the end of the stream and
+     * if no more data is available to be read, or `false` otherwise.
+     */
+    public function stream_eof()
+    {
+        return $this->_download !== null
+            ? ($this->_pointerOffset >= $this->_download->getSize())
+            : true;
+    }
+
+    /**
+     * Opens file.
+     * This method is called immediately after the wrapper is initialized (f.e. by `fopen()` and `file_get_contents()`).
+     * @see fopen()
+     * @param string $path specifies the URL that was passed to the original function.
+     * @param string $mode mode used to open the file, as detailed for `fopen()`.
+     * @param int $options additional flags set by the streams API.
+     * @param string $openedPath real opened path.
+     * @return bool whether operation is successful.
+     */
+    public function stream_open($path, $mode, $options, &$openedPath)
+    {
+        if ($options & STREAM_USE_PATH) {
+            $openedPath = $path;
+        }
+
+        $this->parsePath($path);
+
+        switch ($mode) {
+            case 'r':
+                return $this->prepareDownload();
+            case 'w':
+                return $this->prepareUpload();
+        }
+        return false;
+    }
+
+    /**
+     * Reads from stream.
+     * This method is called in response to `fread()` and `fgets()`.
+     * @see fread()
+     * @param int $count count of bytes of data from the current position should be returned.
+     * @return string|false if there are less than count bytes available, return as many as are available.
+     * If no more data is available, return `false`.
+     */
+    public function stream_read($count)
+    {
+        if ($this->_download === null) {
+            return false;
+        }
+        $result = $this->_download->substr($this->_pointerOffset, $count);
+        $this->_pointerOffset += $count;
+        return $result;
+    }
+
+    /**
+     * Writes to stream.
+     * This method is called in response to `fwrite()`.
+     * @see fwrite()
+     * @param string $data string to be stored into the underlying stream.
+     * @return int the number of bytes that were successfully stored.
+     */
+    public function stream_write($data)
+    {
+        if ($this->_upload === null) {
+            return false;
+        }
+        $this->_upload->addContent($data);
+        $result = StringHelper::byteLength($data);
+        $this->_pointerOffset += $result;
+        return $result;
+    }
+
+    /**
+     * This method is called in response to `fflush()` and when the stream is being closed
+     * while any unflushed data has been written to it before.
+     * @see fflush()
+     * @return bool whether cached data was successfully stored.
+     */
+    public function stream_flush()
+    {
+        return true;
+    }
+
+    /**
+     * Retrieve information about a file resource.
+     * This method is called in response to `stat()`.
+     * @see stat()
+     * @return array file statistic information.
+     */
+    public function stream_stat()
+    {
+        $statistics = $this->fileStatisticsTemplate();
+
+        if ($this->_download !== null) {
+            $statistics[7] = $statistics['size'] = $this->_download->getSize();
+        }
+        if ($this->_upload !== null) {
+            $statistics[7] = $statistics['size'] = $this->_pointerOffset;
+        }
+
+        return $statistics;
+    }
+    
+    /**
+     * Seeks to specific location in a stream.
+     * This method is called in response to `fseek()`.
+     * @see fseek()
+     * @param int $offset The stream offset to seek to.
+     * @param int $whence
+     * Possible values:
+     *
+     * - SEEK_SET - Set position equal to offset bytes.
+     * - SEEK_CUR - Set position to current location plus offset.
+     * - SEEK_END - Set position to end-of-file plus offset.
+     *
+     * @return bool Return true if the position was updated, false otherwise.
+     */
+    public function stream_seek($offset, $whence = SEEK_SET)
+    {
+        switch ($whence) {
+            case SEEK_SET:
+                if ($offset < $this->_download->getSize() && $offset >= 0) {
+                    $this->_pointerOffset = $offset;
+                    return true;
+                }
+                return false;
+            case SEEK_CUR:
+                if ($offset >= 0) {
+                    $this->_pointerOffset += $offset;
+                    return true;
+                }
+                return false;
+            case SEEK_END:
+                if ($this->_download->getSize() + $offset >= 0) {
+                    $this->_pointerOffset = $this->_download->getSize() + $offset;
+                    return true;
+                }
+                return false;
+        }
+        return false;
+    }
+    
+    /**
+     * Retrieve the current position of a stream.
+     * This method is called in response to `fseek()` to determine the current position.
+     * @see fseek()
+     * @return int Should return the current position of the stream.
+     */
+    public function stream_tell()
+    {
+        return $this->_pointerOffset;
+    }
+}

+ 280 - 0
vendor/yiisoft/yii2-mongodb/src/file/Upload.php

@@ -0,0 +1,280 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\file;
+
+use MongoDB\BSON\Binary;
+use MongoDB\BSON\ObjectID;
+use MongoDB\BSON\UTCDatetime;
+use MongoDB\Driver\Exception\InvalidArgumentException;
+use yii\base\InvalidParamException;
+use yii\base\BaseObject;
+use yii\helpers\StringHelper;
+
+/**
+ * Upload represents the GridFS upload operation.
+ *
+ * An `Upload` object is usually created by calling [[Collection::createUpload()]].
+ *
+ * Note: instance of this class is 'single use' only. Do not attempt to use same `Upload` instance for
+ * multiple file upload.
+ *
+ * Usage example:
+ *
+ * ```php
+ * $document = Yii::$app->mongodb->getFileCollection()->createUpload()
+ *     ->addContent('Part 1')
+ *     ->addContent('Part 2')
+ *     // ...
+ *     ->complete();
+ * ```
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.1
+ */
+class Upload extends BaseObject
+{
+    /**
+     * @var Collection file collection to be used.
+     */
+    public $collection;
+    /**
+     * @var string filename to be used for file storage.
+     */
+    public $filename;
+    /**
+     * @var array additional file document contents.
+     * Common GridFS columns:
+     *
+     * - metadata: array, additional data associated with the file.
+     * - aliases: array, an array of aliases.
+     * - contentType: string, content type to be stored with the file.
+     */
+    public $document = [];
+    /**
+     * @var int chunk size in bytes.
+     */
+    public $chunkSize = 261120;
+    /**
+     * @var int total upload length in bytes.
+     */
+    public $length = 0;
+    /**
+     * @var int file chunk counts.
+     */
+    public $chunkCount = 0;
+
+    /**
+     * @var ObjectID file document ID.
+     */
+    private $_documentId;
+    /**
+     * @var resource has context for collecting md5 hash
+     */
+    private $_hashContext;
+    /**
+     * @var string internal data buffer
+     */
+    private $_buffer;
+    /**
+     * @var bool indicates whether upload is complete or not.
+     */
+    private $_isComplete = false;
+
+
+    /**
+     * Destructor.
+     * Makes sure abandoned upload is cancelled.
+     */
+    public function __destruct()
+    {
+        if (!$this->_isComplete) {
+            $this->cancel();
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function init()
+    {
+        $this->_hashContext = hash_init('md5');
+
+        if (isset($this->document['_id'])) {
+            if ($this->document['_id'] instanceof ObjectID) {
+                $this->_documentId = $this->document['_id'];
+            } else {
+                try {
+                    $this->_documentId = new ObjectID($this->document['_id']);
+                } catch (InvalidArgumentException $e) {
+                    // invalid id format
+                    $this->_documentId = $this->document['_id'];
+                }
+            }
+        } else {
+            $this->_documentId = new ObjectID();
+        }
+
+        $this->collection->ensureIndexes();
+    }
+
+    /**
+     * Adds string content to the upload.
+     * This method can invoked several times before [[complete()]] is called.
+     * @param string $content binary content.
+     * @return $this self reference.
+     */
+    public function addContent($content)
+    {
+        $freeBufferLength = $this->chunkSize - StringHelper::byteLength($this->_buffer);
+        $contentLength = StringHelper::byteLength($content);
+        if ($contentLength > $freeBufferLength) {
+            $this->_buffer .= StringHelper::byteSubstr($content, 0, $freeBufferLength);
+            $this->flushBuffer(true);
+            return $this->addContent(StringHelper::byteSubstr($content, $freeBufferLength));
+        } else {
+            $this->_buffer .= $content;
+            $this->flushBuffer();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Adds stream content to the upload.
+     * This method can invoked several times before [[complete()]] is called.
+     * @param resource $stream data source stream.
+     * @return $this self reference.
+     */
+    public function addStream($stream)
+    {
+        while (!feof($stream)) {
+            $freeBufferLength = $this->chunkSize - StringHelper::byteLength($this->_buffer);
+
+            $streamChunk = fread($stream, $freeBufferLength);
+            if ($streamChunk === false) {
+                break;
+            }
+            $this->_buffer .= $streamChunk;
+            $this->flushBuffer();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Adds a file content to the upload.
+     * This method can invoked several times before [[complete()]] is called.
+     * @param string $filename source file name.
+     * @return $this self reference.
+     */
+    public function addFile($filename)
+    {
+        if ($this->filename === null) {
+            $this->filename = basename($filename);
+        }
+
+        $stream = fopen($filename, 'r+');
+        if ($stream === false) {
+            throw new InvalidParamException("Unable to read file '{$filename}'");
+        }
+        return $this->addStream($stream);
+    }
+
+    /**
+     * Completes upload.
+     * @return array saved document.
+     */
+    public function complete()
+    {
+        $this->flushBuffer(true);
+
+        $document = $this->insertFile();
+
+        $this->_isComplete = true;
+
+        return $document;
+    }
+
+    /**
+     * Cancels the upload.
+     */
+    public function cancel()
+    {
+        $this->_buffer = null;
+
+        $this->collection->getChunkCollection()->remove(['files_id' => $this->_documentId], ['limit' => 0]);
+        $this->collection->remove(['_id' => $this->_documentId], ['limit' => 1]);
+
+        $this->_isComplete = true;
+    }
+
+    /**
+     * Flushes [[buffer]] to the chunk if it is full.
+     * @param bool $force whether to enforce flushing.
+     */
+    private function flushBuffer($force = false)
+    {
+        if ($this->_buffer === null) {
+            return;
+        }
+
+        if ($force || StringHelper::byteLength($this->_buffer) == $this->chunkSize) {
+            $this->insertChunk($this->_buffer);
+            $this->_buffer = null;
+        }
+    }
+
+    /**
+     * Inserts file chunk.
+     * @param string $data chunk binary content.
+     */
+    private function insertChunk($data)
+    {
+        $chunkDocument = [
+            'files_id' => $this->_documentId,
+            'n' => $this->chunkCount,
+            'data' => new Binary($data, Binary::TYPE_GENERIC),
+        ];
+
+        hash_update($this->_hashContext, $data);
+
+        $this->collection->getChunkCollection()->insert($chunkDocument);
+        $this->length += StringHelper::byteLength($data);
+        $this->chunkCount++;
+    }
+
+    /**
+     * Inserts [[document]] into file collection.
+     * @return array inserted file document data.
+     */
+    private function insertFile()
+    {
+        $fileDocument = [
+            '_id' => $this->_documentId,
+            'uploadDate' => new UTCDateTime(round(microtime(true) * 1000)),
+        ];
+        if ($this->filename === null) {
+            $fileDocument['filename'] = $this->_documentId . '.dat';
+        } else {
+            $fileDocument['filename'] = $this->filename;
+        }
+
+        $fileDocument = array_merge(
+            $fileDocument,
+            $this->document,
+            [
+                'chunkSize' => $this->chunkSize,
+                'length' => $this->length,
+                'md5' => hash_final($this->_hashContext),
+            ]
+        );
+
+        $this->collection->insert($fileDocument);
+        return $fileDocument;
+    }
+}

+ 283 - 0
vendor/yiisoft/yii2-mongodb/src/gii/model/Generator.php

@@ -0,0 +1,283 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\gii\model;
+
+use Yii;
+use yii\helpers\ArrayHelper;
+use yii\mongodb\ActiveRecord;
+use yii\mongodb\Connection;
+use yii\gii\CodeFile;
+use yii\helpers\Inflector;
+
+/**
+ * This generator will generate ActiveRecord class for the specified MongoDB collection.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class Generator extends \yii\gii\Generator
+{
+    public $db = 'mongodb';
+    public $ns = 'app\models';
+    public $collectionName;
+    public $databaseName;
+    public $attributeList;
+    public $modelClass;
+    public $baseClass = 'yii\mongodb\ActiveRecord';
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'MongoDB Model Generator';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDescription()
+    {
+        return 'This generator generates an ActiveRecord class for the specified MongoDB collection.';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return array_merge(parent::rules(), [
+            [['db', 'ns', 'collectionName', 'databaseName', 'attributeList', 'modelClass', 'baseClass'], 'filter', 'filter' => 'trim'],
+            [['ns'], 'filter', 'filter' => function($value) { return trim($value, '\\'); }],
+
+            [['db', 'ns', 'collectionName', 'baseClass'], 'required'],
+            [['db', 'modelClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
+            [['ns', 'baseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'],
+            [['collectionName'], 'match', 'pattern' => '/^[^$ ]+$/', 'message' => 'Collection name can not contain spaces or "$" symbols.'],
+            [['databaseName'], 'match', 'pattern' => '/^[^\\/\\\\\\. "*:?\\|<>]+$/', 'message' => 'Database name can not contain spaces or any of "/\."*<>:|?" symbols.'],
+            [['db'], 'validateDb'],
+            [['ns'], 'validateNamespace'],
+            [['collectionName'], 'validateCollectionName'],
+            [['attributeList'], 'match', 'pattern' => '/^(\w+\,[ ]*)*([\w]+)$/', 'message' => 'Attributes should contain only word characters, and should be separated by coma.'],
+            [['modelClass'], 'validateModelClass', 'skipOnEmpty' => false],
+            [['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]],
+            [['enableI18N'], 'boolean'],
+            [['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false],
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels()
+    {
+        return array_merge(parent::attributeLabels(), [
+            'ns' => 'Namespace',
+            'db' => 'MongoDB Connection ID',
+            'collectionName' => 'Collection Name',
+            'databaseName' => 'Database Name',
+            'modelClass' => 'Model Class',
+            'baseClass' => 'Base Class',
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function hints()
+    {
+        return array_merge(parent::hints(), [
+            'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>',
+            'db' => 'This is the ID of the MongoDB application component.',
+            'collectionName' => 'This is the name of the MongoDB collection that the new ActiveRecord class is associated with, e.g. <code>post</code>.',
+            'databaseName' => 'This is the name of the MongoDB database, which contains the collection that the new ActiveRecord class is associated with.
+                You may leave this field blank, if your application uses single MongoDB database.',
+            'attributeList' => 'List of the collection attribute names separated by coma.
+                You do not need to specify "_id" attribute here - it will be added automatically.',
+            'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
+                the namespace part as it is specified in "Namespace". You may leave this field blank - in this case class name
+                will be generated automatically.',
+            'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function autoCompleteData()
+    {
+        $db = $this->getDbConnection();
+        if ($db !== null) {
+            return [
+                'collectionName' => function () use ($db) {
+                    $collections = $db->getDatabase()->createCommand()->listCollections();
+                    return ArrayHelper::getColumn($collections, 'name');
+                },
+            ];
+        }
+        return [];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function requiredTemplates()
+    {
+        return ['model.php'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function stickyAttributes()
+    {
+        return array_merge(parent::stickyAttributes(), ['ns', 'db', 'baseClass']);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function generate()
+    {
+        $files = [];
+        $collectionName = $this->collectionName;
+
+        $attributes = ['_id'];
+        if (!empty($this->attributeList)) {
+            $customAttributes = explode(',', $this->attributeList);
+            $customAttributes = array_map('trim', $customAttributes);
+            $attributes = array_merge(['_id'], $customAttributes);
+        }
+
+        $className = $this->generateClassName($collectionName);
+        $params = [
+            'collectionName' => $collectionName,
+            'className' => $className,
+            'attributes' => $attributes,
+            'labels' => $this->generateLabels($attributes),
+            'rules' => $this->generateRules($attributes),
+        ];
+        $files[] = new CodeFile(
+            Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php',
+            $this->render('model.php', $params)
+        );
+
+        return $files;
+    }
+
+    /**
+     * Generates the attribute labels for the specified attributes list.
+     * @param array $attributes the list of attributes
+     * @return array the generated attribute labels (name => label)
+     */
+    public function generateLabels($attributes)
+    {
+        $labels = [];
+        foreach ($attributes as $attribute) {
+            if (!strcasecmp($attribute, '_id')) {
+                $label = 'ID';
+            } else {
+                $label = Inflector::camel2words($attribute);
+                if (substr_compare($label, ' id', -3, 3, true) === 0) {
+                    $label = substr($label, 0, -3) . ' ID';
+                }
+            }
+            $labels[$attribute] = $label;
+        }
+
+        return $labels;
+    }
+
+    /**
+     * Generates validation rules for the specified collection.
+     * @param array $attributes the list of attributes
+     * @return array the generated validation rules
+     */
+    public function generateRules($attributes)
+    {
+        $rules = [];
+        $safeAttributes = [];
+        foreach ($attributes as $attribute) {
+            if ($attribute == '_id') {
+                continue;
+            }
+            $safeAttributes[] = $attribute;
+        }
+        if (!empty($safeAttributes)) {
+            $rules[] = "[['" . implode("', '", $safeAttributes) . "'], 'safe']";
+        }
+        return $rules;
+    }
+
+    /**
+     * Validates the [[db]] attribute.
+     */
+    public function validateDb()
+    {
+        if (!Yii::$app->has($this->db)) {
+            $this->addError('db', 'There is no application component named "' . $this->db . '".');
+        } elseif (!Yii::$app->get($this->db) instanceof Connection) {
+            $this->addError('db', 'The "' . $this->db . '" application component must be a MongoDB connection instance.');
+        }
+    }
+
+    /**
+     * Validates the [[ns]] attribute.
+     */
+    public function validateNamespace()
+    {
+        $this->ns = ltrim($this->ns, '\\');
+        $path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
+        if ($path === false) {
+            $this->addError('ns', 'Namespace must be associated with an existing directory.');
+        }
+    }
+
+    /**
+     * Validates the [[modelClass]] attribute.
+     */
+    public function validateModelClass()
+    {
+        if ($this->isReservedKeyword($this->modelClass)) {
+            $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
+        }
+    }
+
+    /**
+     * Validates the [[collectionName]] attribute.
+     */
+    public function validateCollectionName()
+    {
+        if (empty($this->modelClass)) {
+            $class = $this->generateClassName($this->collectionName);
+            if ($this->isReservedKeyword($class)) {
+                $this->addError('collectionName', "Collection '{$this->collectionName}' will generate a class which is a reserved PHP keyword.");
+            }
+        }
+    }
+
+    /**
+     * Generates a class name from the specified collection name.
+     * @param string $collectionName the collection name (which may contain schema prefix)
+     * @return string the generated class name
+     */
+    protected function generateClassName($collectionName)
+    {
+        $className = preg_replace('/[^\\w]+/is', '_', $collectionName);
+        return Inflector::id2camel($className, '_');
+    }
+
+    /**
+     * @return Connection the DB connection as specified by [[db]].
+     */
+    protected function getDbConnection()
+    {
+        return Yii::$app->get($this->db, false);
+    }
+}

+ 83 - 0
vendor/yiisoft/yii2-mongodb/src/gii/model/default/model.php

@@ -0,0 +1,83 @@
+<?php
+/**
+ * This is the template for generating the model class of a specified collection.
+ */
+
+/* @var $this yii\web\View */
+/* @var $generator yii\mongodb\gii\model\Generator */
+/* @var $collectionName string full collection name */
+/* @var $attributes array list of attribute names */
+/* @var $className string class name */
+/* @var $labels string[] list of attribute labels (name => label) */
+/* @var $rules string[] list of validation rules */
+
+echo "<?php\n";
+?>
+
+namespace <?= $generator->ns ?>;
+
+use Yii;
+
+/**
+ * This is the model class for collection "<?= $collectionName ?>".
+ *
+<?php foreach ($attributes as $attribute): ?>
+ * @property <?= $attribute == '_id' ? '\MongoDB\BSON\ObjectID|string' : 'mixed' ?> <?= "\${$attribute}\n" ?>
+<?php endforeach; ?>
+ */
+class <?= $className ?> extends <?= '\\' . ltrim($generator->baseClass, '\\') . "\n" ?>
+{
+    /**
+     * {@inheritdoc}
+     */
+    public static function collectionName()
+    {
+<?php if (empty($generator->databaseName)): ?>
+        return '<?= $collectionName ?>';
+<?php else: ?>
+        return ['<?= $generator->databaseName ?>', '<?= $collectionName ?>'];
+<?php endif; ?>
+    }
+<?php if ($generator->db !== 'mongodb'): ?>
+
+    /**
+     * @return \yii\mongodb\Connection the MongoDB connection used by this AR class.
+     */
+    public static function getDb()
+    {
+        return Yii::$app->get('<?= $generator->db ?>');
+    }
+<?php endif; ?>
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributes()
+    {
+        return [
+<?php foreach ($attributes as $attribute): ?>
+            <?= "'$attribute',\n" ?>
+<?php endforeach; ?>
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function rules()
+    {
+        return [<?= "\n            " . implode(",\n            ", $rules) . "\n        " ?>];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function attributeLabels()
+    {
+        return [
+<?php foreach ($labels as $name => $label): ?>
+            <?= "'$name' => " . $generator->generateString($label) . ",\n" ?>
+<?php endforeach; ?>
+        ];
+    }
+}

+ 14 - 0
vendor/yiisoft/yii2-mongodb/src/gii/model/form.php

@@ -0,0 +1,14 @@
+<?php
+/* @var $this yii\web\View */
+/* @var $form yii\widgets\ActiveForm */
+/* @var $generator yii\mongodb\gii\model\Generator */
+
+echo $form->field($generator, 'collectionName');
+echo $form->field($generator, 'databaseName');
+echo $form->field($generator, 'attributeList');
+echo $form->field($generator, 'modelClass');
+echo $form->field($generator, 'ns');
+echo $form->field($generator, 'baseClass');
+echo $form->field($generator, 'db');
+echo $form->field($generator, 'enableI18N')->checkbox();
+echo $form->field($generator, 'messageCategory');

+ 215 - 0
vendor/yiisoft/yii2-mongodb/src/i18n/MongoDbMessageSource.php

@@ -0,0 +1,215 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\i18n;
+
+use yii\base\InvalidConfigException;
+use yii\caching\Cache;
+use yii\di\Instance;
+use yii\i18n\MessageSource;
+use yii\mongodb\Connection;
+use yii\mongodb\Query;
+
+/**
+ * MongoDbMessageSource extends [[MessageSource]] and represents a message source that stores translated
+ * messages in MongoDB collection.
+ *
+ * This message source uses single collection for the message translations storage, defined via [[collection]].
+ * Each entry in this collection should have 3 fields:
+ *
+ * - language: string, translation language
+ * - category: string, name translation category
+ * - messages: array, list of actual message translations, in each element: the 'message' key is raw message name
+ *   and 'translation' key - message translation.
+ *
+ * For example:
+ *
+ * ```json
+ * {
+ *     "category": "app",
+ *     "language": "de",
+ *     "messages": {
+ *         {
+ *             "message": "Hello world!",
+ *             "translation": "Hallo Welt!"
+ *         },
+ *         {
+ *             "message": "The dog runs fast.",
+ *             "translation": "Der Hund rennt schnell.",
+ *         },
+ *         ...
+ *     },
+ * }
+ * ```
+ *
+ * You also can specify 'messages' using source message as a direct BSON key, while its value holds the translation.
+ * For example:
+ *
+ * ```json
+ * {
+ *     "category": "app",
+ *     "language": "de",
+ *     "messages": {
+ *         "Hello world!": "Hallo Welt!",
+ *         "See more": "Mehr sehen",
+ *         ...
+ *     },
+ * }
+ * ```
+ *
+ * However such approach is not recommended as BSON keys can not contain symbols like `.` or `$`.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0.5
+ */
+class MongoDbMessageSource extends MessageSource
+{
+    /**
+     * @var Connection|array|string the MongoDB connection object or the application component ID of the MongoDB connection.
+     *
+     * After the MongoDbMessageSource object is created, if you want to change this property, you should only assign
+     * it with a MongoDB connection object.
+     *
+     * This can also be a configuration array for creating the object.
+     */
+    public $db = 'mongodb';
+    /**
+     * @var Cache|array|string the cache object or the application component ID of the cache object.
+     * The messages data will be cached using this cache object.
+     * Note, that to enable caching you have to set [[enableCaching]] to `true`, otherwise setting this property has no effect.
+     *
+     * After the MongoDbMessageSource object is created, if you want to change this property, you should only assign
+     * it with a cache object.
+     *
+     * This can also be a configuration array for creating the object.
+     * @see cachingDuration
+     * @see enableCaching
+     */
+    public $cache = 'cache';
+    /**
+     * @var string|array the name of the MongoDB collection, which stores translated messages.
+     * This collection is better to be pre-created with fields 'category' and 'language' indexed.
+     */
+    public $collection = 'message';
+    /**
+     * @var int the time in seconds that the messages can remain valid in cache.
+     * Use 0 to indicate that the cached data will never expire.
+     * @see enableCaching
+     */
+    public $cachingDuration = 0;
+    /**
+     * @var bool whether to enable caching translated messages
+     */
+    public $enableCaching = false;
+
+
+    /**
+     * Initializes the DbMessageSource component.
+     * This method will initialize the [[db]] property to make sure it refers to a valid DB connection.
+     * Configured [[cache]] component would also be initialized.
+     * @throws InvalidConfigException if [[db]] is invalid or [[cache]] is invalid.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->db = Instance::ensure($this->db, Connection::className());
+        if ($this->enableCaching) {
+            $this->cache = Instance::ensure($this->cache, Cache::className());
+        }
+    }
+
+    /**
+     * Loads the message translation for the specified language and category.
+     * If translation for specific locale code such as `en-US` isn't found it
+     * tries more generic `en`.
+     *
+     * @param string $category the message category
+     * @param string $language the target language
+     * @return array the loaded messages. The keys are original messages, and the values
+     * are translated messages.
+     */
+    protected function loadMessages($category, $language)
+    {
+        if ($this->enableCaching) {
+            $key = [
+                __CLASS__,
+                $category,
+                $language,
+            ];
+            $messages = $this->cache->get($key);
+            if ($messages === false) {
+                $messages = $this->loadMessagesFromDb($category, $language);
+                $this->cache->set($key, $messages, $this->cachingDuration);
+            }
+
+            return $messages;
+        }
+
+        return $this->loadMessagesFromDb($category, $language);
+    }
+
+    /**
+     * Loads the messages from MongoDB.
+     * You may override this method to customize the message storage in the MongoDB.
+     * @param string $category the message category.
+     * @param string $language the target language.
+     * @return array the messages loaded from database.
+     */
+    protected function loadMessagesFromDb($category, $language)
+    {
+        $fallbackLanguage = substr($language, 0, 2);
+        $fallbackSourceLanguage = substr($this->sourceLanguage, 0, 2);
+
+        $languages = [
+            $language,
+            $fallbackLanguage,
+            $fallbackSourceLanguage
+        ];
+
+        $rows = (new Query())
+            ->select(['language', 'messages'])
+            ->from($this->collection)
+            ->andWhere(['category' => $category])
+            ->andWhere(['language' => array_unique($languages)])
+            ->all($this->db);
+
+        if (count($rows) > 1) {
+            $languagePriorities = [
+                $language => 1
+            ];
+            $languagePriorities[$fallbackLanguage] = 2; // language key may be already taken
+            $languagePriorities[$fallbackSourceLanguage] = 3; // language key may be already taken
+
+            usort($rows, function ($a, $b) use ($languagePriorities) {
+                $languageA = $a['language'];
+                $languageB = $b['language'];
+
+                if ($languageA === $languageB) {
+                    return 0;
+                }
+                if ($languagePriorities[$languageA] < $languagePriorities[$languageB]) {
+                    return +1;
+                }
+                return -1;
+            });
+        }
+
+        $messages = [];
+        foreach ($rows as $row) {
+            foreach ($row['messages'] as $key => $value) {
+                // @todo drop message as key specification at 2.2
+                if (is_array($value)) {
+                    $messages[$value['message']] = $value['translation'];
+                } else {
+                    $messages[$key] = $value;
+                }
+            }
+        }
+
+        return $messages;
+    }
+}

+ 79 - 0
vendor/yiisoft/yii2-mongodb/src/log/MongoDbTarget.php

@@ -0,0 +1,79 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\log;
+
+use yii\base\InvalidConfigException;
+use yii\di\Instance;
+use yii\helpers\VarDumper;
+use yii\log\Target;
+use yii\mongodb\Connection;
+
+/**
+ * MongoDbTarget stores log messages in a MongoDB collection.
+ *
+ * By default, MongoDbTarget stores the log messages in a MongoDB collection named 'log'.
+ * The collection can be changed by setting the [[logCollection]] property.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0
+ */
+class MongoDbTarget extends Target
+{
+    /**
+     * @var Connection|string the MongoDB connection object or the application component ID of the MongoDB connection.
+     * After the MongoDbTarget object is created, if you want to change this property, you should only assign it
+     * with a MongoDB connection object.
+     */
+    public $db = 'mongodb';
+    /**
+     * @var string|array the name of the MongoDB collection that stores the session data.
+     * Please refer to [[Connection::getCollection()]] on how to specify this parameter.
+     * This collection is better to be pre-created with fields 'id' and 'expire' indexed.
+     */
+    public $logCollection = 'log';
+
+
+    /**
+     * Initializes the MongoDbTarget component.
+     * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection.
+     * @throws InvalidConfigException if [[db]] is invalid.
+     */
+    public function init()
+    {
+        parent::init();
+        $this->db = Instance::ensure($this->db, Connection::className());
+    }
+
+    /**
+     * Stores log messages to MongoDB collection.
+     */
+    public function export()
+    {
+        $rows = [];
+        foreach ($this->messages as $message) {
+            list($text, $level, $category, $timestamp) = $message;
+            if (!is_string($text)) {
+                // exceptions may not be serializable if in the call stack somewhere is a Closure
+                if ($text instanceof \Throwable || $text instanceof \Exception) {
+                    $text = (string) $text;
+                } else {
+                    $text = VarDumper::export($text);
+                }
+            }
+            $rows[] = [
+                'level' => $level,
+                'category' => $category,
+                'log_time' => $timestamp,
+                'prefix' => $this->getMessagePrefix($message),
+                'message' => $text,
+            ];
+        }
+
+        $this->db->getCollection($this->logCollection)->batchInsert($rows);
+    }
+}

File diff suppressed because it is too large
+ 1078 - 0
vendor/yiisoft/yii2-mongodb/src/rbac/MongoDbManager.php


+ 22 - 0
vendor/yiisoft/yii2-mongodb/src/rbac/Permission.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\rbac;
+
+/**
+ * Permission is a special version of [[\yii\rbac\Permission]] dedicated to MongoDB RBAC implementation.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0.5
+ */
+class Permission extends \yii\rbac\Permission
+{
+    /**
+     * @var array|null list of parent item names.
+     */
+    public $parents;
+}

+ 22 - 0
vendor/yiisoft/yii2-mongodb/src/rbac/Role.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\rbac;
+
+/**
+ * Role is a special version of [[\yii\rbac\Role]] dedicated to MongoDB RBAC implementation.
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0.5
+ */
+class Role extends \yii\rbac\Role
+{
+    /**
+     * @var array|null list of parent item names.
+     */
+    public $parents;
+}

+ 84 - 0
vendor/yiisoft/yii2-mongodb/src/validators/MongoDateValidator.php

@@ -0,0 +1,84 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\validators;
+
+use MongoDB\BSON\UTCDateTime;
+use yii\validators\DateValidator;
+
+/**
+ * MongoDateValidator is an enhanced version of [[DateValidator]], which supports [[\MongoDate]] values.
+ *
+ * Usage example:
+ *
+ * ```php
+ * class Customer extends yii\mongodb\ActiveRecord
+ * {
+ *     ...
+ *     public function rules()
+ *     {
+ *         return [
+ *             ['date', 'yii\mongodb\validators\MongoDateValidator', 'format' => 'MM/dd/yyyy']
+ *         ];
+ *     }
+ * }
+ * ```
+ *
+ * @see DateValidator
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0.4
+ */
+class MongoDateValidator extends DateValidator
+{
+    /**
+     * @var string the name of the attribute to receive the parsing result as [[\MongoDate]] instance.
+     * When this property is not null and the validation is successful, the named attribute will
+     * receive the parsing result as [[\MongoDate]] instance.
+     *
+     * This can be the same attribute as the one being validated. If this is the case,
+     * the original value will be overwritten with the value after successful validation.
+     */
+    public $mongoDateAttribute;
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function validateAttribute($model, $attribute)
+    {
+        $mongoDateAttribute = $this->mongoDateAttribute;
+        if ($this->timestampAttribute === null) {
+            $this->timestampAttribute = $mongoDateAttribute;
+        }
+
+        $originalErrorCount = count($model->getErrors($attribute));
+        parent::validateAttribute($model, $attribute);
+        $afterValidateErrorCount = count($model->getErrors($attribute));
+
+        if ($originalErrorCount === $afterValidateErrorCount) {
+            if ($this->mongoDateAttribute !== null) {
+                $timestamp = $model->{$this->timestampAttribute};
+                $mongoDateAttributeValue = $model->{$this->mongoDateAttribute};
+                // ensure "dirty attributes" support :
+                if (!($mongoDateAttributeValue instanceof UTCDateTime) || $mongoDateAttributeValue->sec !== $timestamp) {
+                    $model->{$this->mongoDateAttribute} = new UTCDateTime($timestamp * 1000);
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function parseDateValue($value)
+    {
+        return $value instanceof UTCDateTime
+            ? $value->toDateTime()->getTimestamp()
+            : parent::parseDateValue($value);
+    }
+}

+ 115 - 0
vendor/yiisoft/yii2-mongodb/src/validators/MongoIdValidator.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace yii\mongodb\validators;
+
+use MongoDB\BSON\ObjectID;
+use yii\base\InvalidConfigException;
+use yii\validators\Validator;
+use Yii;
+
+/**
+ * MongoIdValidator verifies if the attribute is a valid Mongo ID.
+ * Attribute will be considered as valid, if it is an instance of [[\MongoId]] or a its string value.
+ *
+ * Usage example:
+ *
+ * ```php
+ * class Customer extends yii\mongodb\ActiveRecord
+ * {
+ *     ...
+ *     public function rules()
+ *     {
+ *         return [
+ *             ['_id', 'yii\mongodb\validators\MongoIdValidator']
+ *         ];
+ *     }
+ * }
+ * ```
+ *
+ * This validator may also serve as a filter, allowing conversion of Mongo ID value either to the plain string
+ * or to [[\MongoId]] instance. You can enable this feature via [[forceFormat]].
+ *
+ * @author Paul Klimov <klimov.paul@gmail.com>
+ * @since 2.0.4
+ */
+class MongoIdValidator extends Validator
+{
+    /**
+     * @var string|null specifies the format, which validated attribute value should be converted to
+     * in case validation was successful.
+     * valid values are:
+     * - 'string' - enforce value converted to plain string.
+     * - 'object' - enforce value converted to [[\MongoId]] instance.
+     *   If not set - no conversion will be performed, leaving attribute value intact.
+     */
+    public $forceFormat;
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function init()
+    {
+        parent::init();
+        if ($this->message === null) {
+            $this->message = Yii::t('yii', '{attribute} is invalid.');
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function validateAttribute($model, $attribute)
+    {
+        $value = $model->$attribute;
+        $mongoId = $this->parseMongoId($value);
+        if (is_object($mongoId)) {
+            if ($this->forceFormat !== null) {
+                switch ($this->forceFormat) {
+                    case 'string' : {
+                        $model->$attribute = $mongoId->__toString();
+                        break;
+                    }
+                    case 'object' : {
+                        $model->$attribute = $mongoId;
+                        break;
+                    }
+                    default: {
+                        throw new InvalidConfigException("Unrecognized format '{$this->forceFormat}'");
+                    }
+                }
+            }
+        } else {
+            $this->addError($model, $attribute, $this->message, []);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function validateValue($value)
+    {
+        return is_object($this->parseMongoId($value)) ? null : [$this->message, []];
+    }
+
+    /**
+     * @param mixed $value
+     * @return ObjectID|null
+     */
+    private function parseMongoId($value)
+    {
+        if ($value instanceof ObjectID) {
+            return $value;
+        }
+        try {
+            return new ObjectID($value);
+        } catch (\Exception $e) {
+            return null;
+        }
+    }
+}

+ 27 - 0
vendor/yiisoft/yii2-mongodb/src/views/migration.php

@@ -0,0 +1,27 @@
+<?php
+/**
+ * This view is used by console/controllers/MigrateController.php
+ * The following variables are available in this view:
+ */
+
+/* @var $className string the new migration class name */
+echo "<?php\n";
+if (!empty($namespace)) {
+    echo "\nnamespace {$namespace};\n";
+}
+?>
+
+class <?= $className ?> extends \yii\mongodb\Migration
+{
+    public function up()
+    {
+
+    }
+
+    public function down()
+    {
+        echo "<?= $className ?> cannot be reverted.\n";
+
+        return false;
+    }
+}