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

namespace yii\faker;

use Yii;
use yii\console\Exception;
use yii\helpers\FileHelper;
use yii\helpers\Console;

/**
 * This command manage fixtures creations based on given template.
 *
 * Fixtures are one of the important paths in unit testing. To speed up developers
Mark committed
19
 * work these fixtures can be generated automatically, based on prepared template.
Mark committed
20
 * This command is a simple wrapper for the fixtures library [Faker](https://github.com/fzaninotto/Faker).
Qiang Xue committed
21 22 23
 *
 * You should configure this command as follows (you can use any alias, not only "fixture"):
 *
Mark committed
24
 * ~~~
Qiang Xue committed
25 26 27 28 29
 * 'controllerMap' => [
 *     'fixture' => [
 *         'class' => 'yii\faker\FixtureController',
 *     ],
 * ],
Mark committed
30
 * ~~~
Qiang Xue committed
31
 *
Mark committed
32 33
 * To start using this command you need to be familiar (read guide) for the Faker library and
 * generate fixtures template files, according to the given format:
Qiang Xue committed
34
 *
Mark committed
35 36
 * ~~~
 * #users.php file under $templatePath
Qiang Xue committed
37
 *
Mark committed
38
 * return [
Qiang Xue committed
39 40 41 42 43 44 45 46 47
 *    [
 *        'table_column0' => 'faker_formatter',
 *        ...
 *        'table_columnN' => 'other_faker_formatter
 *        'table_columnN+1' => function ($fixture, $faker, $index) {
 *            //set needed fixture fields based on different conditions
 *            return $fixture;
 *        }
 *    ],
Mark committed
48 49
 * ];
 * ~~~
Qiang Xue committed
50
 *
Mark committed
51
 * If you use callback as a attribute value, then it will be called as shown with three parameters:
Qiang Xue committed
52 53 54 55 56
 *
 * - `$fixture` - current fixture array.
 * - `$faker` - faker generator instance
 * - `$index` - current fixture index. For example if user need to generate 3 fixtures for tbl_user, it will be 0..2
 *
Mark committed
57
 * After you set all needed fields in callback, you need to return $fixture array back from the callback.
Qiang Xue committed
58
 *
Mark committed
59
 * After you prepared needed templates for tables you can simply generate your fixtures via command
Qiang Xue committed
60
 *
Mark committed
61
 * ~~~
Qiang Xue committed
62 63
 * yii fixture/generate users
 *
64
 * //also a short version of this command (generate action is default)
Qiang Xue committed
65
 * yii fixture users
Mark committed
66 67
 *
 * //to generate fixtures for several tables, use "," as a separator, for example:
Qiang Xue committed
68
 * yii fixture users,profile
Mark committed
69
 * ~~~
Qiang Xue committed
70
 *
Mark committed
71
 * In the code above "users" is template name, after this command run, new file named same as template
Qiang Xue committed
72
 * will be created under the `$fixturePath` folder.
Qiang Xue committed
73 74
 * You can generate fixtures for all templates by specifying keyword "all"
 *
Mark committed
75
 * ~~~
Qiang Xue committed
76
 * yii fixture/generate all
Mark committed
77
 * ~~~
Qiang Xue committed
78 79 80 81
 *
 * This command will generate fixtures for all template files that are stored under $templatePath and
 * store fixtures under $fixturePath with file names same as templates names.
 *
Mark committed
82 83
 * You can specify how many fixtures per file you need by the second parameter. In the code below we generate
 * all fixtures and in each file there will be 3 rows (fixtures).
Qiang Xue committed
84
 *
Mark committed
85
 * ~~~
Qiang Xue committed
86
 * yii fixture/generate all 3
Mark committed
87
 * ~~~
Qiang Xue committed
88
 *
Mark committed
89
 * You can specify different options of this command:
Qiang Xue committed
90
 *
Mark committed
91
 * ~~~
Qiang Xue committed
92
 * //generate fixtures in russian language
Qiang Xue committed
93
 * yii fixture/generate users 5 --language=ru_RU
Qiang Xue committed
94
 *
95
 * //read templates from the other path
Qiang Xue committed
96
 * yii fixture/generate all --templatePath=@app/path/to/my/custom/templates
Qiang Xue committed
97
 *
98
 * //generate fixtures into other folders, but be sure that this folders exists or you will get notice about that.
Qiang Xue committed
99
 * yii fixture/generate all --fixturePath=@tests/unit/fixtures/subfolder1/subfolder2/subfolder3
Mark committed
100
 * ~~~
Qiang Xue committed
101
 *
Mark committed
102 103
 * You also can create your own data providers for custom tables fields, see Faker library guide for more info (https://github.com/fzaninotto/Faker);
 * After you created custom provider, for example:
Qiang Xue committed
104
 *
Mark committed
105
 * ~~~
Qiang Xue committed
106 107 108 109 110 111 112
 * class Book extends \Faker\Provider\Base
 * {
 *     public function title($nbWords = 5)
 *     {
 *         $sentence = $this->generator->sentence($nbWords);
 *         return mb_substr($sentence, 0, mb_strlen($sentence) - 1);
 *     }
Mark committed
113
 *
Qiang Xue committed
114 115 116 117 118
 *     public function ISBN()
 *     {
 *         return $this->generator->randomNumber(13);
 *     }
 * }
Mark committed
119
 * ~~~
Qiang Xue committed
120
 *
Mark committed
121
 * you can use it by adding it to the $providers property of the current command. In your console.php config:
Qiang Xue committed
122
 *
Mark committed
123
 * ~~~
Qiang Xue committed
124 125 126 127 128 129 130 131
 *    'controllerMap' => [
 *        'fixture' => [
 *            'class' => 'yii\faker\FixtureController',
 *            'providers' => [
 *                'app\tests\unit\faker\providers\Book',
 *            ],
 *        ],
 *    ],
Mark committed
132
 * ~~~
Qiang Xue committed
133
 *
Mark committed
134
 * @property \Faker\Generator $generator
Qiang Xue committed
135 136
 *
 * @author Mark Jebri <mark.github@yandex.ru>
Mark committed
137 138 139 140 141 142 143
 * @since 2.0.0
 */
