Browse Source

Merge pull request #1438 from klimov-paul/mongo

MongoDB integration
tags/2.0.0-beta
Qiang Xue 11 years ago
parent
commit
60d9e04cdb
  1. 107
      extensions/mongo/ActiveQuery.php
  2. 353
      extensions/mongo/ActiveRecord.php
  3. 22
      extensions/mongo/ActiveRelation.php
  4. 899
      extensions/mongo/Collection.php
  5. 253
      extensions/mongo/Connection.php
  6. 172
      extensions/mongo/Database.php
  7. 25
      extensions/mongo/Exception.php
  8. 32
      extensions/mongo/LICENSE.md
  9. 344
      extensions/mongo/Query.php
  10. 116
      extensions/mongo/README.md
  11. 28
      extensions/mongo/composer.json
  12. 107
      extensions/mongo/file/ActiveQuery.php
  13. 340
      extensions/mongo/file/ActiveRecord.php
  14. 22
      extensions/mongo/file/ActiveRelation.php
  15. 186
      extensions/mongo/file/Collection.php
  16. 75
      extensions/mongo/file/Query.php
  17. 3
      framework/yii/db/ActiveRelationTrait.php
  18. 16
      tests/unit/data/ar/mongo/ActiveRecord.php
  19. 32
      tests/unit/data/ar/mongo/Customer.php
  20. 27
      tests/unit/data/ar/mongo/CustomerOrder.php
  21. 16
      tests/unit/data/ar/mongo/file/ActiveRecord.php
  22. 27
      tests/unit/data/ar/mongo/file/CustomerFile.php
  23. 5
      tests/unit/data/config.php
  24. 91
      tests/unit/extensions/mongo/ActiveDataProviderTest.php
  25. 246
      tests/unit/extensions/mongo/ActiveRecordTest.php
  26. 83
      tests/unit/extensions/mongo/ActiveRelationTest.php
  27. 313
      tests/unit/extensions/mongo/CollectionTest.php
  28. 119
      tests/unit/extensions/mongo/ConnectionTest.php
  29. 70
      tests/unit/extensions/mongo/DatabaseTest.php
  30. 149
      tests/unit/extensions/mongo/MongoTestCase.php
  31. 132
      tests/unit/extensions/mongo/QueryRunTest.php
  32. 97
      tests/unit/extensions/mongo/QueryTest.php
  33. 323
      tests/unit/extensions/mongo/file/ActiveRecordTest.php
  34. 98
      tests/unit/extensions/mongo/file/CollectionTest.php
  35. 70
      tests/unit/extensions/mongo/file/QueryTest.php

107
extensions/mongo/ActiveQuery.php

@ -0,0 +1,107 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
/**
* ActiveQuery represents a Mongo query associated with an 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:
*
* ~~~
* $customers = Customer::find()->with('orders')->asArray()->all();
* ~~~
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ActiveQuery extends Query implements ActiveQueryInterface
{
use ActiveQueryTrait;
/**
* 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 the query results. If the query results in nothing, an empty array will be returned.
*/
public function all($db = null)
{
$cursor = $this->buildCursor($db);
$rows = $this->fetchRows($cursor);
if (!empty($rows)) {
$models = $this->createModels($rows);
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
return $models;
} else {
return [];
}
}
/**
* 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) {
if ($this->asArray) {
$model = $row;
} else {
/** @var ActiveRecord $class */
$class = $this->modelClass;
$model = $class::create($row);
}
if (!empty($this->with)) {
$models = [$model];
$this->findWith($this->with, $models);
$model = $models[0];
}
return $model;
} else {
return null;
}
}
/**
* Returns the Mongo collection for this query.
* @param Connection $db Mongo connection.
* @return Collection collection instance.
*/
public function getCollection($db = null)
{
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
if ($db === null) {
$db = $modelClass::getDb();
}
if ($this->from === null) {
$this->from = $modelClass::collectionName();
}
return $db->getCollection($this->from);
}
}

353
extensions/mongo/ActiveRecord.php

@ -0,0 +1,353 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\db\BaseActiveRecord;
use yii\base\UnknownMethodException;
use yii\db\StaleObjectException;
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 "mongo" 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->getComponent('mongo');
}
/**
* 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:
*
* ~~~
* 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 integer 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,
*
* ~~~
* 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 integer 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:
*
* ~~~
* 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 integer the number of documents deleted.
*/
public static function deleteAll($condition = [], $options = [])
{
$options['w'] = 1;
if (!array_key_exists('multiple', $options)) {
$options['multiple'] = true;
}
return static::getCollection()->remove($condition, $options);
}
/**
* Creates an [[ActiveQuery]] instance.
* This method is called by [[find()]] to start a "find" command.
* You may override this method to return a customized query (e.g. `CustomerQuery` specified
* written for querying `Customer` purpose.)
* @return ActiveQuery the newly created [[ActiveQuery]] instance.
*/
public static function createQuery()
{
return new ActiveQuery(['modelClass' => 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 table 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'];
}
/**
* Creates an [[ActiveRelation]] instance.
* This method is called by [[hasOne()]] and [[hasMany()]] to create a relation instance.
* You may override this method to return a customized relation.
* @param array $config the configuration passed to the ActiveRelation class.
* @return ActiveRelation the newly created [[ActiveRelation]] instance.
*/
public static function createActiveRelation($config = [])
{
return new ActiveRelation($config);
}
/**
* 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:
* ~~~
* public function attributes()
* {
* return ['_id', 'name', 'address', 'status'];
* }
* ~~~
* @return array list of attribute names.
*/
public function attributes()
{
throw new InvalidConfigException('The attributes() method of mongo 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:
*
* ~~~
* $customer = new Customer;
* $customer->name = $name;
* $customer->email = $email;
* $customer->insert();
* ~~~
*
* @param boolean $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 boolean 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) {
$values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null;
}
}
$collection = static::getCollection();
$newId = $collection->insert($values);
$this->setAttribute('_id', $newId);
foreach ($values as $name => $value) {
$this->setOldAttribute($name, $value);
}
$this->afterSave(true);
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);
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.');
}
foreach ($values as $name => $value) {
$this->setOldAttribute($name, $this->getAttribute($name));
}
$this->afterSave(false);
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 integer|boolean 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 table 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 boolean 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() && $this->getPrimaryKey() === $record->getPrimaryKey();
}
}

22
extensions/mongo/ActiveRelation.php

