FixtureController.php 11.4 KB
Newer Older
Mark committed
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\faker;

use Yii;
use yii\console\Exception;
use yii\helpers\Console;
Qiang Xue committed
13
use yii\helpers\FileHelper;
Mark committed
14 15 16 17 18

/**
 * 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
 *
 * - `$fixture` - current fixture array.
 * - `$faker` - faker generator instance
55
 * - `$index` - current fixture index. For example if user need to generate 3 fixtures for user table, it will be 0..2
Qiang Xue committed
56
 *
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
Mark committed
72
 * will be created under the `$fixtureDataPath` 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
 *
 * This command will generate fixtures for all template files that are stored under $templatePath and
Mark committed
80
 * store fixtures under `$fixtureDataPath` with file names same as templates names.
Qiang Xue committed
81
 *
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
 *
Mark committed
98 99
 * //generate fixtures into other folders
 * yii fixture/generate all --fixtureDataPath=@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
 *
134
 * @property \Faker\Generator $generator This property is read-only.
Qiang Xue committed
135 136
 *
 * @author Mark Jebri <mark.github@yandex.ru>
Mark committed
137 138 139 140
 * @since 2.0.0
 */
class FixtureController extends \yii\console\controllers\FixtureController
{
141 142 143 144 145 146 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
    /**
     * type of fixture generating
     */
    const GENERATE_ALL = 'all';

    /**
     * @var string controller default action ID.
     */
    public $defaultAction = 'generate';
    /**
     * @var string Alias to the template path, where all tables templates are stored.
     */
    public $templatePath = '@tests/unit/templates/fixtures';
    /**
     * @var string Alias to the fixture data path, where data files should be written.
     */
    public $fixtureDataPath = '@tests/unit/fixtures/data';
    /**
     * @var string Language to use when generating fixtures data.
     */
    public $language;
    /**
     * @var array 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.
     */
    public $providers = [];
    /**
     * @var \Faker\Generator Faker generator instance
     */
    private $_generator;


    /**
174
     * @inheritdoc
175
     */
176
    public function options($actionId)
177
    {
178
        return array_merge(parent::options($actionId), [
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
            'templatePath', 'language', 'fixtureDataPath'
        ]);
    }

    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.
197 198 199
     *
     * @param array|string $file filename for the table template.
     * You can generate all fixtures for all tables by specifying keyword "all" as filename.
200
     * @param integer $times how much fixtures do you want per table
201 202
     * @throws \yii\base\InvalidParamException
     * @throws \yii\console\Exception
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
     */
    public function actionGenerate(array $file, $times = 2)
    {
        $templatePath = Yii::getAlias($this->templatePath);
        $fixtureDataPath = Yii::getAlias($this->fixtureDataPath);

        if ($this->needToGenerateAll($file[0])) {
            $files = FileHelper::findFiles($templatePath, ['only' => ['*.php']]);
        } else {
            $filesToSearch = [];
            foreach ($file as $fileName) {
                $filesToSearch[] = $fileName . '.php';
            }
            $files = FileHelper::findFiles($templatePath, ['only' => $filesToSearch]);
        }

        if (empty($files)) {
            throw new Exception("No files were found by name: \"" . implode(', ', $file) . "\".\n"
                . "Check that template with these name exists, under template path: \n\"{$templatePath}\"."
            );
        }

        if (!$this->confirmGeneration($files)) {
            return;
        }

        foreach ($files as $templateFile) {
            $fixtureFileName = basename($templateFile);
            $template = $this->getTemplate($templateFile);
            $fixtures = [];

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

            $content = $this->exportFixtures($fixtures);
            FileHelper::createDirectory($fixtureDataPath);
            file_put_contents($fixtureDataPath . '/'. $fixtureFileName, $content);

            $this->stdout("Fixture file was generated under: $fixtureDataPath\n", Console::FG_GREEN);
        }
    }

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

            $language = is_null($this->language) ? str_replace('-', '_', Yii::$app->language) : $this->language;
            $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);

        if (!is_dir($path)) {
            throw new Exception("The template path \"{$this->templatePath}\" not exist");
        }
    }

    /**
     * Adds users providers to the faker generator.
     */
    public function addProviders()
    {
        foreach ($this->providers as $provider) {
            $this->generator->addProvider(new $provider($this->generator));
        }
    }

    /**
     * Checks if needed to generate all fixtures.
286
     * @param string $file
287 288 289 290 291 292 293 294 295
     * @return bool
     */
    public function needToGenerateAll($file)
    {
        return $file == self::GENERATE_ALL;
    }

    /**
     * Returns generator template for the given fixture name
296 297
     * @param string $file template file
     * @return array generator template
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
     * @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.
313
     * @param array $fixtures
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
     * @return string exported fixtures format
     */
    public function exportFixtures($fixtures)
    {
        $content = "<?php\n\nreturn [";

        foreach ($fixtures as $fixture) {

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

            foreach ($fixture as $name => $value) {
                $content .= "\n\t\t'{$name}' => '{$value}',";
            }

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

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

        return $content;
    }

    /**
     * Generates fixture from given template
338 339 340
     * @param array $template fixture template
     * @param integer $index current fixture index
     * @return array fixture
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
     */
    public function generateFixture($template, $index)
    {
        $fixture = [];

        foreach ($template as $attribute => $fakerProperty) {
            if (!is_string($fakerProperty)) {
                $fixture = call_user_func_array($fakerProperty, [$fixture, $this->generator, $index]);
            } else {
                $fixture[$attribute] = $this->generator->$fakerProperty;
            }
        }

        return $fixture;
    }

    /**
     * Prompts user with message if he confirm generation with given fixture templates files.
359
     * @param array $files
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
     * @return boolean
     */
    public function confirmGeneration($files)
    {
        $this->stdout("Fixtures will be generated under the path: \n", Console::FG_YELLOW);
        $this->stdout("\t" . Yii::getAlias($this->fixtureDataPath) . "\n\n", Console::FG_GREEN);
        $this->stdout("Templates will be taken from path: \n", Console::FG_YELLOW);
        $this->stdout("\t" . Yii::getAlias($this->templatePath) . "\n\n", Console::FG_GREEN);

        foreach ($files as $index => $fileName) {
            $this->stdout("    " . ($index + 1) . ". " . basename($fileName) . "\n", Console::FG_GREEN);
        }

        return $this->confirm('Generate above fixtures?');
    }
Mark committed
375
}