class FixtureController extends \yii\console\controllers\FixtureController
{
	/**
	 * type of fixture generating
	 */
144
	const GENERATE_ALL = 'all';
Mark committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164

	/**
	 * @var string controller default action ID.
	 */
	public $defaultAction = 'generate';
	/**
	 * Alias to the template path, where all tables templates are stored.
	 * @var string
	 */
	public $templatePath = '@tests/unit/templates/fixtures';
	/**
	 * Language to use when generating fixtures data.
	 * @var string
	 */
	public $language;
	/**
	 * Additional data providers that can be created by user and will be added to the Faker generator.
	 * More info in [Faker](https://github.com/fzaninotto/Faker.) library docs.
	 * @var array
	 */
Mark committed
165
	public $providers = [];
Mark committed
166 167 168 169 170 171
	/**
	 * Faker generator instance
	 * @var \Faker\Generator
	 */
	private $_generator;

172

Mark committed
173 174 175 176 177 178 179
	/**
	 * 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(), [
Qiang Xue committed
180
			'templatePath', 'language'
Mark committed
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
		]);
	}

	public function beforeAction($action)
	{
		if (parent::beforeAction($action)) {
			$this->checkPaths();
			$this->addProviders();
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Generates fixtures and fill them with Faker data.
	 * @param string $file filename for the table template. You can generate all fixtures for all tables
Qiang Xue committed
198
	 * by specifying keyword "all" as filename.
Mark committed
199 200
	 * @param integer $times how much fixtures do you want per table
	 */
201
	public function actionGenerate(array $file, $times = 2)
Mark committed
202 203
	{
		$templatePath = Yii::getAlias($this->templatePath);
Qiang Xue committed
204
		$fixturePath = Yii::getAlias($this->fixturePath);
Mark committed
205

206
		if ($this->needToGenerateAll($file[0])) {
Mark committed
207
			$files = FileHelper::findFiles($templatePath, ['only' => ['.php']]);
Qiang Xue committed
208 209 210
		} else {
			$filesToSearch = [];
			foreach ($file as $fileName) {
211 212 213
				$filesToSearch[] = $fileName . '.php';
			}
			$files = FileHelper::findFiles($templatePath, ['only' => $filesToSearch]);
Mark committed
214 215 216
		}

		if (empty($files)) {
217
			throw new Exception("No files were found by name: \"" . implode(', ', $file) . "\".\n"
Qiang Xue committed
218
				. "Check that template with these name exists, under template path: \n\"{$templatePath}\"."
Mark committed
219 220
			);
		}
Mark committed
221

222 223 224 225
		if (!$this->confirmGeneration($files)) {
			return;
		}

Mark committed
226
		foreach ($files as $templateFile) {
Mark committed
227 228 229 230 231 232 233 234
			$fixtureFileName = basename($templateFile);
			$template = $this->getTemplate($templateFile);
			$fixtures = [];

			for ($i = 0; $i < $times; $i++) {
				$fixtures[$i] = $this->generateFixture($template, $i);
			}

Qiang Xue committed
235 236 237
			$content = $this->exportFixtures($fixtures);
			file_put_contents($fixturePath . '/' . $fixtureFileName, $content);
			$this->stdout("Fixture file was generated under: " . realpath($fixturePath . "/" . $fixtureFileName) . "\n", Console::FG_GREEN);
Mark committed
238 239 240 241 242 243 244 245 246
		}
	}

