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

namespace yii\console\controllers;

use Yii;
use yii\console\Controller;
12
use yii\console\Exception;
Mark committed
13
use yii\helpers\Console;
Qiang Xue committed
14 15
use yii\helpers\FileHelper;
use yii\test\FixtureTrait;
Mark committed
16 17

/**
Mark committed
18
 * This command manages loading and unloading fixtures.
Mark committed
19 20
 * You can specify different options of this command to point fixture manager
 * to the specific tables of the different database connections.
Qiang Xue committed
21
 *
Mark committed
22
 * To use this command simply configure your console.php config like this:
Qiang Xue committed
23
 *
Mark committed
24
 * ~~~
Qiang Xue committed
25 26 27 28 29 30 31
 * 'db' => [
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=localhost;dbname={your_database}',
 *     'username' => '{your_db_user}',
 *     'password' => '',
 *     'charset' => 'utf8',
 * ],
Mark committed
32
 * ~~~
Qiang Xue committed
33
 *
Mark committed
34
 * ~~~
Mark committed
35
 * #load fixtures under $fixturePath from UsersFixture class with default namespace "tests\unit\fixtures"
Mark committed
36
 * yii fixture/load User
Qiang Xue committed
37
 *
Mark committed
38
 * #also a short version of this command (generate action is default)
Mark committed
39
 * yii fixture User
Qiang Xue committed
40
 *
Mark committed
41
 * #load fixtures under $fixturePath with the different database connection
Mark committed
42
 * yii fixture/load User --db=someOtherDbConnection
Qiang Xue committed
43
 *
Mark committed
44
 * #load fixtures under different $fixturePath.
Mark committed
45
 * yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
Mark committed
46
 * ~~~
Qiang Xue committed
47
 *
Mark committed
48 49 50 51 52
 * @author Mark Jebri <mark.github@yandex.ru>
 * @since 2.0
 */
class FixtureController extends Controller
{
Qiang Xue committed
53

Mark committed
54 55
	use FixtureTrait;

Mark committed
56 57 58 59
	/**
	 * type of fixture apply to database
	 */
	const APPLY_ALL = 'all';
Mark committed
60 61 62 63 64 65

	/**
	 * @var string controller default action ID.
	 */
	public $defaultAction = 'apply';
	/**
Mark committed
66
	 * @var string id of the database connection component of the application.
Mark committed
67 68
	 */
	public $db = 'db';
Mark committed
69 70 71 72
	/**
	 * @var string default namespace to search fixtures in
	 */
	public $namespace = 'tests\unit\fixtures';
Mark committed
73 74 75 76 77
	/**
	 * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture`
	 * that disables and enables integrity check, so your data can be safely loaded.
	 */
	public $globalFixtures = [
78
		'yii\test\InitDb',
Mark committed
79
	];
Carsten Brandt committed
80

Mark committed
81 82 83 84 85 86 87
	/**
	 * Returns the names of the global options for this command.
	 * @return array the names of the global options for this command.
	 */
	public function globalOptions()
	{
		return array_merge(parent::globalOptions(), [
Mark committed
88
			'db', 'namespace','globalFixtures'
Mark committed
89 90 91 92
		]);
	}

	/**
Mark committed
93 94 95 96
	 * Loads given fixture. You can load several fixtures specifying
	 * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no
	 * whitespace between names. Note that if you are loading fixtures to storage, for example: database or nosql,
	 * storage will not be cleared, data will be appended to already existed.
Carsten Brandt committed
97 98
	 * @param array $fixtures
	 * @throws \yii\console\Exception
Mark committed
99
	 */
Mark committed
100
	public function actionLoad(array $fixtures, array $except = [])
Mark committed
101
	{
Mark committed
102 103 104 105 106 107 108 109 110 111 112 113
		$foundFixtures = $this->findFixtures($fixtures);

		if (!$this->needToApplyAll($fixtures[0])) {
			$notFoundFixtures = array_diff($fixtures, $foundFixtures);

			if ($notFoundFixtures) {
				$this->notifyNotFound($notFoundFixtures);
			}
		}

		if (!$foundFixtures) {
			throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n"
114
				. "Check that files with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"."
Mark committed
115
			);
Mark committed
116 117
		}

Mark committed
118
		if (!$this->confirmLoad($foundFixtures, $except)) {
Mark committed
119
			return;
Mark committed
120
		}
Mark committed
121

Mark committed
122 123
		$filtered = array_diff($foundFixtures, $except);
		$fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures ,$filtered));
Mark committed
124

Mark committed
125
		if (!$fixtures) {
Qiang Xue committed
126
			throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . '');
