DbFixtureManager.php 7.38 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\test;

use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
13
use yii\db\ActiveRecord;
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
use yii\db\Connection;

/**
 * DbFixtureManager manages database fixtures during tests.
 *
 * A fixture represents a list of rows for a specific table. For a test method,
 * using a fixture means that at the beginning of the method, the table has and only
 * has the rows that are given in the fixture. Therefore, the table's state is
 * predictable.
 *
 * A fixture is represented as a PHP script whose name (without suffix) is the
 * same as the table name (if schema name is needed, it should be prefixed to
 * the table name). The PHP script returns an array representing a list of table
 * rows. Each row is an associative array of column values indexed by column names.
 *
 * Fixtures must be stored under the [[basePath]] directory. The directory
 * may contain a file named `init.php` which will be executed before any fixture is loaded.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @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
Paul Kofmann committed
54
	 * [''], meaning using the default schema (an empty string refers to the
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
	 * 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)


	/**
	 * 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;
94
				if ($model instanceof ActiveRecord) {
95 96 97 98 99 100 101
					$fixtures[$name] = $model->getTableSchema()->name;
				} else {
					throw new InvalidConfigException("Fixture '$fixture' must be an ActiveRecord class.");
				}
			}
		}

102
		$this->_rows = $this->_models = [];
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144

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

145
		$this->db->createCommand()->truncateTable($tableName)->execute();
Carsten Brandt committed
146
		$this->db->createCommand()->resetSequence($tableName, 1)->execute();
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

		$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
185
	 * @return ActiveRecord the ActiveRecord instance. Null is returned if there is no such fixture row.
186 187 188
	 */
	public function getModel($fixtureName, $modelName)
	{
189
		if (!isset($this->_rows[$fixtureName][$modelName])) {
190 191 192 193 194 195
			return null;
		}
		if (isset($this->_models[$fixtureName][$modelName])) {
			return $this->_models[$fixtureName][$modelName];
		}
		$row = $this->_rows[$fixtureName][$modelName];
196
		/** @var ActiveRecord $modelClass */
197
		$modelClass = $this->_models[$fixtureName];
198
		/** @var ActiveRecord $model */
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
		$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) {
215
			$this->db->createCommand()->checkIntegrity($check, $schema)->execute();
216 217 218
		}
	}
}