	/**
	 * Returns Faker generator instance. Getter for private property.
	 * @return \Faker\Generator
	 */
	public function getGenerator()
	{
Mark committed
247 248
		if (is_null($this->_generator)) {
			//replacing - on _ because Faker support only en_US format and not intl
Mark committed
249

Qiang Xue committed
250
			$language = is_null($this->language) ? str_replace('-', '_', Yii::$app->language) : $this->language;
Mark committed
251 252 253 254 255 256 257 258 259 260 261 262 263
			$this->_generator = \Faker\Factory::create($language);
		}

		return $this->_generator;
	}

	/**
	 * Check if the template path and migrations path exists and writable.
	 */
	public function checkPaths()
	{
		$path = Yii::getAlias($this->templatePath);

Mark committed
264
		if (!is_dir($path)) {
Mark committed
265
			throw new Exception("The template path \"{$this->templatePath}\" not exist");
Mark committed
266
		}
Mark committed
267 268 269 270 271 272 273
	}

	/**
	 * Adds users providers to the faker generator.
	 */
	public function addProviders()
	{
Qiang Xue committed
274
		foreach ($this->providers as $provider) {
Mark committed
275
			$this->generator->addProvider(new $provider($this->generator));
Mark committed
276
		}
Mark committed
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
	}

	/**
	 * Checks if needed to generate all fixtures.
	 * @param string $file
	 * @return bool
	 */
	public function needToGenerateAll($file)
	{
		return $file == self::GENERATE_ALL;
	}

	/**
	 * Returns generator template for the given fixture name
	 * @param string $file template file
	 * @return array generator template
	 * @throws \yii\console\Exception if wrong file format
	 */
	public function getTemplate($file)
	{
		$template = require($file);

		if (!is_array($template)) {
			throw new Exception("The template file \"$file\" has wrong format. It should return valid template array");
		}

		return $template;
	}

	/**
	 * Returns exported to the string representation of given fixtures array.
Qiang Xue committed
308
	 * @param array $fixtures
Mark committed
309 310
	 * @return string exported fixtures format
	 */
Mark committed
311
	public function exportFixtures($fixtures)
Mark committed
312 313 314
	{
		$content = "<?php\n\nreturn [";

Qiang Xue committed
315
		foreach ($fixtures as $fixture) {
Mark committed
316 317 318

			$content .= "\n\t[";

Qiang Xue committed
319
			foreach ($fixture as $name => $value) {
Mark committed
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
				$content .= "\n\t\t'{$name}' => '{$value}',";
			}

			$content .= "\n\t],";

		}
		$content .= "\n];\n";
		return $content;
	}

	/**
	 * Generates fixture from given template
	 * @param array $template fixture template
	 * @param integer $index current fixture index
	 * @return array fixture
	 */
	public function generateFixture($template, $index)
	{
		$fixture = [];

Qiang Xue committed
340
		foreach ($template as $attribute => $fakerProperty) {
Mark committed
341
			if (!is_string($fakerProperty)) {
Qiang Xue committed
342
				$fixture = call_user_func_array($fakerProperty, [$fixture, $this->generator, $index]);
Mark committed
343 344 345 346 347 348 349 350
			} else {
				$fixture[$attribute] = $this->generator->$fakerProperty;
			}
		}

		return $fixture;
	}

351 352 353 354 355 356 357 358
	/**
	 * Prompts user with message if he confirm generation with given fixture templates files.
	 * @param array $files
	 * @return boolean
	 */
	public function confirmGeneration($files)
	{
		$this->stdout("Fixtures will be generated under the path: \n", Console::FG_YELLOW);
Qiang Xue committed
359
		$this->stdout(realpath(Yii::getAlias($this->fixturePath, false)) . "\n\n", Console::FG_GREEN);
360 361 362 363
		$this->stdout("Templates will be taken from path: \n", Console::FG_YELLOW);
		$this->stdout(realpath(Yii::getAlias($this->templatePath, false)) . "\n\n", Console::FG_GREEN);

		foreach ($files as $index => $fileName) {
Qiang Xue committed
364
			$this->stdout("    " . ($index + 1) . ". " . basename($fileName) . "\n", Console::FG_GREEN);
365 366 367
		}
		return $this->confirm('Generate above fixtures?');
	}
Mark committed
368
}