FixtureTrait.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\test;
  8. use Yii;
  9. use yii\base\InvalidConfigException;
  10. use yii\base\UnknownMethodException;
  11. use yii\base\UnknownPropertyException;
  12. /**
  13. * FixtureTrait provides functionalities for loading, unloading and accessing fixtures for a test case.
  14. *
  15. * By using FixtureTrait, a test class will be able to specify which fixtures to load by overriding
  16. * the [[fixtures()]] method. It can then load and unload the fixtures using [[loadFixtures()]] and [[unloadFixtures()]].
  17. * Once a fixture is loaded, it can be accessed like an object property, thanks to the PHP `__get()` magic method.
  18. * Also, if the fixture is an instance of [[ActiveFixture]], you will be able to access AR models
  19. * through the syntax `$this->fixtureName('model name')`.
  20. *
  21. * @author Qiang Xue <[email protected]>
  22. * @since 2.0
  23. */
  24. trait FixtureTrait
  25. {
  26. /**
  27. * @var array the list of fixture objects available for the current test.
  28. * The array keys are the corresponding fixture class names.
  29. * The fixtures are listed in their dependency order. That is, fixture A is listed before B
  30. * if B depends on A.
  31. */
  32. private $_fixtures;
  33. /**
  34. * @var array the fixture class names indexed by the corresponding fixture names (aliases).
  35. */
  36. private $_fixtureAliases;
  37. /**
  38. * Returns the value of an object property.
  39. *
  40. * Do not call this method directly as it is a PHP magic method that
  41. * will be implicitly called when executing `$value = $object->property;`.
  42. * @param string $name the property name
  43. * @return mixed the property value
  44. * @throws UnknownPropertyException if the property is not defined
  45. */
  46. public function __get($name)
  47. {
  48. $fixture = $this->getFixture($name);
  49. if ($fixture !== null) {
  50. return $fixture;
  51. } else {
  52. throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
  53. }
  54. }
  55. /**
  56. * Calls the named method which is not a class method.
  57. *
  58. * Do not call this method directly as it is a PHP magic method that
  59. * will be implicitly called when an unknown method is being invoked.
  60. * @param string $name the method name
  61. * @param array $params method parameters
  62. * @throws UnknownMethodException when calling unknown method
  63. * @return mixed the method return value
  64. */
  65. public function __call($name, $params)
  66. {
  67. $fixture = $this->getFixture($name);
  68. if ($fixture instanceof ActiveFixture) {
  69. return $fixture->getModel(reset($params));
  70. } else {
  71. throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()");
  72. }
  73. }
  74. /**
  75. * Declares the fixtures that are needed by the current test case.
  76. * The return value of this method must be an array of fixture configurations. For example,
  77. *
  78. * ```php
  79. * [
  80. * // anonymous fixture
  81. * PostFixture::className(),
  82. * // "users" fixture
  83. * 'users' => UserFixture::className(),
  84. * // "cache" fixture with configuration
  85. * 'cache' => [
  86. * 'class' => CacheFixture::className(),
  87. * 'host' => 'xxx',
  88. * ],
  89. * ]
  90. * ```
  91. *
  92. * Note that the actual fixtures used for a test case will include both [[globalFixtures()]]
  93. * and [[fixtures()]].
  94. *
  95. * @return array the fixtures needed by the current test case
  96. */
  97. protected function fixtures()
  98. {
  99. return [];
  100. }
  101. /**
  102. * Declares the fixtures shared required by different test cases.
  103. * The return value should be similar to that of [[fixtures()]].
  104. * You should usually override this method in a base class.
  105. * @return array the fixtures shared and required by different test cases.
  106. * @see fixtures()
  107. */
  108. protected function globalFixtures()
  109. {
  110. return [];
  111. }
  112. /**
  113. * Loads the fixtures.
  114. * This method will load the fixtures specified by `$fixtures` or [[globalFixtures()]] and [[fixtures()]].
  115. * @param array $fixtures the fixtures to loaded. If not set, [[fixtures()]] will be loaded instead.
  116. * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among
  117. * the fixtures is detected.
  118. */
  119. protected function loadFixtures($fixtures = null)
  120. {
  121. if ($fixtures === null) {
  122. $fixtures = array_merge($this->globalFixtures(), $this->fixtures());
  123. }
  124. // normalize fixture configurations
  125. $config = []; // configuration provided in test case
  126. $this->_fixtureAliases = [];
  127. foreach ($fixtures as $name => $fixture) {
  128. if (!is_array($fixture)) {
  129. $fixtures[$name] = $fixture = ['class' => $fixture];
  130. } elseif (!isset($fixture['class'])) {
  131. throw new InvalidConfigException("You must specify 'class' for the fixture '$name'.");
  132. }
  133. $config[$fixture['class']] = $fixture;
  134. $this->_fixtureAliases[$name] = $fixture['class'];
  135. }
  136. // create fixture instances
  137. $this->_fixtures = [];
  138. $stack = array_reverse($fixtures);
  139. while (($fixture = array_pop($stack)) !== null) {
  140. if ($fixture instanceof Fixture) {
  141. $class = get_class($fixture);
  142. unset($this->_fixtures[$class]); // unset so that the fixture is added to the last in the next line
  143. $this->_fixtures[$class] = $fixture;
  144. } else {
  145. $class = $fixture['class'];
  146. if (!isset($this->_fixtures[$class])) {
  147. $this->_fixtures[$class] = false;
  148. $stack[] = $fixture = Yii::createObject($fixture);
  149. foreach ($fixture->depends as $dep) {
  150. // need to use the configuration provided in test case
  151. $stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep];
  152. }
  153. } elseif ($this->_fixtures[$class] === false) {
  154. throw new InvalidConfigException("A circular dependency is detected for fixture '$class'.");
  155. }
  156. }
  157. }
  158. // load fixtures
  159. /** @var Fixture $fixture */
  160. foreach ($this->_fixtures as $fixture) {
  161. $fixture->beforeLoad();
  162. }
  163. foreach ($this->_fixtures as $fixture) {
  164. $fixture->load();
  165. }
  166. foreach ($this->_fixtures as $fixture) {
  167. $fixture->afterLoad();
  168. }
  169. }
  170. /**
  171. * Unloads all existing fixtures.
  172. */
  173. protected function unloadFixtures()
  174. {
  175. /** @var Fixture $fixture */
  176. foreach (array_reverse($this->_fixtures) as $fixture) {
  177. $fixture->unload();
  178. }
  179. }
  180. /**
  181. * @return array the loaded fixtures for the current test case
  182. */
  183. protected function getFixtures()
  184. {
  185. return $this->_fixtures;
  186. }
  187. /**
  188. * Returns the named fixture.
  189. * @param string $name the fixture alias or class name
  190. * @return Fixture the fixture object, or null if the named fixture does not exist.
  191. */
  192. protected function getFixture($name)
  193. {
  194. $class = isset($this->_fixtureAliases[$name]) ? $this->_fixtureAliases[$name] : $name;
  195. return isset($this->_fixtures[$class]) ? $this->_fixtures[$class] : null;
  196. }
  197. }