Mark committed
127
		}
Mark committed
128

Mark committed
129
		$transaction = $this->getDbConnection()->beginTransaction();
Mark committed
130

Mark committed
131
		try {
Mark committed
132
			$this->loadFixtures($this->createFixtures($fixtures));
Mark committed
133
			$transaction->commit();
Mark committed
134
		} catch (\Exception $e) {
Mark committed
135
			$transaction->rollback();
Qiang Xue committed
136
			$this->stdout("Exception occurred, transaction rollback. Tables will be in same state.\n", Console::BG_RED);
Mark committed
137 138
			throw $e;
		}
Mark committed
139
		$this->notifyLoaded($fixtures);
Mark committed
140 141 142
	}

	/**
Mark committed
143
	 * Unloads given fixtures. You can clear environment and unload multiple fixtures by specifying
Mark committed
144
	 * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no
145
	 * whitespace between tables names.
Mark committed
146 147
	 * @param array|string $fixtures
	 * @param array|string $except
Mark committed
148
	 */
Mark committed
149
	public function actionUnload(array $fixtures, array $except = [])
Mark committed
150 151 152 153 154 155 156 157 158
	{
		$foundFixtures = $this->findFixtures($fixtures);

		if (!$this->needToApplyAll($fixtures[0])) {
			$notFoundFixtures = array_diff($fixtures, $foundFixtures);

			if ($notFoundFixtures) {
				$this->notifyNotFound($notFoundFixtures);
			}
Mark committed
159 160
		}

Mark committed
161 162
		if (!$foundFixtures) {
			throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n"
163
				. "Check that fixtures with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"."
Mark committed
164 165 166
			);
		}

Mark committed
167
		if (!$this->confirmUnload($foundFixtures, $except)) {
Mark committed
168 169 170
			return;
		}

Mark committed
171 172
		$filtered = array_diff($foundFixtures, $except);
		$fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures ,$filtered));
Mark committed
173 174

		if (!$fixtures) {
Mark committed
175
			throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".');
Mark committed
176
		}
Mark committed
177

Mark committed
178
		$transaction = $this->getDbConnection()->beginTransaction();
Mark committed
179

Mark committed
180
		try {
Mark committed
181
			$this->unloadFixtures($this->createFixtures($fixtures));
Mark committed
182 183
			$transaction->commit();

Mark committed
184
		} catch (\Exception $e) {
Mark committed
185
			$transaction->rollback();
Qiang Xue committed
186
			$this->stdout("Exception occurred, transaction rollback. Tables will be in same state.\n", Console::BG_RED);
Mark committed
187
			throw $e;
Mark committed
188
		}
Mark committed
189
		$this->notifyUnloaded($fixtures);
Mark committed
190 191 192 193
	}

	/**
	 * Returns database connection component
Qiang Xue committed
194
	 * @return \yii\db\Connection
Qiang Xue committed
195
	 * @throws \yii\console\Exception if [[db]] is invalid.
Mark committed
196 197 198 199 200
	 */
	public function getDbConnection()
	{
		$db = Yii::$app->getComponent($this->db);

Carsten Brandt committed
201
		if ($db === null) {
202
			throw new Exception("There is no database connection component with id \"{$this->db}\".");
Mark committed
203 204 205 206 207
		}

		return $db;
	}

208 209 210 211
	/**
	 * Notifies user that fixtures were successfully loaded.
	 * @param array $fixtures
	 */
Mark committed
212 213 214 215 216 217 218 219 220 221 222 223
	private function notifyLoaded($fixtures)
	{
		$this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW);
		$this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
		$this->outputList($fixtures);
	}

	/**
	 * Notifies user that fixtures were successfully unloaded.
	 * @param array $fixtures
	 */
	private function notifyUnloaded($fixtures)
224
	{
225
		$this->stdout("Fixtures were successfully unloaded from namespace:\n", Console::FG_YELLOW);
226
		$this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN);
Mark committed
227 228
		$this->outputList($fixtures);
	}
229

Mark committed
230 231 232 233 234 235 236
	/**
	 * Notifies user that fixtures were not found under fixtures path.
	 * @param array $fixtures
	 */
	private function notifyNotFound($fixtures)
	{
		$this->stdout("Some fixtures were not found under path:\n", Console::BG_RED);
237
		$this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN);
Mark committed
238
		$this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED);
Mark committed
239 240 241 242
		$this->outputList($fixtures);
		$this->stdout("\n");
	}