@ -0,0 +1,22 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
use yii\db\ActiveRelationInterface;
use yii\db\ActiveRelationTrait;
/**
* ActiveRelation represents a relation to Mongo Active Record class.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
{
use ActiveRelationTrait;
}

899
extensions/mongo/Collection.php

@ -0,0 +1,899 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
use yii\base\InvalidParamException;
use yii\base\Object;
use Yii;
use yii\helpers\Json;
/**
* 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:
*
* ~~~
* $collection = Yii::$app->mongo->getCollection('customer');
* $collection->insert(['name' => 'John Smith', 'status' => 1]);
* ~~~
*
* To perform "find" queries, please use [[Query]] instead.
*
* Mongo uses JSON format to specify query conditions with quite specific syntax.
* However Collection class provides the ability of "translating" common condition format used "yii\db\*"
* into Mongo condition.
* For example:
* ~~~
* $condition = [
* [
* 'OR',
* ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']],
* ['status' => [1, 2, 3]]
* ],
* ];
* print_r($collection->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 [[\MongoId]] instance,
* even if they are plain strings. However if you have other columns, containing [[\MongoId]], you
* should take care of possible typecast on your own.
*
* @property string $name name of this collection. This property is read-only.
* @property string $fullName full name of this collection, including database name. This property is read-only.
* @property array $lastError last error information. This property is read-only.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Collection extends Object
{
/**
* @var \MongoCollection Mongo collection instance.
*/
public $mongoCollection;
/**
* @return string name of this collection.
*/
public function getName()
{
return $this->mongoCollection->getName();
}
/**
* @return string full name of this collection, including database name.
*/
public function getFullName()
{
return $this->mongoCollection->__toString();
}
/**
* @return array last error information.
*/
public function getLastError()
{
return $this->mongoCollection->db->lastError();
}
/**
* Composes log/profile token.
* @param string $command command name
* @param array $arguments command arguments.
* @return string token.
*/
protected function composeLogToken($command, $arguments = [])
{
$parts = [];
foreach ($arguments as $argument) {
$parts[] = is_scalar($argument) ? $argument : Json::encode($argument);
}
return $this->getFullName() . '.' . $command . '(' . implode(', ', $parts) . ')';
}
/**
* Drops this collection.
* @throws Exception on failure.
* @return boolean whether the operation successful.
*/
public function drop()
{
$token = $this->composeLogToken('drop');
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->drop();
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return true;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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:
* ~~~
* [
* 'name',
* 'status' => -1,
* ]
* ~~~
* @param array $options list of options in format: optionName => optionValue.
* @throws Exception on failure.
* @return boolean whether the operation successful.
*/
public function createIndex($columns, $options = [])
{
if (!is_array($columns)) {
$columns = [$columns];
}
$keys = $this->normalizeIndexKeys($columns);
$token = $this->composeLogToken('createIndex', [$keys, $options]);
$options = array_merge(['w' => 1], $options);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->ensureIndex($keys, $options);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return true;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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:
* ~~~
* [
* 'name',
* 'status' => -1,
* 'description' => 'text',
* ]
* ~~~
* @throws Exception on failure.
* @return boolean whether the operation successful.
*/
public function dropIndex($columns)
{
if (!is_array($columns)) {
$columns = [$columns];
}
$keys = $this->normalizeIndexKeys($columns);
$token = $this->composeLogToken('dropIndex', [$keys]);
Yii::info($token, __METHOD__);
try {
$result = $this->mongoCollection->deleteIndex($keys);
$this->tryResultError($result);
return true;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Compose index keys from given columns/keys list.
* @param array $columns raw columns/keys list.
* @return array normalizes index keys array.
*/
protected function normalizeIndexKeys($columns)
{
$keys = [];
foreach ($columns as $key => $value) {
if (is_numeric($key)) {
$keys[$value] = \MongoCollection::ASCENDING;
} else {
$keys[$key] = $value;
}
}
return $keys;
}
/**
* Drops all indexes for this collection.
* @throws Exception on failure.
* @return integer count of dropped indexes.
*/
public function dropAllIndexes()
{
$token = $this->composeLogToken('dropIndexes');
Yii::info($token, __METHOD__);
try {
$result = $this->mongoCollection->deleteIndexes();
$this->tryResultError($result);
return $result['nIndexesWas'];
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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
* @return \MongoCursor cursor for the search results
* @see Query
*/
public function find($condition = [], $fields = [])
{
return $this->mongoCollection->find($this->buildCondition($condition), $fields);
}
/**
* Inserts new data into collection.
* @param array|object $data data to be inserted.
* @param array $options list of options in format: optionName => optionValue.
* @return \MongoId new record id instance.
* @throws Exception on failure.
*/
public function insert($data, $options = [])
{
$token = $this->composeLogToken('insert', [$data]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$options = array_merge(['w' => 1], $options);
$this->tryResultError($this->mongoCollection->insert($data, $options));
Yii::endProfile($token, __METHOD__);
return is_array($data) ? $data['_id'] : $data->_id;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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 = [])
{
$token = $this->composeLogToken('batchInsert', [$rows]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$options = array_merge(['w' => 1], $options);
$this->tryResultError($this->mongoCollection->batchInsert($rows, $options));
Yii::endProfile($token, __METHOD__);
return $rows;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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 $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 integer|boolean number of updated documents or whether operation was successful.
* @throws Exception on failure.
*/
public function update($condition, $newData, $options = [])
{
$condition = $this->buildCondition($condition);
$options = array_merge(['w' => 1, 'multiple' => true], $options);
if ($options['multiple']) {
$keys = array_keys($newData);
if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) {
$newData = ['$set' => $newData];
}
}
$token = $this->composeLogToken('update', [$condition, $newData, $options]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->update($condition, $newData, $options);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
if (is_array($result) && array_key_exists('n', $result)) {
return $result['n'];
} else {
return true;
}
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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 = [])
{
$token = $this->composeLogToken('save', [$data]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$options = array_merge(['w' => 1], $options);
$this->tryResultError($this->mongoCollection->save($data, $options));
Yii::endProfile($token, __METHOD__);
return is_array($data) ? $data['_id'] : $data->_id;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Removes data from the collection.
* @param array $condition description of records to remove.
* @param array $options list of options in format: optionName => optionValue.
* @return integer|boolean number of updated documents or whether operation was successful.
* @throws Exception on failure.
*/
public function remove($condition = [], $options = [])
{
$condition = $this->buildCondition($condition);
$options = array_merge(['w' => 1, 'multiple' => true], $options);
$token = $this->composeLogToken('remove', [$condition, $options]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->remove($condition, $options);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
if (is_array($result) && array_key_exists('n', $result)) {
return $result['n'];
} else {
return true;
}
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Returns a list of distinct values for the given column across a collection.
* @param string $column column to use.
* @param array $condition query parameters.
* @return array|boolean array of distinct values, or "false" on failure.
* @throws Exception on failure.
*/
public function distinct($column, $condition = [])
{
$condition = $this->buildCondition($condition);
$token = $this->composeLogToken('distinct', [$column, $condition]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->distinct($column, $condition);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Performs aggregation using Mongo Aggregation Framework.
* @param array $pipeline list of pipeline operators, or just the first operator
* @param array $pipelineOperator additional pipeline operator. You can specify additional
* pipelines via third argument, fourth argument etc.
* @return array the result of the aggregation.
* @throws Exception on failure.
* @see http://docs.mongodb.org/manual/applications/aggregation/
*/
public function aggregate($pipeline, $pipelineOperator = [])
{
$args = func_get_args();
$token = $this->composeLogToken('aggregate', $args);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = call_user_func_array([$this->mongoCollection, 'aggregate'], $args);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return $result['result'];
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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 [[\MongoCode]] 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 \MongoCode|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 [[\MongoCode]].
* @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.
* @see http://docs.mongodb.org/manual/reference/command/group/
*/
public function group($keys, $initial, $reduce, $options = [])
{
if (!($reduce instanceof \MongoCode)) {
$reduce = new \MongoCode((string)$reduce);
}
if (array_key_exists('condition', $options)) {
$options['condition'] = $this->buildCondition($options['condition']);
}
if (array_key_exists('finalize', $options)) {
if (!($options['finalize'] instanceof \MongoCode)) {
$options['finalize'] = new \MongoCode((string)$options['finalize']);
}
}
$token = $this->composeLogToken('group', [$keys, $initial, $reduce, $options]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
// Avoid possible E_DEPRECATED for $options:
if (empty($options)) {
$result = $this->mongoCollection->group($keys, $initial, $reduce);
} else {
$result = $this->mongoCollection->group($keys, $initial, $reduce, $options);
}
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
if (array_key_exists('retval', $result)) {
return $result['retval'];
} else {
return [];
}
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Performs aggregation using Mongo "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:
*
* ~~~
* $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 \MongoCode|string $map function, which emits map data from collection.
* Argument will be automatically cast to [[\MongoCode]].
* @param \MongoCode|string $reduce function that takes two arguments (the map key
* and the map values) and does the aggregation.
* Argument will be automatically cast to [[\MongoCode]].
* @param string|array $out output collection name. It could be a string for simple output
* ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection'])
* @param array $condition criteria for including a document in the aggregation.
* @return string the map reduce output collection name.
* @throws Exception on failure.
*/
public function mapReduce($map, $reduce, $out, $condition = [])
{
if (!($map instanceof \MongoCode)) {
$map = new \MongoCode((string)$map);
}
if (!($reduce instanceof \MongoCode)) {
$reduce = new \MongoCode((string)$reduce);
}
$command = [
'mapReduce' => $this->getName(),
'map' => $map,
'reduce' => $reduce,
'out' => $out
];
if (!empty($condition)) {
$command['query'] = $this->buildCondition($condition);
}
$token = $this->composeLogToken('mapReduce', [$map, $reduce, $out]);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$command = array_merge(['mapReduce' => $this->getName()], $command);
$result = $this->mongoCollection->db->command($command);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return $result['result'];
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Performs full text search.
* @param string $search string of terms that MongoDB parses and uses to query the text index.
* @param array $condition criteria for filtering a results list.
* @param array $fields list of fields to be returned in result.
* @param integer $limit the maximum number of documents to include in the response (by default 100).
* @param string $language he language that determines the list of stop words for the search
* and the rules for the stemmer and tokenizer. If not specified, the search uses the default
* language of the index.
* @return array the highest scoring documents, in descending order by score.
* @throws Exception on failure.
*/
public function fullTextSearch($search, $condition = [], $fields = [], $limit = null, $language = null) {
$command = [
'search' => $search
];
if (!empty($condition)) {
$command['filter'] = $this->buildCondition($condition);
}
if (!empty($fields)) {
$command['project'] = $fields;
}
if ($limit !== null) {
$command['limit'] = $limit;
}
if ($language !== null) {
$command['language'] = $language;
}
$token = $this->composeLogToken('text', $command);
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$command = array_merge(['text' => $this->getName()], $command);
$result = $this->mongoCollection->db->command($command);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return $result['results'];
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Checks if command execution result ended with an error.
* @param mixed $result raw command execution result.
* @throws Exception if an error occurred.
*/
protected function tryResultError($result)
{
if (is_array($result)) {
if (!empty($result['errmsg'])) {
$errorMessage = $result['errmsg'];
} elseif (!empty($result['err'])) {
$errorMessage = $result['err'];
}
if (isset($errorMessage)) {
if (array_key_exists('code', $result)) {
$errorCode = (int)$result['code'];
} elseif (array_key_exists('ok', $result)) {
$errorCode = (int)$result['ok'];
} else {
$errorCode = 0;
}
throw new Exception($errorMessage, $errorCode);
}
} elseif (!$result) {
throw new Exception('Unknown error, use "w=1" option to enable error tracking');
}
}
/**
* Throws an exception if there was an error on the last operation.
* @throws Exception if an error occurred.
*/
protected function tryLastError()
{
$this->tryResultError($this->getLastError());
}
/**
* Converts user friendly condition keyword into actual Mongo condition keyword.
* @param string $key raw condition key.
* @return string actual key.
*/
protected function normalizeConditionKeyword($key)
{
static $map = [
'OR' => '$or',
'>' => '$gt',
'>=' => '$gte',
'<' => '$lt',
'<=' => '$lte',
'!=' => '$ne',
'<>' => '$ne',
'IN' => '$in',
'NOT IN' => '$nin',
'ALL' => '$all',
'SIZE' => '$size',
'TYPE' => '$type',
'EXISTS' => '$exists',
'NOTEXISTS' => '$exists',
'ELEMMATCH' => '$elemMatch',
'MOD' => '$mod',
'%' => '$mod',
'=' => '$$eq',
'==' => '$$eq',
'WHERE' => '$where'
];
$matchKey = strtoupper($key);
if (array_key_exists($matchKey, $map)) {
return $map[$matchKey];
} else {
return $key;
}
}
/**
* Converts given value into [[MongoId]] instance.
* If array given, each element of it will be processed.
* @param mixed $rawId raw id(s).
* @return array|\MongoId 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 \MongoId) {
return $rawId;
} else {
$rawId = (string)$rawId;
}
}
return new \MongoId($rawId);
}
/**
* 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 = [
'AND' => 'buildAndCondition',
'OR' => 'buildOrCondition',
'BETWEEN' => 'buildBetweenCondition',
'NOT BETWEEN' => 'buildBetweenCondition',
'IN' => 'buildInCondition',
'NOT IN' => 'buildInCondition',
'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];
array_shift($condition);
return $this->$method($operator, $condition);
} else {
throw new InvalidParamException('Found unknown operator in query: ' . $operator);
}
} else {
// 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) {
$name = $this->normalizeConditionKeyword($name);
if (strncmp('$', $name, 1) === 0) {
// Native Mongo condition:
$result[$name] = $value;
} else {
if (is_array($value)) {
if (array_key_exists(0, $value)) {
// Quick IN condition:
$result = array_merge($result, $this->buildInCondition('IN', [$name, $value]));
} else {
// Normalize possible verbose condition:
$actualValue = [];
foreach ($value as $k => $v) {
$actualValue[$this->normalizeConditionKeyword($k)] = $v;
}
$result[$name] = $actualValue;
}
} else {
// Direct match:
if ($name == '_id') {
$value = $this->ensureMongoId($value);
}
$result[$name] = $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)
{
$result = [];
foreach ($operands as $operand) {
$condition = $this->buildCondition($operand);
$result = array_merge_recursive($result, $condition);
}
return $result;
}
/**
* 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,
]
];
} else {
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;
if (!is_array($column)) {
$columns = [$column];
$values = [$column => $values];
} elseif (count($column) < 2) {
$columns = $column;
$values = [$column[0] => $values];
} else {
$columns = $column;
}
$operator = $this->normalizeConditionKeyword($operator);
$result = [];
foreach ($columns as $column) {
if ($column == '_id') {
$inValues = $this->ensureMongoId($values[$column]);
} else {
$inValues = $values[$column];
}
$result[$column][$operator] = $inValues;
}
return $result;
}
/**
* 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;
return [$column => '/' . $value . '/'];
}
}

253
extensions/mongo/Connection.php

@ -0,0 +1,253 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
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:
*
* ~~~
* $connection = new \yii\mongo\Connection([
* 'dsn' => $dsn,
* ]);
* $connection->open();
* ~~~
*
* After the Mongo connection is established, one can access Mongo databases and collections:
*
* ~~~
* $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:
*
* ~~~
* // 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:
*
* ~~~
* [
* 'components' => [
* 'mongo' => [
* 'class' => '\yii\mongo\Connection',
* 'dsn' => 'mongodb://developer:password@localhost:27017/mydatabase',
* ],
* ],
* ]
* ~~~
*
* @property boolean $isActive Whether the Mongo connection is established. This property is read-only.
* is read-only.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Connection extends Component
{
/**
* @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:
* ~~~
* [
* 'persist' => true, // use persistent connection
* 'socketTimeoutMS' => 1000, // how long a send or receive on a socket can take before timing out
* 'journal' => true // block write operations until the journal be flushed the to disk
* ]
* ~~~
*/
public $options = [];
/**
* @var string name of the Mongo database to use by default.
* If this field left blank, connection instance will attempt to determine it from
* [[options]] and [[dsn]] automatically, if needed.
*/
public $defaultDatabaseName;
/**
* @var \MongoClient mongo client instance.
*/
public $mongoClient;
/**
* @var Database[] list of Mongo databases
*/
private $_databases = [];
/**
* Returns the Mongo collection with the given name.
* @param string|null $name collection name, if null default one will be used.
* @param boolean $refresh whether to reload the table schema even if it is found in the cache.
* @return Database database instance.
*/
public function getDatabase($name = null, $refresh = false)
{
if ($name === null) {
$name = $this->fetchDefaultDatabaseName();
}
if ($refresh || !array_key_exists($name, $this->_databases)) {
$this->_databases[$name] = $this->selectDatabase($name);
}
return $this->_databases[$name];
}
/**
* Returns [[defaultDatabaseName]] value, 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.
*/
protected function fetchDefaultDatabaseName()
{
if ($this->defaultDatabaseName === null) {
if (isset($this->options['db'])) {
$this->defaultDatabaseName = $this->options['db'];
} elseif (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;
}
/**
* Selects the database with given name.
* @param string $name database name.
* @return Database database instance.
*/
protected function selectDatabase($name)
{
$this->open();
return Yii::createObject([
'class' => 'yii\mongo\Database',
'mongoDb' => $this->mongoClient->selectDB($name)
]);
}
/**
* Returns the Mongo 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 boolean $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);
} else {
return $this->getDatabase()->getCollection($name, $refresh);
}
}
/**
* Returns the Mongo 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 boolean $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);
} else {
return $this->getDatabase()->getFileCollection($prefix, $refresh);
}
}
/**
* Returns a value indicating whether the Mongo connection is established.
* @return boolean whether the Mongo connection is established
*/
public function getIsActive()
{
return is_object($this->mongoClient) && $this->mongoClient->connected;
}
/**
* Establishes a Mongo connection.
* It does nothing if a Mongo connection has already been established.
* @throws Exception if connection fails
*/
public function open()
{
if ($this->mongoClient === null) {
if (empty($this->dsn)) {
throw new InvalidConfigException($this->className() . '::dsn cannot be empty.');
}
$token = 'Opening Mongo connection: ' . $this->dsn;
try {
Yii::trace($token, __METHOD__);
Yii::beginProfile($token, __METHOD__);
$options = $this->options;
$options['connect'] = true;
if ($this->defaultDatabaseName !== null) {
$options['db'] = $this->defaultDatabaseName;
}
$this->mongoClient = new \MongoClient($this->dsn, $options);
Yii::endProfile($token, __METHOD__);
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
}
/**
* Closes the currently active DB connection.
* It does nothing if the connection is already closed.
*/
public function close()
{
if ($this->mongoClient !== null) {
Yii::trace('Closing Mongo connection: ' . $this->dsn, __METHOD__);
$this->mongoClient = null;
$this->_databases = [];
}
}
}

172
extensions/mongo/Database.php

@ -0,0 +1,172 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
use yii\base\Object;
use Yii;
use yii\helpers\Json;
/**
* Database represents the Mongo database information.
*
* @property string $name name of this database. This property is read-only.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Database extends Object
{
/**
* @var \MongoDB Mongo database instance.
*/
public $mongoDb;
/**
* @var Collection[] list of collections.
*/
private $_collections = [];
/**
* @var file\Collection[] list of GridFS collections.
*/
private $_fileCollections = [];
/**
* @return string name of this database.
*/
public function getName()
{
return $this->mongoDb->__toString();
}
/**
* Returns the Mongo collection with the given name.
* @param string $name collection name
* @param boolean $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 boolean $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\mongo\Collection',
'mongoCollection' => $this->mongoDb->selectCollection($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\mongo\file\Collection',
'mongoCollection' => $this->mongoDb->getGridFS($prefix)
]);
}
/**
* 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 \MongoCollection new mongo collection instance.
* @throws Exception on failure.
*/
public function createCollection($name, $options = [])
{
$token = $this->getName() . '.create(' . $name . ', ' . Json::encode($options) . ')';
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoDb->createCollection($name, $options);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Executes Mongo command.
* @param array $command command specification.
* @param array $options options in format: "name" => "value"
* @return array database response.
* @throws Exception on failure.
*/
public function executeCommand($command, $options = [])
{
$token = $this->getName() . '.$cmd(' . Json::encode($command) . ', ' . Json::encode($options) . ')';
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoDb->command($command, $options);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Checks if command execution result ended with an error.
* @param mixed $result raw command execution result.
* @throws Exception if an error occurred.
*/
protected function tryResultError($result)
{
if (is_array($result)) {
if (!empty($result['errmsg'])) {
$errorMessage = $result['errmsg'];
} elseif (!empty($result['err'])) {
$errorMessage = $result['err'];
}
if (isset($errorMessage)) {
if (array_key_exists('ok', $result)) {
$errorCode = (int)$result['ok'];
} else {
$errorCode = 0;
}
throw new Exception($errorMessage, $errorCode);
}
} elseif (!$result) {
throw new Exception('Unknown error, use "w=1" option to enable error tracking');
}
}
}

25
extensions/mongo/Exception.php

@ -0,0 +1,25 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
/**
* 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 \Yii::t('yii', 'Mongo Exception');
}
}

32
extensions/mongo/LICENSE.md

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

344
extensions/mongo/Query.php

@ -0,0 +1,344 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo;
use yii\base\Component;
use yii\db\QueryInterface;
use yii\db\QueryTrait;
use yii\helpers\Json;
use Yii;
/**
* 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,
*
* ~~~
* $query = new Query;
* // compose the query
* $query->select(['name', 'status'])
* ->from('customer')
* ->limit(10);
* // execute the query
* $rows = $query->all();
* ~~~
*
* @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']`.
* The "_id" field is always returned. If not set, if 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;
/**
* 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->getComponent('mongo');
}
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 static 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 static the query object itself.
*/
public function from($collection)
{
$this->from = $collection;
return $this;
}
/**
* Builds the Mongo cursor for this query.
* @param Connection $db the database connection used to execute the query.
* @return \MongoCursor mongo cursor instance.
*/
protected function buildCursor($db = null)
{
if ($this->where === null) {
$where = [];
} else {
$where = $this->where;
}
$selectFields = [];
if (!empty($this->select)) {
foreach ($this->select as $fieldName) {
$selectFields[$fieldName] = true;
}
}
$cursor = $this->getCollection($db)->find($where, $selectFields);
if (!empty($this->orderBy)) {
$sort = [];
foreach ($this->orderBy as $fieldName => $sortOrder) {
$sort[$fieldName] = $sortOrder === SORT_DESC ? \MongoCollection::DESCENDING : \MongoCollection::ASCENDING;
}
$cursor->sort($sort);
}
$cursor->limit($this->limit);
$cursor->skip($this->offset);
return $cursor;
}
/**
* Fetches rows from the given Mongo cursor.
* @param \MongoCursor $cursor Mongo cursor instance to fetch data from.
* @param boolean $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|boolean result.
*/
protected function fetchRows($cursor, $all = true, $indexBy = null)
{
$token = 'find(' . Json::encode($cursor->info()) . ')';
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->fetchRowsInternal($cursor, $all, $indexBy);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* @param \MongoCursor $cursor Mongo cursor instance to fetch data from.
* @param boolean $all whether to fetch all rows or only first one.
* @param string|callable $indexBy value to index by.
* @return array|boolean result.
* @see Query::fetchRows()
*/
protected function fetchRowsInternal($cursor, $all, $indexBy)
{
$result = [];
if ($all) {
foreach ($cursor as $row) {
if ($indexBy !== null) {
if (is_string($indexBy)) {
$key = $row[$indexBy];
} else {
$key = call_user_func($indexBy, $row);
}
$result[$key] = $row;
} else {
$result[] = $row;
}
}
} else {
if ($cursor->hasNext()) {
$result = $cursor->getNext();
} else {
$result = false;
}
}
return $result;
}
/**
* 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 `mongo` 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)
{
$cursor = $this->buildCursor($db);
return $this->fetchRows($cursor, true, $this->indexBy);
}
/**
* 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 `mongo` application component will be used.
* @return array|boolean 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)
{
$cursor = $this->buildCursor($db);
return $this->fetchRows($cursor, false);
}
/**
* 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 `mongo` application component will be used.
* @return integer number of records
* @throws Exception on failure.
*/
public function count($q = '*', $db = null)
{
$cursor = $this->buildCursor($db);
$token = 'find.count(' . Json::encode($cursor->info()) . ')';
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $cursor->count();
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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 `mongo` application component will be used.
* @return boolean whether the query result contains any row of data.
*/
public function exists($db = null)
{
return $this->one($db) !== null;
}
/**
* 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 `mongo` application component will be used.
* @return integer the sum of the specified column values
*/
public function sum($q, $db = null)
{
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 `mongo` application component will be used.
* @return integer the average of the specified column values.
*/
public function average($q, $db = null)
{
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 database connection used to generate the SQL statement.
* If this parameter is not given, the `db` application component will be used.
* @return integer 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 Mongo connection used to execute the query.
* If this parameter is not given, the `mongo` application component will be used.
* @return integer 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 integer aggregation result.
*/
protected function aggregate($column, $operator, $db)
{
$collection = $this->getCollection($db);
$pipelines = [];
if ($this->where !== null) {
$pipelines[] = ['$match' => $collection->buildCondition($this->where)];
}
$pipelines[] = [
'$group' => [
'_id' => '1',
'total' => [
'$' . $operator => '$' . $column
],
]
];
$result = $collection->aggregate($pipelines);
if (array_key_exists(0, $result)) {
return $result[0]['total'];
} else {
return 0;
}
}
/**
* Returns a list of distinct values for the given column across a collection.
* @param string $q column to use.
* @param Connection $db the Mongo connection used to execute the query.
* If this parameter is not given, the `mongo` application component will be used.
* @return array array of distinct values
*/
public function distinct($q, $db = null)
{
$collection = $this->getCollection($db);
if ($this->where !== null) {
$condition = $this->where;
} else {
$condition = [];
}
$result = $collection->distinct($q, $condition);
if ($result === false) {
return [];
} else {
return $result;
}
}
}

116
extensions/mongo/README.md

@ -0,0 +1,116 @@
Yii 2.0 Public Preview - MongoDb Extension
==========================================
Thank you for choosing Yii - a high-performance component-based PHP framework.
If you are looking for a production-ready PHP framework, please use
[Yii v1.1](https://github.com/yiisoft/yii).
Yii 2.0 is still under heavy development. We may make significant changes
without prior notices. **Yii 2.0 is not ready for production use yet.**
[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2)
This is the yii2-sphinx extension.
Installation
------------
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
Either run
```
php composer.phar require yiisoft/yii2-mongo "*"
```
or add
```
"yiisoft/yii2-mongo": "*"
```
to the require section of your composer.json.
*Note: You might have to run `php composer.phar selfupdate`*
Usage & Documentation
---------------------
This extension adds [MongoDB](http://www.mongodb.org/) data storage support for the Yii2 framework.
Note: extension requires [MongoDB PHP Extension](http://us1.php.net/manual/en/book.mongo.php) version 1.3.0 or higher.
To use this extension, simply add the following code in your application configuration:
```php
return [
//....
'components' => [
'mongo' => [
'class' => '\yii\mongo\Connection',
'dsn' => 'mongodb://developer:password@localhost:27017/mydatabase',
],
],
];
```
This extension provides ActiveRecord solution similar ot the [[\yii\db\ActiveRecord]].
To declare an ActiveRecord class you need to extend [[\yii\mongo\ActiveRecord]] and
implement the `collectionName` and 'attributes' methods:
```php
use yii\mongo\ActiveRecord;
class Customer extends ActiveRecord
{
/**
* @return string the name of the index associated with this ActiveRecord class.
*/
public static function collectionName()
{
return 'customer';
}
/**
* @return array list of attribute names.
*/
public function attributes()
{
return ['name', 'email', 'address', 'status'];
}
}
```
You can use [[\yii\data\ActiveDataProvider]] with the [[\yii\mongo\Query]] and [[\yii\mongo\ActiveQuery]]:
```php
use yii\data\ActiveDataProvider;
use yii\mongo\Query;
$query = new Query;
$query->from('customer')->where(['status' => 2]);
$provider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 10,
]
]);
$models = $provider->getModels();
```
```php
use yii\data\ActiveDataProvider;
use app\models\Customer;
$provider = new ActiveDataProvider([
'query' => Customer::find(),
'pagination' => [
'pageSize' => 10,
]
]);
$models = $provider->getModels();
```
This extension supports [MongoGridFS](http://docs.mongodb.org/manual/core/gridfs/) via
classes at namespace "\yii\mongo\file".

28
extensions/mongo/composer.json

@ -0,0 +1,28 @@
{
"name": "yiisoft/yii2-mongo",
"description": "MongoDb extension for the Yii framework",
"keywords": ["yii", "mongo", "mongodb", "active-record"],
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2/issues?state=open",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
"source": "https://github.com/yiisoft/yii2"
},
"authors": [
{
"name": "Paul Klimov",
"email": "klimov.paul@gmail.com"
}
],
"minimum-stability": "dev",
"require": {
"yiisoft/yii2": "*",
"ext-mongo": ">=1.3.0"
},
"autoload": {
"psr-0": { "yii\\mongo\\": "" }
}
}

107
extensions/mongo/file/ActiveQuery.php

@ -0,0 +1,107 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo\file;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
/**
* 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:
*
* ~~~
* $images = ImageFile::find()->with('tags')->asArray()->all();
* ~~~
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ActiveQuery extends Query implements ActiveQueryInterface
{
use ActiveQueryTrait;
/**
* Executes query and returns all results as an array.
* @param \yii\mongo\Connection $db the Mongo connection used to execute the query.
* If null, the Mongo connection returned by [[modelClass]] 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)
{
$cursor = $this->buildCursor($db);
$rows = $this->fetchRows($cursor);
if (!empty($rows)) {
$models = $this->createModels($rows);
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
return $models;
} else {
return [];
}
}
/**
* Executes query and returns a single row of result.
* @param \yii\mongo\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) {
if ($this->asArray) {
$model = $row;
} else {
/** @var ActiveRecord $class */
$class = $this->modelClass;
$model = $class::create($row);
}
if (!empty($this->with)) {
$models = [$model];
$this->findWith($this->with, $models);
$model = $models[0];
}
return $model;
} else {
return null;
}
}
/**
* Returns the Mongo collection for this query.
* @param \yii\mongo\Connection $db Mongo connection.
* @return Collection collection instance.
*/
public function getCollection($db = null)
{
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
if ($db === null) {
$db = $modelClass::getDb();
}
if ($this->from === null) {
$this->from = $modelClass::collectionName();
}
return $db->getFileCollection($this->from);
}
}

340
extensions/mongo/file/ActiveRecord.php

@ -0,0 +1,340 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo\file;
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:
*
* ~~~
* $record = new ImageFile();
* $record->file = '/path/to/some/file.jpg';
* $record->save();
* ~~~
*
* You can also specify file content via [[newFileContent]] attribute:
*
* ~~~
* $record = new ImageFile();
* $record->newFileContent = 'New file content';
* $record->save();
* ~~~
*
* Note: [[newFileContent]] always takes precedence over [[file]].
*
* @property \MongoId|string $_id primary key.
* @property string $filename name of stored file.
* @property \MongoDate $uploadDate file upload date.
* @property integer $length file size.
* @property integer $chunkSize file chunk size.
* @property string $md5 file md5 hash.
* @property \MongoGridFSFile|\yii\web\UploadedFile|string $file associated file.
* @property string $newFileContent new file content.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
abstract class ActiveRecord extends \yii\mongo\ActiveRecord
{
/**
* Creates an [[ActiveQuery]] instance.
* This method is called by [[find()]] to start a "find" command.
* You may override this method to return a customized query (e.g. `ImageFileQuery` specified
* written for querying `ImageFile` purpose.)
* @return ActiveQuery the newly created [[ActiveQuery]] instance.
*/
public static function createQuery()
{
return new ActiveQuery(['modelClass' => 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());
}
/**
* Creates an [[ActiveRelation]] instance.
* This method is called by [[hasOne()]] and [[hasMany()]] to create a relation instance.
* You may override this method to return a customized relation.
* @param array $config the configuration passed to the ActiveRelation class.
* @return ActiveRelation the newly created [[ActiveRelation]] instance.
*/
public static function createActiveRelation($config = [])
{
return new ActiveRelation($config);
}
/**
* 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:
* ~~~
* 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);
}
$this->setAttribute('_id', $newId);
foreach ($values as $name => $value) {
$this->setOldAttribute($name, $value);
}
$this->afterSave(true);
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);
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)) {
$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.');
}
}
foreach ($values as $name => $value) {
$this->setOldAttribute($name, $this->getAttribute($name));
}
$this->afterSave(false);
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;
} else {
throw new InvalidParamException("File '{$file}' does not exist.");
}
} else {
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 \MongoGridFSFile) {
$fileSize = $file->getSize();
if (empty($fileSize)) {
return null;
} else {
return $file->getBytes();
}
} elseif ($file instanceof UploadedFile) {
return file_get_contents($file->tempName);
} elseif (is_string($file)) {
if (file_exists($file)) {
return file_get_contents($file);
} else {
throw new InvalidParamException("File '{$file}' does not exist.");
}
} else {
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 boolean 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 \MongoGridFSFile) {
return ($file->write($filename) == $file->getSize());
} elseif ($file instanceof UploadedFile) {
return copy($file->tempName, $filename);
} elseif (is_string($file)) {
if (file_exists($file)) {
return copy($file, $filename);
} else {
throw new InvalidParamException("File '{$file}' does not exist.");
}
} else {
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 \MongoGridFSFile) {
return $file->getResource();
} elseif ($file instanceof UploadedFile) {
return fopen($file->tempName, 'r');
} elseif (is_string($file)) {
if (file_exists($file)) {
return fopen($file, 'r');
} else {
throw new InvalidParamException("File '{$file}' does not exist.");
}
} else {
throw new InvalidParamException('Unsupported type of "file" attribute.');
}
}
}

22
extensions/mongo/file/ActiveRelation.php

@ -0,0 +1,22 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo\file;
use yii\db\ActiveRelationInterface;
use yii\db\ActiveRelationTrait;
/**
* ActiveRelation represents a relation to Mongo GridFS Active Record class.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
{
use ActiveRelationTrait;
}

186
extensions/mongo/file/Collection.php

@ -0,0 +1,186 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo\file;
use yii\mongo\Exception;
use Yii;
/**
* 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\mongo\Collection $chunkCollection file chunks Mongo collection. This property is read-only.
* @method \MongoGridFSCursor find() returns a cursor for the search results.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Collection extends \yii\mongo\Collection
{
/**
* @var \MongoGridFS Mongo GridFS collection instance.
*/
public $mongoCollection;
/**
* @var \yii\mongo\Collection file chunks Mongo collection.
*/
private $_chunkCollection;
/**
* Returns the Mongo collection for the file chunks.
* @param boolean $refresh whether to reload the collection instance even if it is found in the cache.
* @return \yii\mongo\Collection mongo collection instance.
*/
public function getChunkCollection($refresh = false)
{
if ($refresh || !is_object($this->_chunkCollection)) {
$this->_chunkCollection = Yii::createObject([
'class' => 'yii\mongo\Collection',
'mongoCollection' => $this->mongoCollection->chunks
]);
}
return $this->_chunkCollection;
}
/**
* Removes data from the collection.
* @param array $condition description of records to remove.
* @param array $options list of options in format: optionName => optionValue.
* @return integer|boolean number of updated documents or whether operation was successful.
* @throws Exception on failure.
*/
public function remove($condition = [], $options = [])
{
$result = parent::remove($condition, $options);
$this->tryLastError(); // MongoGridFS::remove will return even if the remove failed
return $result;
}
/**
* 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 = [])
{
$token = 'Inserting file into ' . $this->getFullName();
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$options = array_merge(['w' => 1], $options);
$result = $this->mongoCollection->storeFile($filename, $metadata, $options);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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 = [])
{
$token = 'Inserting file content into ' . $this->getFullName();
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$options = array_merge(['w' => 1], $options);
$result = $this->mongoCollection->storeBytes($bytes, $metadata, $options);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* 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.
* @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 = [])
{
$token = 'Inserting file uploads into ' . $this->getFullName();
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->storeUpload($name, $metadata);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Retrieves the file with given _id.
* @param mixed $id _id of the file to find.
* @return \MongoGridFSFile|null found file, or null if file does not exist
* @throws Exception on failure.
*/
public function get($id)
{
$token = 'Inserting file uploads into ' . $this->getFullName();
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->get($id);
Yii::endProfile($token, __METHOD__);
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
/**
* Deletes the file with given _id.
* @param mixed $id _id of the file to find.
* @return boolean whether the operation was successful.
* @throws Exception on failure.
*/
public function delete($id)
{
$token = 'Inserting file uploads into ' . $this->getFullName();
Yii::info($token, __METHOD__);
try {
Yii::beginProfile($token, __METHOD__);
$result = $this->mongoCollection->delete($id);
$this->tryResultError($result);
Yii::endProfile($token, __METHOD__);
return true;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
}
}
}

75
extensions/mongo/file/Query.php

@ -0,0 +1,75 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\mongo\file;
use Yii;
use yii\helpers\Json;
use yii\mongo\Exception;
/**
* Query represents Mongo "find" operation for GridFS collection.
*
* Query behaves exactly as regular [[\yii\mongo\Query]].
* Found files will be represented as arrays of file document attributes with
* additional 'file' key, which stores [[\MongoGridFSFile]] instance.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Query extends \yii\mongo\Query
{
/**
* Returns the Mongo collection for this query.
* @param \yii\mongo\Connection $db Mongo connection.
* @return Collection collection instance.
*/
public function getCollection($db = null)
{
if ($db === null) {
$db = Yii::$app->getComponent('mongo');
}
return $db->getFileCollection($this->from);
}
/**
* @param \MongoGridFSCursor $cursor Mongo cursor instance to fetch data from.
* @param boolean $all whether to fetch all rows or only first one.
* @param string|callable $indexBy value to index by.
* @return array|boolean result.
* @see Query::fetchRows()
*/
protected function fetchRowsInternal($cursor, $all, $indexBy)
{
$result = [];
if ($all) {
foreach ($cursor as $file) {
$row = $file->file;
$row['file'] = $file;
if ($indexBy !== null) {
if (is_string($indexBy)) {
$key = $row[$indexBy];
} else {
$key = call_user_func($indexBy, $row);
}
$result[$key] = $row;
} else {
$result[] = $row;
}
}
} else {
if ($cursor->hasNext()) {
$file = $cursor->getNext();
$result = $file->file;
$result['file'] = $file;
} else {
$result = false;
}
}
return $result;
}
}

3
framework/yii/db/ActiveRelationTrait.php

@ -203,7 +203,8 @@ trait ActiveRelationTrait
return serialize($key); return serialize($key);
} else { } else {
$attribute = reset($attributes); $attribute = reset($attributes);
return $model[$attribute]; $key = $model[$attribute];
return is_scalar($key) ? $key : serialize($key);
} }
} }

16
tests/unit/data/ar/mongo/ActiveRecord.php

@ -0,0 +1,16 @@
<?php
namespace yiiunit\data\ar\mongo;
/**
* Test Mongo ActiveRecord
*/
class ActiveRecord extends \yii\mongo\ActiveRecord
{
public static $db;
public static function getDb()
{
return self::$db;
}
}

32
tests/unit/data/ar/mongo/Customer.php

@ -0,0 +1,32 @@
<?php
namespace yiiunit\data\ar\mongo;
class Customer extends ActiveRecord
{
public static function collectionName()
{
return 'customer';
}
public function attributes()
{
return [
'_id',
'name',
'email',
'address',
'status',
];
}
public static function activeOnly($query)
{
$query->andWhere(['status' => 2]);
}
public function getOrders()
{
return $this->hasMany(CustomerOrder::className(), ['customer_id' => '_id']);
}
}

27
tests/unit/data/ar/mongo/CustomerOrder.php

@ -0,0 +1,27 @@
<?php
namespace yiiunit\data\ar\mongo;
class CustomerOrder extends ActiveRecord
{
public static function collectionName()
{
return 'customer_order';
}
public function attributes()
{
return [
'_id',
'number',
'customer_id',
'items',
];
}
public function getCustomer()
{
return $this->hasOne(Customer::className(), ['_id' => 'customer_id']);
}
}

16
tests/unit/data/ar/mongo/file/ActiveRecord.php

@ -0,0 +1,16 @@
<?php
namespace yiiunit\data\ar\mongo\file;
/**
* Test Mongo ActiveRecord
*/
class ActiveRecord extends \yii\mongo\file\ActiveRecord
{
public static $db;
public static function getDb()
{
return self::$db;
}
}

27
tests/unit/data/ar/mongo/file/CustomerFile.php

@ -0,0 +1,27 @@
<?php
namespace yiiunit\data\ar\mongo\file;
class CustomerFile extends ActiveRecord
{
public static function collectionName()
{
return 'customer_fs';
}
public function attributes()
{
return array_merge(
parent::attributes(),
[
'tag',
'status',
]
);
}
public static function activeOnly($query)
{
$query->andWhere(['status' => 2]);
}
}

5
tests/unit/data/config.php

@ -51,5 +51,10 @@ return [
'password' => '', 'password' => '',
'fixture' => __DIR__ . '/sphinx/source.sql', 'fixture' => __DIR__ . '/sphinx/source.sql',
], ],
],
'mongo' => [
'dsn' => 'mongodb://travis:test@localhost:27017',
'defaultDatabaseName' => 'yii2test',
'options' => [],
] ]
]; ];

91
tests/unit/extensions/mongo/ActiveDataProviderTest.php

@ -0,0 +1,91 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\data\ActiveDataProvider;
use yii\mongo\Query;
use yiiunit\data\ar\mongo\ActiveRecord;
use yiiunit\data\ar\mongo\Customer;
/**
* @group mongo
*/
class ActiveDataProviderTest extends MongoTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
$this->setUpTestRows();
}
protected function tearDown()
{
$this->dropCollection(Customer::collectionName());
parent::tearDown();
}
/**
* Sets up test rows.
*/
protected function setUpTestRows()
{
$collection = $this->getConnection()->getCollection('customer');
$rows = [];
for ($i = 1; $i <= 10; $i++) {
$rows[] = [
'name' => 'name' . $i,
'email' => 'email' . $i,
'address' => 'address' . $i,
'status' => $i,
];
}
$collection->batchInsert($rows);
}
// Tests :
public function testQuery()
{
$query = new Query;
$query->from('customer');
$provider = new ActiveDataProvider([
'query' => $query,
'db' => $this->getConnection(),
]);
$models = $provider->getModels();
$this->assertEquals(10, count($models));
$provider = new ActiveDataProvider([
'query' => $query,
'db' => $this->getConnection(),
'pagination' => [
'pageSize' => 5,
]
]);
$models = $provider->getModels();
$this->assertEquals(5, count($models));
}
public function testActiveQuery()
{
$provider = new ActiveDataProvider([
'query' => Customer::find()->orderBy('id ASC'),
]);
$models = $provider->getModels();
$this->assertEquals(10, count($models));
$this->assertTrue($models[0] instanceof Customer);
$keys = $provider->getKeys();
$this->assertTrue($keys[0] instanceof \MongoId);
$provider = new ActiveDataProvider([
'query' => Customer::find(),
'pagination' => [
'pageSize' => 5,
]
]);
$models = $provider->getModels();
$this->assertEquals(5, count($models));
}
}

246
tests/unit/extensions/mongo/ActiveRecordTest.php

@ -0,0 +1,246 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\mongo\ActiveQuery;
use yiiunit\data\ar\mongo\ActiveRecord;
use yiiunit\data\ar\mongo\Customer;
/**
* @group mongo
*/
class ActiveRecordTest extends MongoTestCase
{
/**
* @var array[] list of test rows.
*/
protected $testRows = [];
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
$this->setUpTestRows();
}
protected function tearDown()
{
$this->dropCollection(Customer::collectionName());
parent::tearDown();
}
/**
* Sets up test rows.
*/
protected function setUpTestRows()
{
$collection = $this->getConnection()->getCollection('customer');
$rows = [];
for ($i = 1; $i <= 10; $i++) {
$rows[] = [
'name' => 'name' . $i,
'email' => 'email' . $i,
'address' => 'address' . $i,
'status' => $i,
];
}
$collection->batchInsert($rows);
$this->testRows = $rows;
}
// Tests :
public function testFind()
{
// find one
$result = Customer::find();
$this->assertTrue($result instanceof ActiveQuery);
$customer = $result->one();
$this->assertTrue($customer instanceof Customer);
// find all
$customers = Customer::find()->all();
$this->assertEquals(10, count($customers));
$this->assertTrue($customers[0] instanceof Customer);
$this->assertTrue($customers[1] instanceof Customer);
// find by _id
$testId = $this->testRows[0]['_id'];
$customer = Customer::find($testId);
$this->assertTrue($customer instanceof Customer);
$this->assertEquals($testId, $customer->_id);
// find by column values
$customer = Customer::find(['name' => 'name5']);
$this->assertTrue($customer instanceof Customer);
$this->assertEquals($this->testRows[4]['_id'], $customer->_id);
$this->assertEquals('name5', $customer->name);
$customer = Customer::find(['name' => 'unexisting name']);
$this->assertNull($customer);
// find by attributes
$customer = Customer::find()->where(['status' => 4])->one();
$this->assertTrue($customer instanceof Customer);
$this->assertEquals(4, $customer->status);
// find count, sum, average, min, max, distinct
$this->assertEquals(10, Customer::find()->count());
$this->assertEquals(1, Customer::find()->where(['status' => 2])->count());
$this->assertEquals((1+10)/2*10, Customer::find()->sum('status'));
$this->assertEquals((1+10)/2, Customer::find()->average('status'));
$this->assertEquals(1, Customer::find()->min('status'));
$this->assertEquals(10, Customer::find()->max('status'));
$this->assertEquals(range(1, 10), Customer::find()->distinct('status'));
// scope
$this->assertEquals(1, Customer::find()->activeOnly()->count());
// asArray
$testRow = $this->testRows[2];
$customer = Customer::find()->where(['_id' => $testRow['_id']])->asArray()->one();
$this->assertEquals($testRow, $customer);
// indexBy
$customers = Customer::find()->indexBy('name')->all();
$this->assertTrue($customers['name1'] instanceof Customer);
$this->assertTrue($customers['name2'] instanceof Customer);
// indexBy callable
$customers = Customer::find()->indexBy(function ($customer) {
return $customer->status . '-' . $customer->status;
})->all();
$this->assertTrue($customers['1-1'] instanceof Customer);
$this->assertTrue($customers['2-2'] instanceof Customer);
}
public function testInsert()
{
$record = new Customer;
$record->name = 'new name';
$record->email = 'new email';
$record->address = 'new address';
$record->status = 7;
$this->assertTrue($record->isNewRecord);
$record->save();
$this->assertTrue($record->_id instanceof \MongoId);
$this->assertFalse($record->isNewRecord);
}
/**
* @depends testInsert
*/
public function testUpdate()
{
$record = new Customer;
$record->name = 'new name';
$record->email = 'new email';
$record->address = 'new address';
$record->status = 7;
$record->save();
// save
$record = Customer::find($record->_id);
$this->assertTrue($record instanceof Customer);
$this->assertEquals(7, $record->status);
$this->assertFalse($record->isNewRecord);
$record->status = 9;
$record->save();
$this->assertEquals(9, $record->status);
$this->assertFalse($record->isNewRecord);
$record2 = Customer::find($record->_id);
$this->assertEquals(9, $record2->status);
// updateAll
$pk = ['_id' => $record->_id];
$ret = Customer::updateAll(['status' => 55], $pk);
$this->assertEquals(1, $ret);
$record = Customer::find($pk);
$this->assertEquals(55, $record->status);
}
/**
* @depends testInsert
*/
public function testDelete()
{
// delete
$record = new Customer;
$record->name = 'new name';
$record->email = 'new email';
$record->address = 'new address';
$record->status = 7;
$record->save();
$record = Customer::find($record->_id);
$record->delete();
$record = Customer::find($record->_id);
$this->assertNull($record);
// deleteAll
$record = new Customer;
$record->name = 'new name';
$record->email = 'new email';
$record->address = 'new address';
$record->status = 7;
$record->save();
$ret = Customer::deleteAll(['name' => 'new name']);
$this->assertEquals(1, $ret);
$records = Customer::find()->where(['name' => 'new name'])->all();
$this->assertEquals(0, count($records));
}
public function testUpdateAllCounters()
{
$this->assertEquals(1, Customer::updateAllCounters(['status' => 10], ['status' => 10]));
$record = Customer::find(['status' => 10]);
$this->assertNull($record);
}
/**
* @depends testUpdateAllCounters
*/
public function testUpdateCounters()
{
$record = Customer::find($this->testRows[9]);
$originalCounter = $record->status;
$counterIncrement = 20;
$record->updateCounters(['status' => $counterIncrement]);
$this->assertEquals($originalCounter + $counterIncrement, $record->status);
$refreshedRecord = Customer::find($record->_id);
$this->assertEquals($originalCounter + $counterIncrement, $refreshedRecord->status);
}
/**
* @depends testUpdate
*/
public function testUpdateNestedAttribute()
{
$record = new Customer;
$record->name = 'new name';
$record->email = 'new email';
$record->address = [
'city' => 'SomeCity',
'street' => 'SomeStreet',
];
$record->status = 7;
$record->save();
// save
$record = Customer::find($record->_id);
$newAddress = [
'city' => 'AnotherCity'
];
$record->address = $newAddress;
$record->save();
$record2 = Customer::find($record->_id);
$this->assertEquals($newAddress, $record2->address);
}
}

83
tests/unit/extensions/mongo/ActiveRelationTest.php

@ -0,0 +1,83 @@
<?php
namespace yiiunit\extensions\mongo;
use yiiunit\data\ar\mongo\ActiveRecord;
use yiiunit\data\ar\mongo\Customer;
use yiiunit\data\ar\mongo\CustomerOrder;
/**
* @group mongo
*/
class ActiveRelationTest extends MongoTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
$this->setUpTestRows();
}
protected function tearDown()
{
$this->dropCollection(Customer::collectionName());
$this->dropCollection(CustomerOrder::collectionName());
parent::tearDown();
}
/**
* Sets up test rows.
*/
protected function setUpTestRows()
{
$customerCollection = $this->getConnection()->getCollection('customer');
$customers = [];
for ($i = 1; $i <= 5; $i++) {
$customers[] = [
'name' => 'name' . $i,
'email' => 'email' . $i,
'address' => 'address' . $i,
'status' => $i,
];
}
$customerCollection->batchInsert($customers);
$customerOrderCollection = $this->getConnection()->getCollection('customer_order');
$customerOrders = [];
foreach ($customers as $customer) {
$customerOrders[] = [
'customer_id' => $customer['_id'],
'number' => $customer['status'],
];
$customerOrders[] = [
'customer_id' => $customer['_id'],
'number' => $customer['status'] + 1,
];
}
$customerOrderCollection->batchInsert($customerOrders);
}
// Tests :
public function testFindLazy()
{
/** @var CustomerOrder $order */
$order = CustomerOrder::find(['number' => 2]);
$this->assertFalse($order->isRelationPopulated('customer'));
$index = $order->customer;
$this->assertTrue($order->isRelationPopulated('customer'));
$this->assertTrue($index instanceof Customer);
$this->assertEquals(1, count($order->populatedRelations));
}
public function testFindEager()
{
$orders = CustomerOrder::find()->with('customer')->all();
$this->assertEquals(10, count($orders));
$this->assertTrue($orders[0]->isRelationPopulated('customer'));
$this->assertTrue($orders[1]->isRelationPopulated('customer'));
$this->assertTrue($orders[0]->customer instanceof Customer);
$this->assertTrue($orders[1]->customer instanceof Customer);
}
}

313
tests/unit/extensions/mongo/CollectionTest.php

@ -0,0 +1,313 @@
<?php
namespace yiiunit\extensions\mongo;
/**
* @group mongo
*/
class CollectionTest extends MongoTestCase
{
protected function tearDown()
{
$this->dropCollection('customer');
$this->dropCollection('mapReduceOut');
parent::tearDown();
}
// Tests :
public function testGetName()
{
$collectionName = 'customer';
$collection = $this->getConnection()->getCollection($collectionName);
$this->assertEquals($collectionName, $collection->getName());
$this->assertEquals($this->mongoConfig['defaultDatabaseName'] . '.' . $collectionName, $collection->getFullName());
}
public function testFind()
{
$collection = $this->getConnection()->getCollection('customer');
$cursor = $collection->find();
$this->assertTrue($cursor instanceof \MongoCursor);
}
public function testInsert()
{
$collection = $this->getConnection()->getCollection('customer');
$data = [
'name' => 'customer 1',
'address' => 'customer 1 address',
];
$id = $collection->insert($data);
$this->assertTrue($id instanceof \MongoId);
$this->assertNotEmpty($id->__toString());
}
/**
* @depends testInsert
* @depends testFind
*/
public function testFindAll()
{
$collection = $this->getConnection()->getCollection('customer');
$data = [
'name' => 'customer 1',
'address' => 'customer 1 address',
];
$id = $collection->insert($data);
$cursor = $collection->find();
$rows = [];
foreach ($cursor as $row) {
$rows[] = $row;
}
$this->assertEquals(1, count($rows));
$this->assertEquals($id, $rows[0]['_id']);
}
/**
* @depends testFind
*/
public function testBatchInsert()
{
$collection = $this->getConnection()->getCollection('customer');
$rows = [
[
'name' => 'customer 1',
'address' => 'customer 1 address',
],
[
'name' => 'customer 2',
'address' => 'customer 2 address',
],
];
$insertedRows = $collection->batchInsert($rows);
$this->assertTrue($insertedRows[0]['_id'] instanceof \MongoId);
$this->assertTrue($insertedRows[1]['_id'] instanceof \MongoId);
$this->assertEquals(count($rows), $collection->find()->count());
}
public function testSave()
{
$collection = $this->getConnection()->getCollection('customer');
$data = [
'name' => 'customer 1',
'address' => 'customer 1 address',
];
$id = $collection->save($data);
$this->assertTrue($id instanceof \MongoId);
$this->assertNotEmpty($id->__toString());
}
/**
* @depends testSave
*/
public function testUpdateBySave()
{
$collection = $this->getConnection()->getCollection('customer');
$data = [
'name' => 'customer 1',
'address' => 'customer 1 address',
];
$newId = $collection->save($data);
$updatedId = $collection->save($data);
$this->assertEquals($newId, $updatedId, 'Unable to update data!');
$data['_id'] = $newId->__toString();
$updatedId = $collection->save($data);
$this->assertEquals($newId, $updatedId, 'Unable to updated data by string id!');
}
/**
* @depends testFindAll
*/
public function testRemove()
{
$collection = $this->getConnection()->getCollection('customer');
$data = [
'name' => 'customer 1',
'address' => 'customer 1 address',
];
$id = $collection->insert($data);
$count = $collection->remove(['_id' => $id]);
$this->assertEquals(1, $count);
$rows = $this->findAll($collection);
$this->assertEquals(0, count($rows));
}
/**
* @depends testFindAll
*/
public function testUpdate()
{
$collection = $this->getConnection()->getCollection('customer');
$data = [
'name' => 'customer 1',
'address' => 'customer 1 address',
];
$id = $collection->insert($data);
$newData = [
'name' => 'new name'
];
$count = $collection->update(['_id' => $id], $newData);
$this->assertEquals(1, $count);
list($row) = $this->findAll($collection);
$this->assertEquals($newData['name'], $row['name']);
}
/**
* @depends testBatchInsert
*/
public function testGroup()
{
$collection = $this->getConnection()->getCollection('customer');
$rows = [
[
'name' => 'customer 1',
'address' => 'customer 1 address',
],
[
'name' => 'customer 2',
'address' => 'customer 2 address',
],
];
$collection->batchInsert($rows);
$keys = ['address' => 1];
$initial = ['items' => []];
$reduce = "function (obj, prev) { prev.items.push(obj.name); }";
$result = $collection->group($keys, $initial, $reduce);
$this->assertEquals(2, count($result));
$this->assertNotEmpty($result[0]['address']);
$this->assertNotEmpty($result[0]['items']);
}
/**
* @depends testBatchInsert
*/
public function testMapReduce()
{
$collection = $this->getConnection()->getCollection('customer');
$rows = [
[
'name' => 'customer 1',
'status' => 1,
'amount' => 100,
],
[
'name' => 'customer 2',
'status' => 1,
'amount' => 200,
],
[
'name' => 'customer 2',
'status' => 2,
'amount' => 400,
],
[
'name' => 'customer 2',
'status' => 3,
'amount' => 500,
],
];
$collection->batchInsert($rows);
$result = $collection->mapReduce(
'function () {emit(this.status, this.amount)}',
'function (key, values) {return Array.sum(values)}',
'mapReduceOut',
['status' => ['$lt' => 3]]
);
$this->assertEquals('mapReduceOut', $result);
$outputCollection = $this->getConnection()->getCollection($result);
$rows = $this->findAll($outputCollection);
$expectedRows = [
[
'_id' => 1,
'value' => 300,
],
[
'_id' => 2,
'value' => 400,
],
];
$this->assertEquals($expectedRows, $rows);
}
public function testCreateIndex()
{
$collection = $this->getConnection()->getCollection('customer');
$columns = [
'name',
'status' => \MongoCollection::DESCENDING,
];
$this->assertTrue($collection->createIndex($columns));
$indexInfo = $collection->mongoCollection->getIndexInfo();
$this->assertEquals(2, count($indexInfo));
}
/**
* @depends testCreateIndex
*/
public function testDropIndex()
{
$collection = $this->getConnection()->getCollection('customer');
$collection->createIndex('name');
$this->assertTrue($collection->dropIndex('name'));
$indexInfo = $collection->mongoCollection->getIndexInfo();
$this->assertEquals(1, count($indexInfo));
$this->setExpectedException('\yii\mongo\Exception');
$collection->dropIndex('name');
}
/**
* @depends testCreateIndex
*/
public function testDropAllIndexes()
{
$collection = $this->getConnection()->getCollection('customer');
$collection->createIndex('name');
$this->assertEquals(2, $collection->dropAllIndexes());
$indexInfo = $collection->mongoCollection->getIndexInfo();
$this->assertEquals(1, count($indexInfo));
}
/**
* @depends testBatchInsert
* @depends testCreateIndex
*/
public function testFullTextSearch()
{
if (version_compare('2.4', $this->getServerVersion(), '>')) {
$this->markTestSkipped("Mongo Server 2.4 required.");
}
$collection = $this->getConnection()->getCollection('customer');
$rows = [
[
'name' => 'customer 1',
'status' => 1,
'amount' => 100,
],
[
'name' => 'some customer',
'status' => 1,
'amount' => 200,
],
];
$collection->batchInsert($rows);
$collection->createIndex(['name' => 'text']);
$result = $collection->fullTextSearch('some');
$this->assertNotEmpty($result);
}
}

119
tests/unit/extensions/mongo/ConnectionTest.php

@ -0,0 +1,119 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\mongo\Collection;
use yii\mongo\file\Collection as FileCollection;
use yii\mongo\Connection;
use yii\mongo\Database;
/**
* @group mongo
*/
class ConnectionTest extends MongoTestCase
{
public function testConstruct()
{
$connection = $this->getConnection(false);
$params = $this->mongoConfig;
$connection->open();
$this->assertEquals($params['dsn'], $connection->dsn);
$this->assertEquals($params['defaultDatabaseName'], $connection->defaultDatabaseName);
$this->assertEquals($params['options'], $connection->options);
}
public function testOpenClose()
{
$connection = $this->getConnection(false, false);
$this->assertFalse($connection->isActive);
$this->assertEquals(null, $connection->mongoClient);
$connection->open();
$this->assertTrue($connection->isActive);
$this->assertTrue(is_object($connection->mongoClient));
$connection->close();
$this->assertFalse($connection->isActive);
$this->assertEquals(null, $connection->mongoClient);
$connection = new Connection;
$connection->dsn = 'unknown::memory:';
$this->setExpectedException('yii\mongo\Exception');
$connection->open();
}
public function testGetDatabase()
{
$connection = $this->getConnection();
$database = $connection->getDatabase($connection->defaultDatabaseName);
$this->assertTrue($database instanceof Database);
$this->assertTrue($database->mongoDb instanceof \MongoDB);
$database2 = $connection->getDatabase($connection->defaultDatabaseName);
$this->assertTrue($database === $database2);
$databaseRefreshed = $connection->getDatabase($connection->defaultDatabaseName, true);
$this->assertFalse($database === $databaseRefreshed);
}
/**
* @depends testGetDatabase
*/
public function testGetDefaultDatabase()
{
$connection = new Connection();
$connection->dsn = $this->mongoConfig['dsn'];
$connection->defaultDatabaseName = $this->mongoConfig['defaultDatabaseName'];
$database = $connection->getDatabase();
$this->assertTrue($database instanceof Database, 'Unable to get default database!');
$connection = new Connection();
$connection->dsn = $this->mongoConfig['dsn'];
$connection->options = ['db' => $this->mongoConfig['defaultDatabaseName']];
$database = $connection->getDatabase();
$this->assertTrue($database instanceof Database, 'Unable to determine default database from options!');
$connection = new Connection();
$connection->dsn = $this->mongoConfig['dsn'] . '/' . $this->mongoConfig['defaultDatabaseName'];
$database = $connection->getDatabase();
$this->assertTrue($database instanceof Database, 'Unable to determine default database from dsn!');
}
/**
* @depends testGetDefaultDatabase
*/
public function testGetCollection()
{
$connection = $this->getConnection();
$collection = $connection->getCollection('customer');
$this->assertTrue($collection instanceof Collection);
$collection2 = $connection->getCollection('customer');
$this->assertTrue($collection === $collection2);
$collection2 = $connection->getCollection('customer', true);
$this->assertFalse($collection === $collection2);
}
/**
* @depends testGetDefaultDatabase
*/
public function testGetFileCollection()
{
$connection = $this->getConnection();
$collection = $connection->getFileCollection('testfs');
$this->assertTrue($collection instanceof FileCollection);
$collection2 = $connection->getFileCollection('testfs');
$this->assertTrue($collection === $collection2);
$collection2 = $connection->getFileCollection('testfs', true);
$this->assertFalse($collection === $collection2);
}
}

70
tests/unit/extensions/mongo/DatabaseTest.php

@ -0,0 +1,70 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\mongo\Collection;
use yii\mongo\file\Collection as FileCollection;
/**
* @group mongo
*/
class DatabaseTest extends MongoTestCase
{
protected function tearDown()
{
$this->dropCollection('customer');
$this->dropFileCollection('testfs');
parent::tearDown();
}
// Tests :
public function testGetCollection()
{
$database = $connection = $this->getConnection()->getDatabase();
$collection = $database->getCollection('customer');
$this->assertTrue($collection instanceof Collection);
$this->assertTrue($collection->mongoCollection instanceof \MongoCollection);
$collection2 = $database->getCollection('customer');
$this->assertTrue($collection === $collection2);
$collectionRefreshed = $database->getCollection('customer', true);
$this->assertFalse($collection === $collectionRefreshed);
}
public function testGetFileCollection()
{
$database = $connection = $this->getConnection()->getDatabase();
$collection = $database->getFileCollection('testfs');
$this->assertTrue($collection instanceof FileCollection);
$this->assertTrue($collection->mongoCollection instanceof \MongoGridFS);
$collection2 = $database->getFileCollection('testfs');
$this->assertTrue($collection === $collection2);
$collectionRefreshed = $database->getFileCollection('testfs', true);
$this->assertFalse($collection === $collectionRefreshed);
}
public function testExecuteCommand()
{
$database = $connection = $this->getConnection()->getDatabase();
$result = $database->executeCommand([
'distinct' => 'customer',
'key' => 'name'
]);
$this->assertTrue(array_key_exists('ok', $result));
$this->assertTrue(array_key_exists('values', $result));
}
public function testCreateCollection()
{
$database = $connection = $this->getConnection()->getDatabase();
$collection = $database->createCollection('customer');
$this->assertTrue($collection instanceof \MongoCollection);
}
}

149
tests/unit/extensions/mongo/MongoTestCase.php

@ -0,0 +1,149 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\helpers\FileHelper;
use yii\mongo\Connection;
use Yii;
use yii\mongo\Exception;
use yiiunit\TestCase;
class MongoTestCase extends TestCase
{
/**
* @var array Mongo connection configuration.
*/
protected $mongoConfig = [
'dsn' => 'mongodb://localhost:27017',
'defaultDatabaseName' => 'yii2test',
'options' => [],
];
/**
* @var Connection Mongo connection instance.
*/
protected $mongo;
public static function setUpBeforeClass()
{
static::loadClassMap();
}
protected function setUp()
{
parent::setUp();
if (!extension_loaded('mongo')) {
$this->markTestSkipped('mongo extension required.');
}
$config = $this->getParam('mongo');
if (!empty($config)) {
$this->mongoConfig = $config;
}
$this->mockApplication();
static::loadClassMap();
}
protected function tearDown()
{
if ($this->mongo) {
$this->mongo->close();
}
$this->destroyApplication();
}
/**
* Adds sphinx extension files to [[Yii::$classPath]],
* avoiding the necessity of usage Composer autoloader.
*/
protected static function loadClassMap()
{
$baseNameSpace = 'yii/mongo';
$basePath = realpath(__DIR__. '/../../../../extensions/mongo');
$files = FileHelper::findFiles($basePath);
foreach ($files as $file) {
$classRelativePath = str_replace($basePath, '', $file);
$classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath);
Yii::$classMap[$classFullName] = $file;
}
}
/**
* @param boolean $reset whether to clean up the test database
* @param boolean $open whether to open test database
* @return \yii\mongo\Connection
*/
public function getConnection($reset = false, $open = true)
{
if (!$reset && $this->mongo) {
return $this->mongo;
}
$db = new Connection;
$db->dsn = $this->mongoConfig['dsn'];
$db->defaultDatabaseName = $this->mongoConfig['defaultDatabaseName'];
if (isset($this->mongoConfig['options'])) {
$db->options = $this->mongoConfig['options'];
}
if ($open) {
$db->open();
}
$this->mongo = $db;
return $db;
}
/**
* Drops the specified collection.
* @param string $name collection name.
*/
protected function dropCollection($name)
{
if ($this->mongo) {
try {
$this->mongo->getCollection($name)->drop();
} catch (Exception $e) {
// shut down exception
}
}
}
/**
* Drops the specified file collection.
* @param string $name file collection name.
*/
protected function dropFileCollection($name = 'fs')
{
if ($this->mongo) {
try {
$this->mongo->getFileCollection($name)->drop();
} catch (Exception $e) {
// shut down exception
}
}
}
/**
* Finds all records in collection.
* @param \yii\mongo\Collection $collection
* @param array $condition
* @param array $fields
* @return array rows
*/
protected function findAll($collection, $condition = [], $fields = [])
{
$cursor = $collection->find($condition, $fields);
$result = [];
foreach ($cursor as $data) {
$result[] = $data;
}
return $result;
}
/**
* Returns the Mongo server version.
* @return string Mongo server version.
*/
protected function getServerVersion()
{
$connection = $this->getConnection();
$buildInfo = $connection->getDatabase()->executeCommand(['buildinfo' => true]);
return $buildInfo['version'];
}
}

132
tests/unit/extensions/mongo/QueryRunTest.php

@ -0,0 +1,132 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\mongo\Query;
/**
* @group mongo
*/
class QueryRunTest extends MongoTestCase
{
protected function setUp()
{
parent::setUp();
$this->setUpTestRows();
}
protected function tearDown()
{
$this->dropCollection('customer');
parent::tearDown();
}
/**
* Sets up test rows.
*/
protected function setUpTestRows()
{
$collection = $this->getConnection()->getCollection('customer');
$rows = [];
for ($i = 1; $i <= 10; $i++) {
$rows[] = [
'name' => 'name' . $i,
'address' => 'address' . $i,
'avatar' => [
'width' => 50 + $i,
'height' => 100 + $i,
'url' => 'http://some.url/' . $i,
],
];
}
$collection->batchInsert($rows);
}
// Tests :
public function testAll()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('customer')->all($connection);
$this->assertEquals(10, count($rows));
}
public function testDirectMatch()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('customer')
->where(['name' => 'name1'])
->all($connection);
$this->assertEquals(1, count($rows));
$this->assertEquals('name1', $rows[0]['name']);
}
public function testIndexBy()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('customer')
->indexBy('name')
->all($connection);
$this->assertEquals(10, count($rows));
$this->assertNotEmpty($rows['name1']);
}
public function testInCondition()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('customer')
->where([
'name' => ['name1', 'name5']
])
->all($connection);
$this->assertEquals(2, count($rows));
$this->assertEquals('name1', $rows[0]['name']);
$this->assertEquals('name5', $rows[1]['name']);
}
public function testOrCondition()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('customer')
->where(['name' => 'name1'])
->orWhere(['address' => 'address5'])
->all($connection);
$this->assertEquals(2, count($rows));
$this->assertEquals('name1', $rows[0]['name']);
$this->assertEquals('address5', $rows[1]['address']);
}
public function testOrder()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('customer')
->orderBy(['name' => SORT_DESC])
->all($connection);
$this->assertEquals('name9', $rows[0]['name']);
$query = new Query;
$rows = $query->from('customer')
->orderBy(['avatar.height' => SORT_DESC])
->all($connection);
$this->assertEquals('name10', $rows[0]['name']);
}
public function testMatchPlainId()
{
$connection = $this->getConnection();
$query = new Query;
$row = $query->from('customer')->one($connection);
$query = new Query;
$rows = $query->from('customer')
->where(['_id' => $row['_id']->__toString()])
->all($connection);
$this->assertEquals(1, count($rows));
}
}

97
tests/unit/extensions/mongo/QueryTest.php

@ -0,0 +1,97 @@
<?php
namespace yiiunit\extensions\mongo;
use yii\mongo\Query;
/**
* @group mongo
*/
class QueryTest extends MongoTestCase
{
public function testSelect()
{
// default
$query = new Query;
$select = [];
$query->select($select);
$this->assertEquals($select, $query->select);
$query = new Query;
$select = ['name', 'something'];
$query->select($select);
$this->assertEquals($select, $query->select);
}
public function testFrom()
{
$query = new Query;
$from = 'customer';
$query->from($from);
$this->assertEquals($from, $query->from);
$query = new Query;
$from = ['', 'customer'];
$query->from($from);
$this->assertEquals($from, $query->from);
}
public function testWhere()
{
$query = new Query;
$query->where(['name' => 'name1']);
$this->assertEquals(['name' => 'name1'], $query->where);
$query->andWhere(['address' => 'address1']);
$this->assertEquals(
[
'and',
['name' => 'name1'],
['address' => 'address1']
],
$query->where
);
$query->orWhere(['name' => 'name2']);
$this->assertEquals(
[
'or',
[
'and',
['name' => 'name1'],
['address' => 'address1']
],
['name' => 'name2']
],
$query->where
);
}
public function testOrder()
{
$query = new Query;
$query->orderBy('team');
$this->assertEquals(['team' => SORT_ASC], $query->orderBy);
$query->addOrderBy('company');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy);
$query->addOrderBy('age');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy);
$query->addOrderBy(['age' => SORT_DESC]);
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy);
$query->addOrderBy('age ASC, company DESC');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy);
}
public function testLimitOffset()
{
$query = new Query;
$query->limit(10)->offset(5);
$this->assertEquals(10, $query->limit);
$this->assertEquals(5, $query->offset);
}
}

323
tests/unit/extensions/mongo/file/ActiveRecordTest.php

@ -0,0 +1,323 @@
<?php
namespace yiiunit\extensions\mongo\file;
use Yii;
use yii\helpers\FileHelper;
use yiiunit\extensions\mongo\MongoTestCase;
use yii\mongo\file\ActiveQuery;
use yiiunit\data\ar\mongo\file\ActiveRecord;
use yiiunit\data\ar\mongo\file\CustomerFile;
/**
* @group mongo
*/
class ActiveRecordTest extends MongoTestCase
{
/**
* @var array[] list of test rows.
*/
protected $testRows = [];
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
$this->setUpTestRows();
$filePath = $this->getTestFilePath();
if (!file_exists($filePath)) {
FileHelper::createDirectory($filePath);
}
}
protected function tearDown()
{
$filePath = $this->getTestFilePath();
if (file_exists($filePath)) {
FileHelper::removeDirectory($filePath);
}
$this->dropFileCollection(CustomerFile::collectionName());
parent::tearDown();
}
/**
* @return string test file path.
*/
protected function getTestFilePath()
{
return Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . basename(get_class($this)) . '_' . getmypid();
}
/**
* Sets up test rows.
*/
protected function setUpTestRows()
{
$collection = $this->getConnection()->getFileCollection(CustomerFile::collectionName());
$rows = [];
for ($i = 1; $i <= 10; $i++) {
$record = [
'tag' => 'tag' . $i,
'status' => $i,
];
$content = 'content' . $i;
$record['_id'] = $collection->insertFileContent($content, $record);
$record['content'] = $content;
$rows[] = $record;
}
$this->testRows = $rows;
}
// Tests :
public function testFind()
{
// find one
$result = CustomerFile::find();
$this->assertTrue($result instanceof ActiveQuery);
$customer = $result->one();
$this->assertTrue($customer instanceof CustomerFile);
// find all
$customers = CustomerFile::find()->all();
$this->assertEquals(10, count($customers));
$this->assertTrue($customers[0] instanceof CustomerFile);
$this->assertTrue($customers[1] instanceof CustomerFile);
// find by _id
$testId = $this->testRows[0]['_id'];
$customer = CustomerFile::find($testId);
$this->assertTrue($customer instanceof CustomerFile);
$this->assertEquals($testId, $customer->_id);
// find by column values
$customer = CustomerFile::find(['tag' => 'tag5']);
$this->assertTrue($customer instanceof CustomerFile);
$this->assertEquals($this->testRows[4]['_id'], $customer->_id);
$this->assertEquals('tag5', $customer->tag);
$customer = CustomerFile::find(['tag' => 'unexisting tag']);
$this->assertNull($customer);
// find by attributes
$customer = CustomerFile::find()->where(['status' => 4])->one();
$this->assertTrue($customer instanceof CustomerFile);
$this->assertEquals(4, $customer->status);
// find count, sum, average, min, max, distinct
$this->assertEquals(10, CustomerFile::find()->count());
$this->assertEquals(1, CustomerFile::find()->where(['status' => 2])->count());
$this->assertEquals((1+10)/2*10, CustomerFile::find()->sum('status'));
$this->assertEquals((1+10)/2, CustomerFile::find()->average('status'));
$this->assertEquals(1, CustomerFile::find()->min('status'));
$this->assertEquals(10, CustomerFile::find()->max('status'));
$this->assertEquals(range(1, 10), CustomerFile::find()->distinct('status'));
// scope
$this->assertEquals(1, CustomerFile::find()->activeOnly()->count());
// asArray
$testRow = $this->testRows[2];
$customer = CustomerFile::find()->where(['_id' => $testRow['_id']])->asArray()->one();
$this->assertEquals($testRow['_id'], $customer['_id']);
$this->assertEquals($testRow['tag'], $customer['tag']);
$this->assertEquals($testRow['status'], $customer['status']);
// indexBy
$customers = CustomerFile::find()->indexBy('tag')->all();
$this->assertTrue($customers['tag1'] instanceof CustomerFile);
$this->assertTrue($customers['tag2'] instanceof CustomerFile);
// indexBy callable
$customers = CustomerFile::find()->indexBy(function ($customer) {
return $customer->status . '-' . $customer->status;
})->all();
$this->assertTrue($customers['1-1'] instanceof CustomerFile);
$this->assertTrue($customers['2-2'] instanceof CustomerFile);
}
public function testInsert()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$this->assertTrue($record->isNewRecord);
$record->save();
$this->assertTrue($record->_id instanceof \MongoId);
$this->assertFalse($record->isNewRecord);
$fileContent = $record->getFileContent();
$this->assertEmpty($fileContent);
}
/**
* @depends testInsert
*/
public function testInsertFile()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$fileName = __FILE__;
$record->setAttribute('file', $fileName);
$record->save();
$this->assertTrue($record->_id instanceof \MongoId);
$this->assertFalse($record->isNewRecord);
$fileContent = $record->getFileContent();
$this->assertEquals(file_get_contents($fileName), $fileContent);
}
/**
* @depends testInsert
*/
public function testInsertFileContent()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$newFileContent = 'Test new file content';
$record->setAttribute('newFileContent', $newFileContent);
$record->save();
$this->assertTrue($record->_id instanceof \MongoId);
$this->assertFalse($record->isNewRecord);
$fileContent = $record->getFileContent();
$this->assertEquals($newFileContent, $fileContent);
}
/**
* @depends testInsert
*/
public function testUpdate()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$record->save();
// save
$record = CustomerFile::find($record->_id);
$this->assertTrue($record instanceof CustomerFile);
$this->assertEquals(7, $record->status);
$this->assertFalse($record->isNewRecord);
$record->status = 9;
$record->save();
$this->assertEquals(9, $record->status);
$this->assertFalse($record->isNewRecord);
$record2 = CustomerFile::find($record->_id);
$this->assertEquals(9, $record2->status);
// updateAll
$pk = ['_id' => $record->_id];
$ret = CustomerFile::updateAll(['status' => 55], $pk);
$this->assertEquals(1, $ret);
$record = CustomerFile::find($pk);
$this->assertEquals(55, $record->status);
}
/**
* @depends testUpdate
* @depends testInsertFileContent
*/
public function testUpdateFile()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$newFileContent = 'Test new file content';
$record->setAttribute('newFileContent', $newFileContent);
$record->save();
$updateFileName = __FILE__;
$record = CustomerFile::find($record->_id);
$record->setAttribute('file', $updateFileName);
$record->status = 55;
$record->save();
$this->assertEquals(file_get_contents($updateFileName), $record->getFileContent());
$record2 = CustomerFile::find($record->_id);
$this->assertEquals($record->status, $record2->status);
$this->assertEquals(file_get_contents($updateFileName), $record2->getFileContent());
}
/**
* @depends testUpdate
* @depends testInsertFileContent
*/
public function testUpdateFileContent()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$newFileContent = 'Test new file content';
$record->setAttribute('newFileContent', $newFileContent);
$record->save();
$updateFileContent = 'New updated file content';
$record = CustomerFile::find($record->_id);
$record->setAttribute('newFileContent', $updateFileContent);
$record->status = 55;
$record->save();
$this->assertEquals($updateFileContent, $record->getFileContent());
$record2 = CustomerFile::find($record->_id);
$this->assertEquals($record->status, $record2->status);
$this->assertEquals($updateFileContent, $record2->getFileContent());
}
/**
* @depends testInsertFileContent
*/
public function testWriteFile()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$newFileContent = 'Test new file content';
$record->setAttribute('newFileContent', $newFileContent);
$record->save();
$outputFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . 'out.txt';
$this->assertTrue($record->writeFile($outputFileName));
$this->assertEquals($newFileContent, file_get_contents($outputFileName));
$record2 = CustomerFile::find($record->_id);
$outputFileName = $this->getTestFilePath() . DIRECTORY_SEPARATOR . 'out_refreshed.txt';
$this->assertTrue($record2->writeFile($outputFileName));
$this->assertEquals($newFileContent, file_get_contents($outputFileName));
}
/**
* @depends testInsertFileContent
*/
public function testGetFileResource()
{
$record = new CustomerFile;
$record->tag = 'new new';
$record->status = 7;
$newFileContent = 'Test new file content';
$record->setAttribute('newFileContent', $newFileContent);
$record->save();
$fileResource = $record->getFileResource();
$contents = stream_get_contents($fileResource);
fclose($fileResource);
$this->assertEquals($newFileContent, $contents);
$record2 = CustomerFile::find($record->_id);
$fileResource = $record2->getFileResource();
$contents = stream_get_contents($fileResource);
fclose($fileResource);
$this->assertEquals($newFileContent, $contents);
}
}

