From 9b5f3fab992c076c85cd7223217090982d2b7919 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 6 Dec 2013 17:35:32 +0200 Subject: [PATCH] Extension "mongo" renamed into "mongodb" --- extensions/mongo/ActiveQuery.php | 107 --- extensions/mongo/ActiveRecord.php | 353 -------- extensions/mongo/ActiveRelation.php | 22 - extensions/mongo/Collection.php | 915 --------------------- extensions/mongo/Connection.php | 254 ------ extensions/mongo/Database.php | 172 ---- extensions/mongo/Exception.php | 25 - extensions/mongo/LICENSE.md | 32 - extensions/mongo/Query.php | 344 -------- extensions/mongo/README.md | 116 --- extensions/mongo/composer.json | 28 - extensions/mongo/file/ActiveQuery.php | 107 --- extensions/mongo/file/ActiveRecord.php | 340 -------- extensions/mongo/file/ActiveRelation.php | 22 - extensions/mongo/file/Collection.php | 186 ----- extensions/mongo/file/Query.php | 75 -- extensions/mongodb/ActiveQuery.php | 107 +++ extensions/mongodb/ActiveRecord.php | 353 ++++++++ extensions/mongodb/ActiveRelation.php | 22 + extensions/mongodb/Collection.php | 915 +++++++++++++++++++++ extensions/mongodb/Connection.php | 254 ++++++ extensions/mongodb/Database.php | 172 ++++ extensions/mongodb/Exception.php | 25 + extensions/mongodb/LICENSE.md | 32 + extensions/mongodb/Query.php | 344 ++++++++ extensions/mongodb/README.md | 116 +++ extensions/mongodb/composer.json | 28 + extensions/mongodb/file/ActiveQuery.php | 107 +++ extensions/mongodb/file/ActiveRecord.php | 340 ++++++++ extensions/mongodb/file/ActiveRelation.php | 22 + extensions/mongodb/file/Collection.php | 186 +++++ extensions/mongodb/file/Query.php | 73 ++ tests/unit/data/ar/mongo/ActiveRecord.php | 16 - tests/unit/data/ar/mongo/Customer.php | 32 - tests/unit/data/ar/mongo/CustomerOrder.php | 27 - tests/unit/data/ar/mongo/file/ActiveRecord.php | 16 - tests/unit/data/ar/mongo/file/CustomerFile.php | 27 - tests/unit/data/ar/mongodb/ActiveRecord.php | 16 + tests/unit/data/ar/mongodb/Customer.php | 32 + tests/unit/data/ar/mongodb/CustomerOrder.php | 27 + tests/unit/data/ar/mongodb/file/ActiveRecord.php | 16 + tests/unit/data/ar/mongodb/file/CustomerFile.php | 27 + tests/unit/data/config.php | 2 +- .../extensions/mongo/ActiveDataProviderTest.php | 91 -- tests/unit/extensions/mongo/ActiveRecordTest.php | 246 ------ tests/unit/extensions/mongo/ActiveRelationTest.php | 83 -- tests/unit/extensions/mongo/CollectionTest.php | 313 ------- tests/unit/extensions/mongo/ConnectionTest.php | 119 --- tests/unit/extensions/mongo/DatabaseTest.php | 70 -- tests/unit/extensions/mongo/MongoTestCase.php | 149 ---- tests/unit/extensions/mongo/QueryRunTest.php | 144 ---- tests/unit/extensions/mongo/QueryTest.php | 97 --- .../extensions/mongo/file/ActiveRecordTest.php | 323 -------- .../unit/extensions/mongo/file/CollectionTest.php | 98 --- tests/unit/extensions/mongo/file/QueryTest.php | 70 -- .../extensions/mongodb/ActiveDataProviderTest.php | 91 ++ tests/unit/extensions/mongodb/ActiveRecordTest.php | 246 ++++++ .../unit/extensions/mongodb/ActiveRelationTest.php | 83 ++ tests/unit/extensions/mongodb/CollectionTest.php | 313 +++++++ tests/unit/extensions/mongodb/ConnectionTest.php | 119 +++ tests/unit/extensions/mongodb/DatabaseTest.php | 70 ++ tests/unit/extensions/mongodb/MongoDbTestCase.php | 149 ++++ tests/unit/extensions/mongodb/QueryRunTest.php | 144 ++++ tests/unit/extensions/mongodb/QueryTest.php | 97 +++ .../extensions/mongodb/file/ActiveRecordTest.php | 323 ++++++++ .../extensions/mongodb/file/CollectionTest.php | 98 +++ tests/unit/extensions/mongodb/file/QueryTest.php | 70 ++ 67 files changed, 5018 insertions(+), 5020 deletions(-) delete mode 100644 extensions/mongo/ActiveQuery.php delete mode 100644 extensions/mongo/ActiveRecord.php delete mode 100644 extensions/mongo/ActiveRelation.php delete mode 100644 extensions/mongo/Collection.php delete mode 100644 extensions/mongo/Connection.php delete mode 100644 extensions/mongo/Database.php delete mode 100644 extensions/mongo/Exception.php delete mode 100644 extensions/mongo/LICENSE.md delete mode 100644 extensions/mongo/Query.php delete mode 100644 extensions/mongo/README.md delete mode 100644 extensions/mongo/composer.json delete mode 100644 extensions/mongo/file/ActiveQuery.php delete mode 100644 extensions/mongo/file/ActiveRecord.php delete mode 100644 extensions/mongo/file/ActiveRelation.php delete mode 100644 extensions/mongo/file/Collection.php delete mode 100644 extensions/mongo/file/Query.php create mode 100644 extensions/mongodb/ActiveQuery.php create mode 100644 extensions/mongodb/ActiveRecord.php create mode 100644 extensions/mongodb/ActiveRelation.php create mode 100644 extensions/mongodb/Collection.php create mode 100644 extensions/mongodb/Connection.php create mode 100644 extensions/mongodb/Database.php create mode 100644 extensions/mongodb/Exception.php create mode 100644 extensions/mongodb/LICENSE.md create mode 100644 extensions/mongodb/Query.php create mode 100644 extensions/mongodb/README.md create mode 100644 extensions/mongodb/composer.json create mode 100644 extensions/mongodb/file/ActiveQuery.php create mode 100644 extensions/mongodb/file/ActiveRecord.php create mode 100644 extensions/mongodb/file/ActiveRelation.php create mode 100644 extensions/mongodb/file/Collection.php create mode 100644 extensions/mongodb/file/Query.php delete mode 100644 tests/unit/data/ar/mongo/ActiveRecord.php delete mode 100644 tests/unit/data/ar/mongo/Customer.php delete mode 100644 tests/unit/data/ar/mongo/CustomerOrder.php delete mode 100644 tests/unit/data/ar/mongo/file/ActiveRecord.php delete mode 100644 tests/unit/data/ar/mongo/file/CustomerFile.php create mode 100644 tests/unit/data/ar/mongodb/ActiveRecord.php create mode 100644 tests/unit/data/ar/mongodb/Customer.php create mode 100644 tests/unit/data/ar/mongodb/CustomerOrder.php create mode 100644 tests/unit/data/ar/mongodb/file/ActiveRecord.php create mode 100644 tests/unit/data/ar/mongodb/file/CustomerFile.php delete mode 100644 tests/unit/extensions/mongo/ActiveDataProviderTest.php delete mode 100644 tests/unit/extensions/mongo/ActiveRecordTest.php delete mode 100644 tests/unit/extensions/mongo/ActiveRelationTest.php delete mode 100644 tests/unit/extensions/mongo/CollectionTest.php delete mode 100644 tests/unit/extensions/mongo/ConnectionTest.php delete mode 100644 tests/unit/extensions/mongo/DatabaseTest.php delete mode 100644 tests/unit/extensions/mongo/MongoTestCase.php delete mode 100644 tests/unit/extensions/mongo/QueryRunTest.php delete mode 100644 tests/unit/extensions/mongo/QueryTest.php delete mode 100644 tests/unit/extensions/mongo/file/ActiveRecordTest.php delete mode 100644 tests/unit/extensions/mongo/file/CollectionTest.php delete mode 100644 tests/unit/extensions/mongo/file/QueryTest.php create mode 100644 tests/unit/extensions/mongodb/ActiveDataProviderTest.php create mode 100644 tests/unit/extensions/mongodb/ActiveRecordTest.php create mode 100644 tests/unit/extensions/mongodb/ActiveRelationTest.php create mode 100644 tests/unit/extensions/mongodb/CollectionTest.php create mode 100644 tests/unit/extensions/mongodb/ConnectionTest.php create mode 100644 tests/unit/extensions/mongodb/DatabaseTest.php create mode 100644 tests/unit/extensions/mongodb/MongoDbTestCase.php create mode 100644 tests/unit/extensions/mongodb/QueryRunTest.php create mode 100644 tests/unit/extensions/mongodb/QueryTest.php create mode 100644 tests/unit/extensions/mongodb/file/ActiveRecordTest.php create mode 100644 tests/unit/extensions/mongodb/file/CollectionTest.php create mode 100644 tests/unit/extensions/mongodb/file/QueryTest.php diff --git a/extensions/mongo/ActiveQuery.php b/extensions/mongo/ActiveQuery.php deleted file mode 100644 index fc02df9..0000000 --- a/extensions/mongo/ActiveQuery.php +++ /dev/null @@ -1,107 +0,0 @@ -with('orders')->asArray()->all(); - * ~~~ - * - * @author Paul Klimov - * @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); - } -} \ No newline at end of file diff --git a/extensions/mongo/ActiveRecord.php b/extensions/mongo/ActiveRecord.php deleted file mode 100644 index 86aa6d1..0000000 --- a/extensions/mongo/ActiveRecord.php +++ /dev/null @@ -1,353 +0,0 @@ - - * @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() && (string)$this->getPrimaryKey() === (string)$record->getPrimaryKey(); - } -} \ No newline at end of file diff --git a/extensions/mongo/ActiveRelation.php b/extensions/mongo/ActiveRelation.php deleted file mode 100644 index 539dc7b..0000000 --- a/extensions/mongo/ActiveRelation.php +++ /dev/null @@ -1,22 +0,0 @@ - - * @since 2.0 - */ -class ActiveRelation extends ActiveQuery implements ActiveRelationInterface -{ - use ActiveRelationTrait; -} \ No newline at end of file diff --git a/extensions/mongo/Collection.php b/extensions/mongo/Collection.php deleted file mode 100644 index 9bd8bcc..0000000 --- a/extensions/mongo/Collection.php +++ /dev/null @@ -1,915 +0,0 @@ -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 - * @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. - * @param array $options additional optional parameters to the mapReduce command. Valid options include: - * - sort - array - key to sort the input documents. The sort key must be in an existing index for this collection. - * - limit - the maximum number of documents to return in the collection. - * - finalize - function, which follows the reduce method and modifies the output. - * - scope - array - specifies global variables that are accessible in the map, reduce and finalize functions. - * - jsMode - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions. - * - verbose - boolean - specifies whether to include the timing information in the result information. - * @return string the map reduce output collection name. - * @throws Exception on failure. - */ - public function mapReduce($map, $reduce, $out, $condition = [], $options = []) - { - 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); - } - if (array_key_exists('finalize', $options)) { - if (!($options['finalize'] instanceof \MongoCode)) { - $options['finalize'] = new \MongoCode((string)$options['finalize']); - } - } - if (!empty($options)) { - $command = array_merge($command, $options); - } - $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 array $options additional optional parameters to the mapReduce command. Valid options include: - * - limit - the maximum number of documents to include in the response (by default 100). - * - language - the 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 = [], $options = []) { - $command = [ - 'search' => $search - ]; - if (!empty($condition)) { - $command['filter'] = $this->buildCondition($condition); - } - if (!empty($fields)) { - $command['project'] = $fields; - } - if (!empty($options)) { - $command = array_merge($command, $options); - } - $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; - if (!($value instanceof \MongoRegex)) { - $value = new \MongoRegex($value); - } - return [$column => $value]; - } -} \ No newline at end of file diff --git a/extensions/mongo/Connection.php b/extensions/mongo/Connection.php deleted file mode 100644 index 3b9ea9e..0000000 --- a/extensions/mongo/Connection.php +++ /dev/null @@ -1,254 +0,0 @@ - $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. - * - * @author Paul Klimov - * @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: - * - * ~~~ - * [ - * '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 - * ] - * ~~~ - * - * @see http://www.php.net/manual/en/mongoclient.construct.php - */ - 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 reestablish the database connection even if it is found in the cache. - * @return Database database instance. - */ - public function getDatabase($name = null, $refresh = false) - { - if ($name === null) { - $name = $this->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 = []; - } - } -} \ No newline at end of file diff --git a/extensions/mongo/Database.php b/extensions/mongo/Database.php deleted file mode 100644 index bb919b5..0000000 --- a/extensions/mongo/Database.php +++ /dev/null @@ -1,172 +0,0 @@ - - * @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'); - } - } -} \ No newline at end of file diff --git a/extensions/mongo/Exception.php b/extensions/mongo/Exception.php deleted file mode 100644 index 0687e48..0000000 --- a/extensions/mongo/Exception.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @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'); - } -} \ No newline at end of file diff --git a/extensions/mongo/LICENSE.md b/extensions/mongo/LICENSE.md deleted file mode 100644 index 0bb1a8d..0000000 --- a/extensions/mongo/LICENSE.md +++ /dev/null @@ -1,32 +0,0 @@ -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. diff --git a/extensions/mongo/Query.php b/extensions/mongo/Query.php deleted file mode 100644 index cce6645..0000000 --- a/extensions/mongo/Query.php +++ /dev/null @@ -1,344 +0,0 @@ -select(['name', 'status']) - * ->from('customer') - * ->limit(10); - * // execute the query - * $rows = $query->all(); - * ~~~ - * - * @author Paul Klimov - * @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; - } - } -} \ No newline at end of file diff --git a/extensions/mongo/README.md b/extensions/mongo/README.md deleted file mode 100644 index 7f5ef70..0000000 --- a/extensions/mongo/README.md +++ /dev/null @@ -1,116 +0,0 @@ -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". \ No newline at end of file diff --git a/extensions/mongo/composer.json b/extensions/mongo/composer.json deleted file mode 100644 index 38296b6..0000000 --- a/extensions/mongo/composer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "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" - } - ], - "require": { - "yiisoft/yii2": "*", - "ext-mongo": ">=1.3.0" - }, - "autoload": { - "psr-0": { "yii\\mongo\\": "" } - }, - "target-dir": "yii/mongo" -} diff --git a/extensions/mongo/file/ActiveQuery.php b/extensions/mongo/file/ActiveQuery.php deleted file mode 100644 index 91661d6..0000000 --- a/extensions/mongo/file/ActiveQuery.php +++ /dev/null @@ -1,107 +0,0 @@ -with('tags')->asArray()->all(); - * ~~~ - * - * @author Paul Klimov - * @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); - } -} \ No newline at end of file diff --git a/extensions/mongo/file/ActiveRecord.php b/extensions/mongo/file/ActiveRecord.php deleted file mode 100644 index c7ca752..0000000 --- a/extensions/mongo/file/ActiveRecord.php +++ /dev/null @@ -1,340 +0,0 @@ -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 - * @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.'); - } - } -} \ No newline at end of file diff --git a/extensions/mongo/file/ActiveRelation.php b/extensions/mongo/file/ActiveRelation.php deleted file mode 100644 index 6ea0831..0000000 --- a/extensions/mongo/file/ActiveRelation.php +++ /dev/null @@ -1,22 +0,0 @@ - - * @since 2.0 - */ -class ActiveRelation extends ActiveQuery implements ActiveRelationInterface -{ - use ActiveRelationTrait; -} \ No newline at end of file diff --git a/extensions/mongo/file/Collection.php b/extensions/mongo/file/Collection.php deleted file mode 100644 index b3c722b..0000000 --- a/extensions/mongo/file/Collection.php +++ /dev/null @@ -1,186 +0,0 @@ - - * @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); - } - } -} \ No newline at end of file diff --git a/extensions/mongo/file/Query.php b/extensions/mongo/file/Query.php deleted file mode 100644 index c15d4f1..0000000 --- a/extensions/mongo/file/Query.php +++ /dev/null @@ -1,75 +0,0 @@ - - * @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; - } -} \ No newline at end of file diff --git a/extensions/mongodb/ActiveQuery.php b/extensions/mongodb/ActiveQuery.php new file mode 100644 index 0000000..6b87533 --- /dev/null +++ b/extensions/mongodb/ActiveQuery.php @@ -0,0 +1,107 @@ +with('orders')->asArray()->all(); + * ~~~ + * + * @author Paul Klimov + * @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); + } +} \ No newline at end of file diff --git a/extensions/mongodb/ActiveRecord.php b/extensions/mongodb/ActiveRecord.php new file mode 100644 index 0000000..d2178ea --- /dev/null +++ b/extensions/mongodb/ActiveRecord.php @@ -0,0 +1,353 @@ + + * @since 2.0 + */ +abstract class ActiveRecord extends BaseActiveRecord +{ + /** + * Returns the Mongo connection used by this AR class. + * By default, the "mongodb" application component is used as the Mongo connection. + * You may override this method if you want to use a different database connection. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->getComponent('mongodb'); + } + + /** + * Updates all documents in the collection using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * 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 mongodb ActiveRecord has to be implemented by child classes.'); + } + + /** + * Inserts a row into the associated Mongo collection using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into collection. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. + * + * If the primary key is null during insertion, it will be populated with the actual + * value after insertion. + * + * For example, to insert a customer record: + * + * ~~~ + * $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() && (string)$this->getPrimaryKey() === (string)$record->getPrimaryKey(); + } +} \ No newline at end of file diff --git a/extensions/mongodb/ActiveRelation.php b/extensions/mongodb/ActiveRelation.php new file mode 100644 index 0000000..d4541cc --- /dev/null +++ b/extensions/mongodb/ActiveRelation.php @@ -0,0 +1,22 @@ + + * @since 2.0 + */ +class ActiveRelation extends ActiveQuery implements ActiveRelationInterface +{ + use ActiveRelationTrait; +} \ No newline at end of file diff --git a/extensions/mongodb/Collection.php b/extensions/mongodb/Collection.php new file mode 100644 index 0000000..f90298d --- /dev/null +++ b/extensions/mongodb/Collection.php @@ -0,0 +1,915 @@ +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 + * @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. + * @param array $options additional optional parameters to the mapReduce command. Valid options include: + * - sort - array - key to sort the input documents. The sort key must be in an existing index for this collection. + * - limit - the maximum number of documents to return in the collection. + * - finalize - function, which follows the reduce method and modifies the output. + * - scope - array - specifies global variables that are accessible in the map, reduce and finalize functions. + * - jsMode - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions. + * - verbose - boolean - specifies whether to include the timing information in the result information. + * @return string the map reduce output collection name. + * @throws Exception on failure. + */ + public function mapReduce($map, $reduce, $out, $condition = [], $options = []) + { + 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); + } + if (array_key_exists('finalize', $options)) { + if (!($options['finalize'] instanceof \MongoCode)) { + $options['finalize'] = new \MongoCode((string)$options['finalize']); + } + } + if (!empty($options)) { + $command = array_merge($command, $options); + } + $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 array $options additional optional parameters to the mapReduce command. Valid options include: + * - limit - the maximum number of documents to include in the response (by default 100). + * - language - the 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 = [], $options = []) { + $command = [ + 'search' => $search + ]; + if (!empty($condition)) { + $command['filter'] = $this->buildCondition($condition); + } + if (!empty($fields)) { + $command['project'] = $fields; + } + if (!empty($options)) { + $command = array_merge($command, $options); + } + $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; + if (!($value instanceof \MongoRegex)) { + $value = new \MongoRegex($value); + } + return [$column => $value]; + } +} \ No newline at end of file diff --git a/extensions/mongodb/Connection.php b/extensions/mongodb/Connection.php new file mode 100644 index 0000000..175667e --- /dev/null +++ b/extensions/mongodb/Connection.php @@ -0,0 +1,254 @@ + $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' => [ + * 'mongodb' => [ + * 'class' => '\yii\mongodb\Connection', + * 'dsn' => 'mongodb://developer:password@localhost:27017/mydatabase', + * ], + * ], + * ] + * ~~~ + * + * @property boolean $isActive Whether the Mongo connection is established. This property is read-only. + * + * @author Paul Klimov + * @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: + * + * ~~~ + * [ + * '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 + * ] + * ~~~ + * + * @see http://www.php.net/manual/en/mongoclient.construct.php + */ + 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 reestablish the database connection even if it is found in the cache. + * @return Database database instance. + */ + public function getDatabase($name = null, $refresh = false) + { + if ($name === null) { + $name = $this->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\mongodb\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 = []; + } + } +} \ No newline at end of file diff --git a/extensions/mongodb/Database.php b/extensions/mongodb/Database.php new file mode 100644 index 0000000..3606afa --- /dev/null +++ b/extensions/mongodb/Database.php @@ -0,0 +1,172 @@ + + * @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\mongodb\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\mongodb\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'); + } + } +} \ No newline at end of file diff --git a/extensions/mongodb/Exception.php b/extensions/mongodb/Exception.php new file mode 100644 index 0000000..3288167 --- /dev/null +++ b/extensions/mongodb/Exception.php @@ -0,0 +1,25 @@ + + * @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'); + } +} \ No newline at end of file diff --git a/extensions/mongodb/LICENSE.md b/extensions/mongodb/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/mongodb/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/mongodb/Query.php b/extensions/mongodb/Query.php new file mode 100644 index 0000000..28e123c --- /dev/null +++ b/extensions/mongodb/Query.php @@ -0,0 +1,344 @@ +select(['name', 'status']) + * ->from('customer') + * ->limit(10); + * // execute the query + * $rows = $query->all(); + * ~~~ + * + * @author Paul Klimov + * @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('mongodb'); + } + return $db->getCollection($this->from); + } + + /** + * Sets the list of fields of the results to return. + * @param array $fields fields of the results to return. + * @return 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 `mongodb` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null) + { + $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 `mongodb` 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 `mongodb` 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 `mongodb` 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 `mongodb` 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 `mongodb` 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 `mongodb` 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 `mongodb` 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; + } + } +} \ No newline at end of file diff --git a/extensions/mongodb/README.md b/extensions/mongodb/README.md new file mode 100644 index 0000000..85c1a96 --- /dev/null +++ b/extensions/mongodb/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-mongodb "*" +``` + +or add +``` +"yiisoft/yii2-mongodb": "*" +``` +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' => [ + 'mongodb' => [ + 'class' => '\yii\mongodb\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\mongodb\ActiveRecord]] and +implement the `collectionName` and 'attributes' methods: + +```php +use yii\mongodb\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\mongodb\Query]] and [[\yii\mongodb\ActiveQuery]]: + +```php +use yii\data\ActiveDataProvider; +use yii\mongodb\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". \ No newline at end of file diff --git a/extensions/mongodb/composer.json b/extensions/mongodb/composer.json new file mode 100644 index 0000000..fc850b2 --- /dev/null +++ b/extensions/mongodb/composer.json @@ -0,0 +1,28 @@ +{ + "name": "yiisoft/yii2-mongodb", + "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" + } + ], + "require": { + "yiisoft/yii2": "*", + "ext-mongo": ">=1.3.0" + }, + "autoload": { + "psr-0": { "yii\\mongodb\\": "" } + }, + "target-dir": "yii/mongodb" +} diff --git a/extensions/mongodb/file/ActiveQuery.php b/extensions/mongodb/file/ActiveQuery.php new file mode 100644 index 0000000..4cdac5c --- /dev/null +++ b/extensions/mongodb/file/ActiveQuery.php @@ -0,0 +1,107 @@ +with('tags')->asArray()->all(); + * ~~~ + * + * @author Paul Klimov + * @since 2.0 + */ +class ActiveQuery extends Query implements ActiveQueryInterface +{ + use ActiveQueryTrait; + + /** + * Executes query and returns all results as an array. + * @param \yii\mongodb\Connection $db the Mongo connection used to execute the query. + * If null, the Mongo connection returned by [[modelClass]] will be used. + * @return array 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\mongodb\Connection $db the Mongo connection used to execute the query. + * If null, the Mongo connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one($db = null) + { + $row = parent::one($db); + if ($row !== false) { + 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); + } +} \ No newline at end of file diff --git a/extensions/mongodb/file/ActiveRecord.php b/extensions/mongodb/file/ActiveRecord.php new file mode 100644 index 0000000..014accf --- /dev/null +++ b/extensions/mongodb/file/ActiveRecord.php @@ -0,0 +1,340 @@ +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 + * @since 2.0 + */ +abstract class ActiveRecord extends \yii\mongodb\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.'); + } + } +} \ No newline at end of file diff --git a/extensions/mongodb/file/ActiveRelation.php b/extensions/mongodb/file/ActiveRelation.php new file mode 100644 index 0000000..ea1f7e6 --- /dev/null +++ b/extensions/mongodb/file/ActiveRelation.php @@ -0,0 +1,22 @@ + + * @since 2.0 + */ +class ActiveRelation extends ActiveQuery implements ActiveRelationInterface +{ + use ActiveRelationTrait; +} \ No newline at end of file diff --git a/extensions/mongodb/file/Collection.php b/extensions/mongodb/file/Collection.php new file mode 100644 index 0000000..064d4bc --- /dev/null +++ b/extensions/mongodb/file/Collection.php @@ -0,0 +1,186 @@ + + * @since 2.0 + */ +class Collection extends \yii\mongodb\Collection +{ + /** + * @var \MongoGridFS Mongo GridFS collection instance. + */ + public $mongoCollection; + /** + * @var \yii\mongodb\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\mongodb\Collection mongo collection instance. + */ + public function getChunkCollection($refresh = false) + { + if ($refresh || !is_object($this->_chunkCollection)) { + $this->_chunkCollection = Yii::createObject([ + 'class' => 'yii\mongodb\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); + } + } +} \ No newline at end of file diff --git a/extensions/mongodb/file/Query.php b/extensions/mongodb/file/Query.php new file mode 100644 index 0000000..d49da94 --- /dev/null +++ b/extensions/mongodb/file/Query.php @@ -0,0 +1,73 @@ + + * @since 2.0 + */ +class Query extends \yii\mongodb\Query +{ + /** + * Returns the Mongo collection for this query. + * @param \yii\mongodb\Connection $db Mongo connection. + * @return Collection collection instance. + */ + public function getCollection($db = null) + { + if ($db === null) { + $db = Yii::$app->getComponent('mongodb'); + } + 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; + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/mongo/ActiveRecord.php b/tests/unit/data/ar/mongo/ActiveRecord.php deleted file mode 100644 index 6f5bc49..0000000 --- a/tests/unit/data/ar/mongo/ActiveRecord.php +++ /dev/null @@ -1,16 +0,0 @@ -andWhere(['status' => 2]); - } - - public function getOrders() - { - return $this->hasMany(CustomerOrder::className(), ['customer_id' => '_id']); - } -} \ No newline at end of file diff --git a/tests/unit/data/ar/mongo/CustomerOrder.php b/tests/unit/data/ar/mongo/CustomerOrder.php deleted file mode 100644 index a01e47f..0000000 --- a/tests/unit/data/ar/mongo/CustomerOrder.php +++ /dev/null @@ -1,27 +0,0 @@ -hasOne(Customer::className(), ['_id' => 'customer_id']); - } -} \ No newline at end of file diff --git a/tests/unit/data/ar/mongo/file/ActiveRecord.php b/tests/unit/data/ar/mongo/file/ActiveRecord.php deleted file mode 100644 index 70ebeb2..0000000 --- a/tests/unit/data/ar/mongo/file/ActiveRecord.php +++ /dev/null @@ -1,16 +0,0 @@ -andWhere(['status' => 2]); - } -} \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/ActiveRecord.php b/tests/unit/data/ar/mongodb/ActiveRecord.php new file mode 100644 index 0000000..b0709a8 --- /dev/null +++ b/tests/unit/data/ar/mongodb/ActiveRecord.php @@ -0,0 +1,16 @@ +andWhere(['status' => 2]); + } + + public function getOrders() + { + return $this->hasMany(CustomerOrder::className(), ['customer_id' => '_id']); + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/CustomerOrder.php b/tests/unit/data/ar/mongodb/CustomerOrder.php new file mode 100644 index 0000000..f037aae --- /dev/null +++ b/tests/unit/data/ar/mongodb/CustomerOrder.php @@ -0,0 +1,27 @@ +hasOne(Customer::className(), ['_id' => 'customer_id']); + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/file/ActiveRecord.php b/tests/unit/data/ar/mongodb/file/ActiveRecord.php new file mode 100644 index 0000000..8ebc1b4 --- /dev/null +++ b/tests/unit/data/ar/mongodb/file/ActiveRecord.php @@ -0,0 +1,16 @@ +andWhere(['status' => 2]); + } +} \ No newline at end of file diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index 7b23a8d..a3dfdd4 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -52,7 +52,7 @@ return [ 'fixture' => __DIR__ . '/sphinx/source.sql', ], ], - 'mongo' => [ + 'mongodb' => [ 'dsn' => 'mongodb://travis:test@localhost:27017', 'defaultDatabaseName' => 'yii2test', 'options' => [], diff --git a/tests/unit/extensions/mongo/ActiveDataProviderTest.php b/tests/unit/extensions/mongo/ActiveDataProviderTest.php deleted file mode 100644 index 3660516..0000000 --- a/tests/unit/extensions/mongo/ActiveDataProviderTest.php +++ /dev/null @@ -1,91 +0,0 @@ -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)); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/ActiveRecordTest.php b/tests/unit/extensions/mongo/ActiveRecordTest.php deleted file mode 100644 index 8467ba0..0000000 --- a/tests/unit/extensions/mongo/ActiveRecordTest.php +++ /dev/null @@ -1,246 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/ActiveRelationTest.php b/tests/unit/extensions/mongo/ActiveRelationTest.php deleted file mode 100644 index 26cf63e..0000000 --- a/tests/unit/extensions/mongo/ActiveRelationTest.php +++ /dev/null @@ -1,83 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/CollectionTest.php b/tests/unit/extensions/mongo/CollectionTest.php deleted file mode 100644 index 153ffa9..0000000 --- a/tests/unit/extensions/mongo/CollectionTest.php +++ /dev/null @@ -1,313 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/ConnectionTest.php b/tests/unit/extensions/mongo/ConnectionTest.php deleted file mode 100644 index 04d5351..0000000 --- a/tests/unit/extensions/mongo/ConnectionTest.php +++ /dev/null @@ -1,119 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/DatabaseTest.php b/tests/unit/extensions/mongo/DatabaseTest.php deleted file mode 100644 index 6847d2e..0000000 --- a/tests/unit/extensions/mongo/DatabaseTest.php +++ /dev/null @@ -1,70 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/MongoTestCase.php b/tests/unit/extensions/mongo/MongoTestCase.php deleted file mode 100644 index 291debd..0000000 --- a/tests/unit/extensions/mongo/MongoTestCase.php +++ /dev/null @@ -1,149 +0,0 @@ - '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']; - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/QueryRunTest.php b/tests/unit/extensions/mongo/QueryRunTest.php deleted file mode 100644 index 121e826..0000000 --- a/tests/unit/extensions/mongo/QueryRunTest.php +++ /dev/null @@ -1,144 +0,0 @@ -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)); - } - - public function testLike() - { - $connection = $this->getConnection(); - $query = new Query; - $rows = $query->from('customer') - ->where(['LIKE', 'name', '/me1/']) - ->all($connection); - $this->assertEquals(2, count($rows)); - $this->assertEquals('name1', $rows[0]['name']); - $this->assertEquals('name10', $rows[1]['name']); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/QueryTest.php b/tests/unit/extensions/mongo/QueryTest.php deleted file mode 100644 index 35d45e0..0000000 --- a/tests/unit/extensions/mongo/QueryTest.php +++ /dev/null @@ -1,97 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/file/ActiveRecordTest.php b/tests/unit/extensions/mongo/file/ActiveRecordTest.php deleted file mode 100644 index 93fb552..0000000 --- a/tests/unit/extensions/mongo/file/ActiveRecordTest.php +++ /dev/null @@ -1,323 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/file/CollectionTest.php b/tests/unit/extensions/mongo/file/CollectionTest.php deleted file mode 100644 index 58a5864..0000000 --- a/tests/unit/extensions/mongo/file/CollectionTest.php +++ /dev/null @@ -1,98 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/file/QueryTest.php b/tests/unit/extensions/mongo/file/QueryTest.php deleted file mode 100644 index 2f9ec67..0000000 --- a/tests/unit/extensions/mongo/file/QueryTest.php +++ /dev/null @@ -1,70 +0,0 @@ -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']); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/ActiveDataProviderTest.php b/tests/unit/extensions/mongodb/ActiveDataProviderTest.php new file mode 100644 index 0000000..350769d --- /dev/null +++ b/tests/unit/extensions/mongodb/ActiveDataProviderTest.php @@ -0,0 +1,91 @@ +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)); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/ActiveRecordTest.php b/tests/unit/extensions/mongodb/ActiveRecordTest.php new file mode 100644 index 0000000..df24500 --- /dev/null +++ b/tests/unit/extensions/mongodb/ActiveRecordTest.php @@ -0,0 +1,246 @@ +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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/ActiveRelationTest.php b/tests/unit/extensions/mongodb/ActiveRelationTest.php new file mode 100644 index 0000000..695f972 --- /dev/null +++ b/tests/unit/extensions/mongodb/ActiveRelationTest.php @@ -0,0 +1,83 @@ +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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/CollectionTest.php b/tests/unit/extensions/mongodb/CollectionTest.php new file mode 100644 index 0000000..a01568d --- /dev/null +++ b/tests/unit/extensions/mongodb/CollectionTest.php @@ -0,0 +1,313 @@ +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->mongoDbConfig['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\mongodb\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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/ConnectionTest.php b/tests/unit/extensions/mongodb/ConnectionTest.php new file mode 100644 index 0000000..e96acab --- /dev/null +++ b/tests/unit/extensions/mongodb/ConnectionTest.php @@ -0,0 +1,119 @@ +getConnection(false); + $params = $this->mongoDbConfig; + + $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\mongodb\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->mongoDbConfig['dsn']; + $connection->defaultDatabaseName = $this->mongoDbConfig['defaultDatabaseName']; + $database = $connection->getDatabase(); + $this->assertTrue($database instanceof Database, 'Unable to get default database!'); + + $connection = new Connection(); + $connection->dsn = $this->mongoDbConfig['dsn']; + $connection->options = ['db' => $this->mongoDbConfig['defaultDatabaseName']]; + $database = $connection->getDatabase(); + $this->assertTrue($database instanceof Database, 'Unable to determine default database from options!'); + + $connection = new Connection(); + $connection->dsn = $this->mongoDbConfig['dsn'] . '/' . $this->mongoDbConfig['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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/DatabaseTest.php b/tests/unit/extensions/mongodb/DatabaseTest.php new file mode 100644 index 0000000..10b6f14 --- /dev/null +++ b/tests/unit/extensions/mongodb/DatabaseTest.php @@ -0,0 +1,70 @@ +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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/MongoDbTestCase.php b/tests/unit/extensions/mongodb/MongoDbTestCase.php new file mode 100644 index 0000000..8177c44 --- /dev/null +++ b/tests/unit/extensions/mongodb/MongoDbTestCase.php @@ -0,0 +1,149 @@ + 'mongodb://localhost:27017', + 'defaultDatabaseName' => 'yii2test', + 'options' => [], + ]; + /** + * @var Connection Mongo connection instance. + */ + protected $mongodb; + + public static function setUpBeforeClass() + { + static::loadClassMap(); + } + + protected function setUp() + { + parent::setUp(); + if (!extension_loaded('mongo')) { + $this->markTestSkipped('mongo extension required.'); + } + $config = $this->getParam('mongodb'); + if (!empty($config)) { + $this->mongoDbConfig = $config; + } + $this->mockApplication(); + static::loadClassMap(); + } + + protected function tearDown() + { + if ($this->mongodb) { + $this->mongodb->close(); + } + $this->destroyApplication(); + } + + /** + * Adds sphinx extension files to [[Yii::$classPath]], + * avoiding the necessity of usage Composer autoloader. + */ + protected static function loadClassMap() + { + $baseNameSpace = 'yii/mongodb'; + $basePath = realpath(__DIR__. '/../../../../extensions/mongodb'); + $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\mongodb\Connection + */ + public function getConnection($reset = false, $open = true) + { + if (!$reset && $this->mongodb) { + return $this->mongodb; + } + $db = new Connection; + $db->dsn = $this->mongoDbConfig['dsn']; + $db->defaultDatabaseName = $this->mongoDbConfig['defaultDatabaseName']; + if (isset($this->mongoDbConfig['options'])) { + $db->options = $this->mongoDbConfig['options']; + } + if ($open) { + $db->open(); + } + $this->mongodb = $db; + return $db; + } + + /** + * Drops the specified collection. + * @param string $name collection name. + */ + protected function dropCollection($name) + { + if ($this->mongodb) { + try { + $this->mongodb->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->mongodb) { + try { + $this->mongodb->getFileCollection($name)->drop(); + } catch (Exception $e) { + // shut down exception + } + } + } + + /** + * Finds all records in collection. + * @param \yii\mongodb\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']; + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/QueryRunTest.php b/tests/unit/extensions/mongodb/QueryRunTest.php new file mode 100644 index 0000000..c295ac8 --- /dev/null +++ b/tests/unit/extensions/mongodb/QueryRunTest.php @@ -0,0 +1,144 @@ +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)); + } + + public function testLike() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where(['LIKE', 'name', '/me1/']) + ->all($connection); + $this->assertEquals(2, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + $this->assertEquals('name10', $rows[1]['name']); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/QueryTest.php b/tests/unit/extensions/mongodb/QueryTest.php new file mode 100644 index 0000000..1ac7ea6 --- /dev/null +++ b/tests/unit/extensions/mongodb/QueryTest.php @@ -0,0 +1,97 @@ +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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/file/ActiveRecordTest.php b/tests/unit/extensions/mongodb/file/ActiveRecordTest.php new file mode 100644 index 0000000..a15733d --- /dev/null +++ b/tests/unit/extensions/mongodb/file/ActiveRecordTest.php @@ -0,0 +1,323 @@ +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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/file/CollectionTest.php b/tests/unit/extensions/mongodb/file/CollectionTest.php new file mode 100644 index 0000000..2e67511 --- /dev/null +++ b/tests/unit/extensions/mongodb/file/CollectionTest.php @@ -0,0 +1,98 @@ +dropFileCollection('fs'); + parent::tearDown(); + } + + // Tests : + + public function testGetChunkCollection() + { + $collection = $this->getConnection()->getFileCollection(); + $chunkCollection = $collection->getChunkCollection(); + $this->assertTrue($chunkCollection instanceof \yii\mongodb\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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/file/QueryTest.php b/tests/unit/extensions/mongodb/file/QueryTest.php new file mode 100644 index 0000000..c5a0c4f --- /dev/null +++ b/tests/unit/extensions/mongodb/file/QueryTest.php @@ -0,0 +1,70 @@ +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']); + } +} \ No newline at end of file