Mark committed
243 244 245
	/**
	 * Prompts user with confirmation if fixtures should be loaded.
	 * @param array $fixtures
Mark committed
246
	 * @param array $except
Mark committed
247 248
	 * @return boolean
	 */
Mark committed
249
	private function confirmLoad($fixtures, $except)
Mark committed
250
	{
Mark committed
251 252 253
		$this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
		$this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);

Mark committed
254 255 256 257
		if (count($this->globalFixtures)) {
			$this->stdout("Global fixtures will be loaded:\n\n", Console::FG_YELLOW);
			$this->outputList($this->globalFixtures);
		}
Mark committed
258 259

		$this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW);
Mark committed
260
		$this->outputList($fixtures);
Mark committed
261 262 263 264 265 266

		if (count($except)) {
			$this->stdout("\nFixtures that will NOT be loaded: \n\n", Console::FG_YELLOW);
			$this->outputList($except);
		}

267
		return $this->confirm("\nLoad above fixtures?");
Mark committed
268 269 270
	}

	/**
Mark committed
271 272
	 * Prompts user with confirmation for fixtures that should be unloaded.
	 * @param array $fixtures
Mark committed
273
	 * @param array $except
Mark committed
274 275
	 * @return boolean
	 */
Mark committed
276
	private function confirmUnload($fixtures, $except)
Mark committed
277
	{
Mark committed
278 279 280
		$this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW);
		$this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN);

Mark committed
281 282 283 284
		if (count($this->globalFixtures)) {
			$this->stdout("Global fixtures will be unloaded:\n\n", Console::FG_YELLOW);
			$this->outputList($this->globalFixtures);
		}
Mark committed
285 286

		$this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW);
Mark committed
287
		$this->outputList($fixtures);
Mark committed
288 289

		if (count($except)) {
Mark committed
290
			$this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW);
Mark committed
291 292 293
			$this->outputList($except);
		}

Mark committed
294
		return $this->confirm("\nUnload fixtures?");
Mark committed
295 296 297 298 299 300 301 302
	}

	/**
	 * Outputs data to the console as a list.
	 * @param array $data
	 */
	private function outputList($data)
	{
Qiang Xue committed
303
		foreach ($data as $index => $item) {
Mark committed
304
			$this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN);
305 306
		}
	}
Mark committed
307 308 309 310 311 312 313 314 315 316 317 318 319

	/**
	 * Checks if needed to apply all fixtures.
	 * @param string $fixture
	 * @return bool
	 */
	public function needToApplyAll($fixture)
	{
		return $fixture == self::APPLY_ALL;
	}

	/**
	 * @param array $fixtures
Qiang Xue committed
320
	 * @return array Array of found fixtures. These may differ from input parameter as not all fixtures may exists.
Mark committed
321 322 323
	 */
	private function findFixtures(array $fixtures)
	{
324
		$fixturesPath = $this->getFixturePath();
325

Mark committed
326
		$filesToSearch = ['*Fixture.php'];
327 328
		if (!$this->needToApplyAll($fixtures[0])) {
			$filesToSearch = [];
Mark committed
329
			foreach ($fixtures as $fileName) {
Mark committed
330
				$filesToSearch[] = $fileName . 'Fixture.php';
Mark committed
331 332
			}
		}
Alexander Makarov committed
333 334

		$files = FileHelper::findFiles($fixturesPath, ['only' => $filesToSearch]);
Mark committed
335 336
		$foundFixtures = [];

Alexander Makarov committed
337
		foreach ($files as $fixture) {
Qiang Xue committed
338
			$foundFixtures[] = basename($fixture, 'Fixture.php');
Mark committed
339 340 341 342 343
		}

		return $foundFixtures;
	}

Mark committed
344 345 346 347 348
	/**
	 * Returns valid fixtures config that can be used to load them.
	 * @param array $fixtures fixtures to configure
	 * @return array
	 */
Mark committed
349
	private function getFixturesConfig($fixtures)
Mark committed
350 351 352
	{
		$config = [];

Qiang Xue committed
353
		foreach ($fixtures as $fixture) {
Mark committed
354

Mark committed
355
			$isNamespaced = (strpos($fixture, '\\') !== false);
Mark committed
356
			$fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture';
Mark committed
357 358

			if (class_exists($fullClassName)) {
Mark committed
359
				$config[] = $fullClassName;
Mark committed
360 361 362 363 364 365
			}
		}

		return $config;
	}

366 367 368 369 370 371 372 373 374
	/**
	 * Returns fixture path that determined on fixtures namespace.
	 * @return string fixture path
	 */
	private function getFixturePath()
	{
		return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace));
	}

Mark committed
375
}