98
tests/unit/extensions/mongo/file/CollectionTest.php

@ -0,0 +1,98 @@
<?php
namespace yiiunit\extensions\mongo\file;
use yiiunit\extensions\mongo\MongoTestCase;
/**
* @group mongo
*/
class CollectionTest extends MongoTestCase
{
protected function tearDown()
{
$this->dropFileCollection('fs');
parent::tearDown();
}
// Tests :
public function testGetChunkCollection()
{
$collection = $this->getConnection()->getFileCollection();
$chunkCollection = $collection->getChunkCollection();
$this->assertTrue($chunkCollection instanceof \yii\mongo\Collection);
$this->assertTrue($chunkCollection->mongoCollection instanceof \MongoCollection);
}
public function testFind()
{
$collection = $this->getConnection()->getFileCollection();
$cursor = $collection->find();
$this->assertTrue($cursor instanceof \MongoGridFSCursor);
}
public function testInsertFile()
{
$collection = $this->getConnection()->getFileCollection();
$filename = __FILE__;
$id = $collection->insertFile($filename);
$this->assertTrue($id instanceof \MongoId);
$files = $this->findAll($collection);
$this->assertEquals(1, count($files));
/** @var $file \MongoGridFSFile */
$file = $files[0];
$this->assertEquals($filename, $file->getFilename());
$this->assertEquals(file_get_contents($filename), $file->getBytes());
}
public function testInsertFileContent()
{
$collection = $this->getConnection()->getFileCollection();
$bytes = 'Test file content';
$id = $collection->insertFileContent($bytes);
$this->assertTrue($id instanceof \MongoId);
$files = $this->findAll($collection);
$this->assertEquals(1, count($files));
/** @var $file \MongoGridFSFile */
$file = $files[0];
$this->assertEquals($bytes, $file->getBytes());
}
/**
* @depends testInsertFileContent
*/
public function testGet()
{
$collection = $this->getConnection()->getFileCollection();
$bytes = 'Test file content';
$id = $collection->insertFileContent($bytes);
$file = $collection->get($id);
$this->assertTrue($file instanceof \MongoGridFSFile);
$this->assertEquals($bytes, $file->getBytes());
}
/**
* @depends testGet
*/
public function testDelete()
{
$collection = $this->getConnection()->getFileCollection();
$bytes = 'Test file content';
$id = $collection->insertFileContent($bytes);
$this->assertTrue($collection->delete($id));
$file = $collection->get($id);
$this->assertNull($file);
}
}

