diff --git a/framework/yii/test/DbFixtureManager.php b/framework/yii/test/DbFixtureManager.php new file mode 100644 index 0000000..ed90284 --- /dev/null +++ b/framework/yii/test/DbFixtureManager.php @@ -0,0 +1,219 @@ + + * @since 2.0 + */ +class DbFixtureManager extends Component +{ + /** + * @var string the init script file that should be executed before running each test. + * This should be a path relative to [[basePath]]. + */ + public $initScript = 'init.php'; + /** + * @var string the base path containing all fixtures. This can be either a directory path or path alias. + */ + public $basePath = '@app/tests/fixtures'; + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbFixtureManager object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var array list of database schemas that the test tables may reside in. Defaults to + * array(''), meaning using the default schema (an empty string refers to the + * default schema). This property is mainly used when turning on and off integrity checks + * so that fixture data can be populated into the database without causing problem. + */ + public $schemas = ['']; + + private $_rows; // fixture name, row alias => row + private $_models; // fixture name, row alias => record (or class name) + private $_modelClasses; + + + /** + * Loads the specified fixtures. + * + * This method does the following things to load the fixtures: + * + * - Run [[initScript]] if any. + * - Clean up data and models loaded in memory previously. + * - Load each specified fixture by calling [[loadFixture()]]. + * + * @param array $fixtures a list of fixtures (fixture name => table name or AR class name) to be loaded. + * Each array element can be either a table name (with schema prefix if needed), or a fully-qualified + * ActiveRecord class name (e.g. `app\models\Post`). An element can be associated with a key + * which will be treated as the fixture name. + * @return array the loaded fixture data (fixture name => table rows) + * @throws InvalidConfigException if a model class specifying a fixture is not an ActiveRecord class. + */ + public function load(array $fixtures = []) + { + $this->basePath = Yii::getAlias($this->basePath); + + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("The 'db' property must be either a DB connection instance or the application component ID of a DB connection."); + } + + foreach ($fixtures as $name => $fixture) { + if (strpos($fixture, '\\') !== false) { + $model = new $fixture; + if ($model instanceof ActiveRecord) { + $this->_modelClasses[$name] = $fixture; + $fixtures[$name] = $model->getTableSchema()->name; + } else { + throw new InvalidConfigException("Fixture '$fixture' must be an ActiveRecord class."); + } + } + } + + $this->_modelClasses = $this->_rows = $this->_models = []; + + $this->checkIntegrity(false); + + if (!empty($this->initScript)) { + $initFile = $this->basePath . '/' . $this->initScript; + if (is_file($initFile)) { + require($initFile); + } + } + + foreach ($fixtures as $name => $tableName) { + $rows = $this->loadFixture($tableName); + if (is_array($rows)) { + $this->_rows[$name] = $rows; + } + } + $this->checkIntegrity(true); + return $this->_rows; + } + + /** + * Loads the fixture for the specified table. + * + * This method does the following tasks to load the fixture for a table: + * + * - Remove existing rows in the table. + * - If there is any auto-incremental column, the corresponding sequence will be reset to 0. + * - If a fixture file is found, it will be executed, and its return value will be treated + * as rows which will then be inserted into the table. + * + * @param string $tableName table name + * @return array|boolean the loaded fixture rows indexed by row aliases (if any). + * False is returned if the table does not have a fixture. + * @throws InvalidConfigException if the specified table does not exist + */ + public function loadFixture($tableName) + { + $table = $this->db->getSchema()->getTableSchema($tableName); + if ($table === null) { + throw new InvalidConfigException("Table does not exist: $tableName"); + } + + $this->db->createCommand()->truncateTable($tableName); + + $fileName = $this->basePath . '/' . $tableName . '.php'; + if (!is_file($fileName)) { + return false; + } + + $rows = []; + foreach (require($fileName) as $alias => $row) { + $this->db->createCommand()->insert($tableName, $row)->execute(); + if ($table->sequenceName !== null) { + foreach ($table->primaryKey as $pk) { + if (!isset($row[$pk])) { + $row[$pk] = $this->db->getLastInsertID($table->sequenceName); + break; + } + } + } + $rows[$alias] = $row; + } + + return $rows; + } + + /** + * Returns the fixture data rows. + * The rows will have updated primary key values if the primary key is auto-incremental. + * @param string $fixtureName the fixture name + * @return array the fixture data rows. False is returned if there is no such fixture data. + */ + public function getRows($fixtureName) + { + return isset($this->_rows[$fixtureName]) ? $this->_rows[$fixtureName] : false; + } + + /** + * Returns the specified ActiveRecord instance in the fixture data. + * @param string $fixtureName the fixture name + * @param string $modelName the alias for the fixture data row + * @return \yii\db\ActiveRecord the ActiveRecord instance. Null is returned if there is no such fixture row. + */ + public function getModel($fixtureName, $modelName) + { + if (!isset($this->_modelClasses[$fixtureName]) || !isset($this->_rows[$fixtureName][$modelName])) { + return null; + } + if (isset($this->_models[$fixtureName][$modelName])) { + return $this->_models[$fixtureName][$modelName]; + } + $row = $this->_rows[$fixtureName][$modelName]; + /** @var \yii\db\ActiveRecord $modelClass */ + $modelClass = $this->_models[$fixtureName]; + /** @var \yii\db\ActiveRecord $model */ + $model = new $modelClass; + $keys = []; + foreach ($model->primaryKey() as $key) { + $keys[$key] = isset($row[$key]) ? $row[$key] : null; + } + return $this->_models[$fixtureName][$modelName] = $modelClass::find($keys); + } + + /** + * Enables or disables database integrity check. + * This method may be used to temporarily turn off foreign constraints check. + * @param boolean $check whether to enable database integrity check + */ + public function checkIntegrity($check) + { + foreach ($this->schemas as $schema) { + $this->db->createCommand()->checkIntegrity($check, $schema); + } + } +} diff --git a/framework/yii/test/DbTestTrait.php b/framework/yii/test/DbTestTrait.php new file mode 100644 index 0000000..8a6dc3c --- /dev/null +++ b/framework/yii/test/DbTestTrait.php @@ -0,0 +1,110 @@ +loadFixtures([ + * 'posts' => Post::className(), + * 'users' => User::className(), + * ]); + * } + * } + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +trait DbTestTrait +{ + /** + * Loads the specified fixtures. + * + * This method should typically be called in the setup method of test cases so that + * the fixtures are loaded before running each test method. + * + * This method does the following things: + * + * - Run [[DbFixtureManager::initScript]] if it is found under [[DbFixtureManager::basePath]]. + * - Clean up data and models loaded in memory previously. + * - Load each specified fixture: + * * Truncate the corresponding table. + * * If a fixture file named `TableName.php` is found under [[DbFixtureManager::basePath]], + * the file will be executed, and the return value will be treated as rows which will + * then be inserted into the table. + * + * @param array $fixtures a list of fixtures (fixture name => table name or AR class name) to be loaded. + * Each array element can be either a table name (with schema prefix if needed), or a fully-qualified + * ActiveRecord class name (e.g. `app\models\Post`). An element can be optionally associated with a key + * which will be treated as the fixture name. For example, + * + * ~~~ + * [ + * 'tbl_comment', + * 'users' => 'tbl_user', // 'users' is the fixture name, 'tbl_user' is a table name + * 'posts' => 'app\models\Post, // 'app\models\Post' is a model class name + * ] + * ~~~ + * + * @return array the loaded fixture data (fixture name => table rows) + */ + public function loadFixtures(array $fixtures = []) + { + return $this->getFixtureManager()->load($fixtures); + } + + /** + * Returns the DB fixture manager. + * @return DbFixtureManager the DB fixture manager + */ + public function getFixtureManager() + { + return Yii::$app->getComponent('fixture'); + } + + /** + * Returns the table rows of the named fixture. + * @param string $fixtureName the fixture name. + * @return array the named fixture table rows. False is returned if there is no such fixture data. + */ + public function getFixtureRows($fixtureName) + { + return $this->getFixtureManager()->getRows($fixtureName); + } + + /** + * Returns the named AR instance corresponding to the named fixture. + * @param string $fixtureName the fixture name. + * @param string $modelName the name of the fixture data row + * @return \yii\db\ActiveRecord the named AR instance corresponding to the named fixture. + * Null is returned if there is no such fixture or the record cannot be found. + */ + public function getFixtureModel($fixtureName, $modelName) + { + return $this->getFixtureManager()->getModel($fixtureName, $modelName); + } +}