diff --git a/extensions/mongo/ActiveRecord.php b/extensions/mongo/ActiveRecord.php index 41c1d7a..b0a360e 100644 --- a/extensions/mongo/ActiveRecord.php +++ b/extensions/mongo/ActiveRecord.php @@ -250,7 +250,7 @@ abstract class ActiveRecord extends BaseActiveRecord } /** - * @see CActiveRecord::update() + * @see ActiveRecord::update() * @throws StaleObjectException */ protected function updateInternal($attributes = null) @@ -309,24 +309,34 @@ abstract class ActiveRecord extends BaseActiveRecord { $result = false; if ($this->beforeDelete()) { - // 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); + $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. diff --git a/extensions/mongo/file/ActiveRecord.php b/extensions/mongo/file/ActiveRecord.php index 35f7ad1..36fe644 100644 --- a/extensions/mongo/file/ActiveRecord.php +++ b/extensions/mongo/file/ActiveRecord.php @@ -7,6 +7,10 @@ namespace yii\mongo\file; +use yii\base\InvalidParamException; +use yii\db\StaleObjectException; +use yii\web\UploadedFile; + /** * ActiveRecord is the base class for classes representing Mongo GridFS files in terms of objects. * @@ -16,11 +20,6 @@ namespace yii\mongo\file; class ActiveRecord extends \yii\mongo\ActiveRecord { /** - * @var \MongoGridFSFile|string - */ - public $file; - - /** * 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 @@ -54,29 +53,6 @@ class ActiveRecord extends \yii\mongo\ActiveRecord } /** - * Creates an active record object using a row of data. - * This method is called by [[ActiveQuery]] to populate the query results - * into Active Records. It is not meant to be used to create new records. - * @param \MongoGridFSFile $row attribute values (name => value) - * @return ActiveRecord the newly created active record. - */ - public static function create($row) - { - $record = static::instantiate($row); - $columns = array_flip($record->attributes()); - foreach ($row->file as $name => $value) { - if (isset($columns[$name])) { - $record->setAttribute($name, $value); - } else { - $record->$name = $value; - } - } - $record->setOldAttributes($record->getAttributes()); - $record->afterFind(); - return $record; - } - - /** * Returns the list of all attribute names of the model. * This method could be overridden by child classes to define available attributes. * Note: primary key attribute "_id" should be always present in returned array. @@ -84,7 +60,16 @@ class ActiveRecord extends \yii\mongo\ActiveRecord */ public function attributes() { - return ['id', 'filename']; + return [ + '_id', + 'filename', + 'uploadDate', + 'length', + 'chunkSize', + 'md5', + 'file', + 'newFileContent' + ]; } /** @@ -103,7 +88,30 @@ class ActiveRecord extends \yii\mongo\ActiveRecord } } $collection = static::getCollection(); - $newId = $collection->insert($values); + if (array_key_exists('newFileContent', $values)) { + $fileContent = $values['newFileContent']; + unset($values['newFileContent']); + unset($values['file']); + $newId = $collection->storeBytes($fileContent, $values); + } elseif (array_key_exists('file', $values)) { + $file = $values['file']; + if ($file instanceof UploadedFile) { + $fileName = $file->tempName; + } elseif (is_string($file)) { + if (file_exists($file)) { + $fileName = $file; + } else { + throw new InvalidParamException("File '{$file}' does not exist."); + } + } else { + throw new InvalidParamException('Unsupported type of "file" attribute.'); + } + unset($values['newFileContent']); + unset($values['file']); + $newId = $collection->storeFile($fileName, $values); + } else { + $newId = $collection->insert($values); + } $this->setAttribute('_id', $newId); foreach ($values as $name => $value) { $this->setOldAttribute($name, $value); @@ -113,7 +121,7 @@ class ActiveRecord extends \yii\mongo\ActiveRecord } /** - * @see CActiveRecord::update() + * @see ActiveRecord::update() * @throws StaleObjectException */ protected function updateInternal($attributes = null) @@ -126,20 +134,50 @@ class ActiveRecord extends \yii\mongo\ActiveRecord $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.'); + $collection = static::getCollection(); + if (array_key_exists('newFileContent', $values)) { + $fileContent = $values['newFileContent']; + unset($values['newFileContent']); + unset($values['file']); + $values['_id'] = $this->getAttribute('_id'); + $this->deleteInternal(); + $collection->storeBytes($fileContent, $values); + $rows = 1; + } elseif (array_key_exists('file', $values)) { + $file = $values['file']; + if ($file instanceof UploadedFile) { + $fileName = $file->tempName; + } elseif (is_string($file)) { + if (file_exists($file)) { + $fileName = $file; + } else { + throw new InvalidParamException("File '{$file}' does not exist."); + } + } else { + throw new InvalidParamException('Unsupported type of "file" attribute.'); + } + unset($values['newFileContent']); + unset($values['file']); + $values['_id'] = $this->getAttribute('_id'); + $this->deleteInternal(); + $collection->storeFile($fileName, $values); + $rows = 1; + } 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) { @@ -149,26 +187,26 @@ class ActiveRecord extends \yii\mongo\ActiveRecord return $rows; } - public function getContent() + /** + * Returns the associated file content. + * @return null|string file content. + * @throws \yii\base\InvalidParamException on invalid file value. + */ + public function getFileContent() { $file = $this->getAttribute('file'); if (empty($file)) { return null; - } - if ($file instanceof \MongoGridFSFile) { + } elseif ($file instanceof \MongoGridFSFile) { 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('Unsupported type of "file" attribute.'); } } - - public function getFileName() - { - $file = $this->getAttribute('file'); - if (empty($file)) { - return null; - } - if ($file instanceof \MongoGridFSFile) { - return $file->getFilename(); - } - } - } \ No newline at end of file diff --git a/extensions/mongo/file/Query.php b/extensions/mongo/file/Query.php index 6cf2215..b22b64f 100644 --- a/extensions/mongo/file/Query.php +++ b/extensions/mongo/file/Query.php @@ -8,6 +8,8 @@ namespace yii\mongo\file; use Yii; +use yii\helpers\Json; +use yii\mongo\Exception; /** * Class Query @@ -29,4 +31,52 @@ class Query extends \yii\mongo\Query } return $db->getFileCollection($this->from); } + + /** + * 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(\MongoCursor $cursor, $all = true, $indexBy = null) + { + $token = 'Querying: ' . Json::encode($cursor->info()); + Yii::info($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); + $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; + } + } + Yii::endProfile($token, __METHOD__); + return $result; + } 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/tests/unit/data/ar/mongo/file/ActiveRecord.php b/tests/unit/data/ar/mongo/file/ActiveRecord.php new file mode 100644 index 0000000..70ebeb2 --- /dev/null +++ b/tests/unit/data/ar/mongo/file/ActiveRecord.php @@ -0,0 +1,16 @@ +andWhere(['status' => 2]); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/file/ActiveRecordTest.php b/tests/unit/extensions/mongo/file/ActiveRecordTest.php new file mode 100644 index 0000000..b4ab890 --- /dev/null +++ b/tests/unit/extensions/mongo/file/ActiveRecordTest.php @@ -0,0 +1,119 @@ +getConnection(); + $this->setUpTestRows(); + } + + protected function tearDown() + { + $this->dropFileCollection(CustomerFile::collectionName()); + parent::tearDown(); + } + + /** + * 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->storeBytes($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); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/file/QueryTest.php b/tests/unit/extensions/mongo/file/QueryTest.php index b4c2f0f..2fdf1f1 100644 --- a/tests/unit/extensions/mongo/file/QueryTest.php +++ b/tests/unit/extensions/mongo/file/QueryTest.php @@ -51,7 +51,8 @@ class QueryTest extends MongoTestCase $connection = $this->getConnection(); $query = new Query; $row = $query->from('fs')->one($connection); - $this->assertTrue($row instanceof \MongoGridFSFile); + $this->assertTrue(is_array($row)); + $this->assertTrue($row['file'] instanceof \MongoGridFSFile); } public function testDirectMatch() @@ -64,6 +65,6 @@ class QueryTest extends MongoTestCase $this->assertEquals(1, count($rows)); /** @var $file \MongoGridFSFile */ $file = $rows[0]; - $this->assertEquals('name5', $file->file['filename']); + $this->assertEquals('name5', $file['filename']); } } \ No newline at end of file