70
tests/unit/extensions/mongo/file/QueryTest.php

@ -0,0 +1,70 @@
<?php
namespace yiiunit\extensions\mongo\file;
use yii\mongo\file\Query;
use yiiunit\extensions\mongo\MongoTestCase;
/**
* @group mongo
*/
class QueryTest extends MongoTestCase
{
protected function setUp()
{
parent::setUp();
$this->setUpTestRows();
}
protected function tearDown()
{
$this->dropFileCollection();
parent::tearDown();
}
/**
* Sets up test rows.
*/
protected function setUpTestRows()
{
$collection = $this->getConnection()->getFileCollection();
for ($i = 1; $i <= 10; $i++) {
$collection->insertFileContent('content' . $i, [
'filename' => 'name' . $i,
'file_index' => $i,
]);
}
}
// Tests :
public function testAll()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('fs')->all($connection);
$this->assertEquals(10, count($rows));
}
public function testOne()
{
$connection = $this->getConnection();
$query = new Query;
$row = $query->from('fs')->one($connection);
$this->assertTrue(is_array($row));
$this->assertTrue($row['file'] instanceof \MongoGridFSFile);
}
public function testDirectMatch()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('fs')
->where(['file_index' => 5])
->all($connection);
$this->assertEquals(1, count($rows));
/** @var $file \MongoGridFSFile */
$file = $rows[0];
$this->assertEquals('name5', $file['filename']);
}
}
Loading…
Cancel
Save