1: <?php
2: /**
3: * This file contains the CDbFixtureManager class.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CDbFixtureManager manages database fixtures during tests.
13: *
14: * A fixture represents a list of rows for a specific table. For a test method,
15: * using a fixture means that at the beginning of the method, the table has and only
16: * has the rows that are given in the fixture. Therefore, the table's state is
17: * predictable.
18: *
19: * A fixture is represented as a PHP script whose name (without suffix) is the
20: * same as the table name (if schema name is needed, it should be prefixed to
21: * the table name). The PHP script returns an array representing a list of table
22: * rows. Each row is an associative array of column values indexed by column names.
23: *
24: * A fixture can be associated with an init script which sits under the same fixture
25: * directory and is named as "TableName.init.php". The init script is used to
26: * initialize the table before populating the fixture data into the table.
27: * If the init script does not exist, the table will be emptied.
28: *
29: * Fixtures must be stored under the {@link basePath} directory. The directory
30: * may contain a file named "init.php" which will be executed once to initialize
31: * the database. If this file is not found, all available fixtures will be loaded
32: * into the database.
33: *
34: * @property CDbConnection $dbConnection The database connection.
35: * @property array $fixtures The information of the available fixtures (table name => fixture file).
36: *
37: * @author Qiang Xue <qiang.xue@gmail.com>
38: * @package system.test
39: * @since 1.1
40: */
41: class CDbFixtureManager extends CApplicationComponent
42: {
43: /**
44: * @var string the name of the initialization script that would be executed before the whole test set runs.
45: * Defaults to 'init.php'. If the script does not exist, every table with a fixture file will be reset.
46: */
47: public $initScript='init.php';
48: /**
49: * @var string the suffix for fixture initialization scripts.
50: * If a table is associated with such a script whose name is TableName suffixed this property value,
51: * then the script will be executed each time before the table is reset.
52: */
53: public $initScriptSuffix='.init.php';
54: /**
55: * @var string the base path containing all fixtures. Defaults to null, meaning
56: * the path 'protected/tests/fixtures'.
57: */
58: public $basePath;
59: /**
60: * @var string the ID of the database connection. Defaults to 'db'.
61: * Note, data in this database may be deleted or modified during testing.
62: * Make sure you have a backup database.
63: */
64: public $connectionID='db';
65: /**
66: * @var array list of database schemas that the test tables may reside in. Defaults to
67: * array(''), meaning using the default schema (an empty string refers to the
68: * default schema). This property is mainly used when turning on and off integrity checks
69: * so that fixture data can be populated into the database without causing problem.
70: */
71: public $schemas=array('');
72:
73: private $_db;
74: private $_fixtures;
75: /* x2modstart */
76: // private changed to protected
77: protected $_rows; // fixture name, row alias => row
78: protected $_records; // fixture name, row alias => record (or class name)
79: /* x2modend */
80:
81:
82: /**
83: * Initializes this application component.
84: */
85: public function init()
86: {
87: parent::init();
88: if($this->basePath===null)
89: $this->basePath=Yii::getPathOfAlias('application.tests.fixtures');
90: $this->prepare();
91: }
92:
93: /**
94: * Returns the database connection used to load fixtures.
95: * @throws CException if {@link connectionID} application component is invalid
96: * @return CDbConnection the database connection
97: */
98: public function getDbConnection()
99: {
100: if($this->_db===null)
101: {
102: $this->_db=Yii::app()->getComponent($this->connectionID);
103: if(!$this->_db instanceof CDbConnection)
104: throw new CException(Yii::t('yii','CDbTestFixture.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.',
105: array('{id}'=>$this->connectionID)));
106: }
107: return $this->_db;
108: }
109:
110: /**
111: * Prepares the fixtures for the whole test.
112: * This method is invoked in {@link init}. It executes the database init script
113: * if it exists. Otherwise, it will load all available fixtures.
114: */
115: public function prepare()
116: {
117: $initFile=$this->basePath . DIRECTORY_SEPARATOR . $this->initScript;
118:
119: $this->checkIntegrity(false);
120:
121: if(is_file($initFile))
122: require($initFile);
123: else
124: {
125: foreach($this->getFixtures() as $tableName=>$fixturePath)
126: {
127: $this->resetTable($tableName);
128: $this->loadFixture($tableName);
129: }
130: }
131: $this->checkIntegrity(true);
132: }
133:
134: /**
135: * Resets the table to the state that it contains no fixture data.
136: * If there is an init script named "tests/fixtures/TableName.init.php",
137: * the script will be executed.
138: * Otherwise, {@link truncateTable} will be invoked to delete all rows in the table
139: * and reset primary key sequence, if any.
140: * @param string $tableName the table name
141: */
142: public function resetTable($tableName)
143: {
144: $initFile=$this->basePath . DIRECTORY_SEPARATOR . $tableName . $this->initScriptSuffix;
145: if(is_file($initFile))
146: require($initFile);
147: else
148: $this->truncateTable($tableName);
149: }
150:
151: /**
152: * Loads the fixture for the specified table.
153: * This method will insert rows given in the fixture into the corresponding table.
154: * The loaded rows will be returned by this method.
155: * If the table has auto-incremental primary key, each row will contain updated primary key value.
156: * If the fixture does not exist, this method will return false.
157: * Note, you may want to call {@link resetTable} before calling this method
158: * so that the table is emptied first.
159: * @param string $tableName table name
160: * @return array the loaded fixture rows indexed by row aliases (if any).
161: * False is returned if the table does not have a fixture.
162: */
163: public function loadFixture($tableName)
164: {
165: $fileName=$this->basePath.DIRECTORY_SEPARATOR.$tableName.'.php';
166: if(!is_file($fileName))
167: return false;
168:
169: $rows=array();
170: $schema=$this->getDbConnection()->getSchema();
171: $builder=$schema->getCommandBuilder();
172: $table=$schema->getTable($tableName);
173:
174: foreach(require($fileName) as $alias=>$row)
175: {
176: $builder->createInsertCommand($table,$row)->execute();
177: $primaryKey=$table->primaryKey;
178: if($table->sequenceName!==null)
179: {
180: if(is_string($primaryKey) && !isset($row[$primaryKey]))
181: $row[$primaryKey]=$builder->getLastInsertID($table);
182: elseif(is_array($primaryKey))
183: {
184: foreach($primaryKey as $pk)
185: {
186: if(!isset($row[$pk]))
187: {
188: $row[$pk]=$builder->getLastInsertID($table);
189: break;
190: }
191: }
192: }
193: }
194: $rows[$alias]=$row;
195: }
196: return $rows;
197: }
198:
199: /**
200: * Returns the information of the available fixtures.
201: * This method will search for all PHP files under {@link basePath}.
202: * If a file's name is the same as a table name, it is considered to be the fixture data for that table.
203: * @return array the information of the available fixtures (table name => fixture file)
204: */
205: public function getFixtures()
206: {
207: if($this->_fixtures===null)
208: {
209: $this->_fixtures=array();
210: $schema=$this->getDbConnection()->getSchema();
211: $folder=opendir($this->basePath);
212: $suffixLen=strlen($this->initScriptSuffix);
213: while($file=readdir($folder))
214: {
215: if($file==='.' || $file==='..' || $file===$this->initScript)
216: continue;
217: $path=$this->basePath.DIRECTORY_SEPARATOR.$file;
218: if(substr($file,-4)==='.php' && is_file($path) && substr($file,-$suffixLen)!==$this->initScriptSuffix)
219: {
220: $tableName=substr($file,0,-4);
221: if($schema->getTable($tableName)!==null)
222: $this->_fixtures[$tableName]=$path;
223: }
224: }
225: closedir($folder);
226: }
227: return $this->_fixtures;
228: }
229:
230: /**
231: * Enables or disables database integrity check.
232: * This method may be used to temporarily turn off foreign constraints check.
233: * @param boolean $check whether to enable database integrity check
234: */
235: public function checkIntegrity($check)
236: {
237: foreach($this->schemas as $schema)
238: $this->getDbConnection()->getSchema()->checkIntegrity($check,$schema);
239: }
240:
241: /**
242: * Removes all rows from the specified table and resets its primary key sequence, if any.
243: * You may need to call {@link checkIntegrity} to turn off integrity check temporarily
244: * before you call this method.
245: * @param string $tableName the table name
246: * @throws CException if given table does not exist
247: */
248: public function truncateTable($tableName)
249: {
250: $db=$this->getDbConnection();
251: $schema=$db->getSchema();
252: if(($table=$schema->getTable($tableName))!==null)
253: {
254: $db->createCommand('DELETE FROM '.$table->rawName)->execute();
255: $schema->resetSequence($table,1);
256: }
257: else
258: throw new CException("Table '$tableName' does not exist.");
259: }
260:
261: /**
262: * Truncates all tables in the specified schema.
263: * You may need to call {@link checkIntegrity} to turn off integrity check temporarily
264: * before you call this method.
265: * @param string $schema the schema name. Defaults to empty string, meaning the default database schema.
266: * @see truncateTable
267: */
268: public function truncateTables($schema='')
269: {
270: $tableNames=$this->getDbConnection()->getSchema()->getTableNames($schema);
271: foreach($tableNames as $tableName)
272: $this->truncateTable($tableName);
273: }
274:
275: /**
276: * Loads the specified fixtures.
277: * For each fixture, the corresponding table will be reset first by calling
278: * {@link resetTable} and then be populated with the fixture data.
279: * The loaded fixture data may be later retrieved using {@link getRows}
280: * and {@link getRecord}.
281: * Note, if a table does not have fixture data, {@link resetTable} will still
282: * be called to reset the table.
283: * @param array $fixtures fixtures to be loaded. The array keys are fixture names,
284: * and the array values are either AR class names or table names.
285: * If table names, they must begin with a colon character (e.g. 'Post'
286: * means an AR class, while ':Post' means a table name).
287: */
288: public function load($fixtures)
289: {
290: $schema=$this->getDbConnection()->getSchema();
291: $schema->checkIntegrity(false);
292:
293: $this->_rows=array();
294: $this->_records=array();
295: foreach($fixtures as $fixtureName=>$tableName)
296: {
297: if($tableName[0]===':')
298: {
299: $tableName=substr($tableName,1);
300: unset($modelClass);
301: }
302: else
303: {
304: $modelClass=Yii::import($tableName,true);
305: $tableName=CActiveRecord::model($modelClass)->tableName();
306: }
307: if(($prefix=$this->getDbConnection()->tablePrefix)!==null)
308: $tableName=preg_replace('/{{(.*?)}}/',$prefix.'\1',$tableName);
309: $this->resetTable($tableName);
310: $rows=$this->loadFixture($tableName);
311: if(is_array($rows) && is_string($fixtureName))
312: {
313: $this->_rows[$fixtureName]=$rows;
314: if(isset($modelClass))
315: {
316: foreach(array_keys($rows) as $alias)
317: $this->_records[$fixtureName][$alias]=$modelClass;
318: }
319: }
320: }
321:
322: $schema->checkIntegrity(true);
323: }
324:
325: /**
326: * Returns the fixture data rows.
327: * The rows will have updated primary key values if the primary key is auto-incremental.
328: * @param string $name the fixture name
329: * @return array the fixture data rows. False is returned if there is no such fixture data.
330: */
331: public function getRows($name)
332: {
333: if(isset($this->_rows[$name]))
334: return $this->_rows[$name];
335: else
336: return false;
337: }
338:
339: /**
340: * Returns the specified ActiveRecord instance in the fixture data.
341: * @param string $name the fixture name
342: * @param string $alias the alias for the fixture data row
343: * @return CActiveRecord the ActiveRecord instance. False is returned if there is no such fixture row.
344: */
345: public function getRecord($name,$alias)
346: {
347: if(isset($this->_records[$name][$alias]))
348: {
349: if(is_string($this->_records[$name][$alias]))
350: {
351: $row=$this->_rows[$name][$alias];
352: $model=CActiveRecord::model($this->_records[$name][$alias]);
353: $key=$model->getTableSchema()->primaryKey;
354: if(is_string($key))
355: $pk=$row[$key];
356: else
357: {
358: foreach($key as $k)
359: $pk[$k]=$row[$k];
360: }
361: $this->_records[$name][$alias]=$model->findByPk($pk);
362: }
363: return $this->_records[$name][$alias];
364: }
365: else
366: return false;
367: }
368: }
369: