This commit is contained in:
2020-10-06 14:27:47 +07:00
commit 586be80cf6
16613 changed files with 3274099 additions and 0 deletions

159
vendor/yiisoft/yii2-mongodb/CHANGELOG.md vendored Normal file
View File

@@ -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
vendor/yiisoft/yii2-mongodb/LICENSE.md vendored Normal file
View File

@@ -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
vendor/yiisoft/yii2-mongodb/README.md vendored Normal file
View File

@@ -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
vendor/yiisoft/yii2-mongodb/UPGRADE.md vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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