Fixes #9465: ./yii migrate/create now generates code based on migration name and --fields

This commit is contained in:
Daniel Gomez Pan
2015-11-20 21:35:58 +03:00
committed by Alexander Makarov
parent 7c9b7c77a7
commit 9afd240ab6
9 changed files with 841 additions and 5 deletions

View File

@ -181,6 +181,196 @@ class m150101_185401_create_news_table extends Migration
A list of all available methods for defining the column types is available in the API documentation of [[yii\db\SchemaBuilderTrait]]. A list of all available methods for defining the column types is available in the API documentation of [[yii\db\SchemaBuilderTrait]].
## Creating Migrations with Generators <span id="creating-migrations-with-generators"></span>
Since version 2.0.7 migration console which provides convenient way creating migrations.
If the migration name is of the form "create_xxx" or "drop_xxx" then a migration creating the table xxx with the columns listed will be generated. For example:
```php
yii migrate/create create_post
```
generates
```php
class m150811_220037_create_post extends Migration
{
public function up()
{
$this->createTable('post', [
'id' => $this->primaryKey()
]);
}
public function down()
{
$this->dropTable('post');
}
}
```
For create column schema you may use fields option with as follows:
```php
yii migrate/create create_post --fields=title:string,body:text
```
generates
```php
class m150811_220037_create_post extends Migration
{
public function up()
{
$this->createTable('post', [
'id' => $this->primaryKey(),
'title' => $this->string(),
'body' => $this->text()
]);
}
public function down()
{
$this->dropTable('post');
}
}
```
You are not limited to one magically generated column. For example:
```php
yii migrate/create create_post --fields=title:string(12):notNull:unique,body:text
```
generates
```php
class m150811_220037_create_post extends Migration
{
public function up()
{
$this->createTable('post', [
'id' => $this->primaryKey(),
'title' => $this->string(12)->notNull()->unique(),
'body' => $this->text()
]);
}
public function down()
{
$this->dropTable('post');
}
}
```
> Note: primary Key is added automatically, if you want to use another name for primary key, you
may specify in fields options, for example "--fields=name:primaryKey"
Similarly, you can generate a migration to drop table from the command line:
```php
yii migrate/create drop_post
```
generates
```php
class m150811_220037_drop_post extends Migration
{
public function up()
{
$this->dropTable('post');
}
public function down()
{
$this->createTable('post', [
]);
}
}
```
If the migration name is of the form "add_xxx_from_yyy" or "drop_xxx_from_yyy" then a migration containing the appropriate addColumn and dropColumn statements will be created.
```php
yii migrate/create add_position_from_post --fields=position:integer
```
generates
```php
class m150811_220037_add_position_from_post extends Migration
{
public function up()
{
this->addColumn('post', 'position', this->integer());
}
public function down()
{
this->dropColumn('post', 'position');
}
}
```
Similarly, you can generate a migration to remove a column from the command line:
```php
yii migrate/create drop_position_from_post --fields=position:integer
```
generates
```php
class m150811_220037_remove_position_from_post extends Migration
{
public function up()
{
this->dropColumn('post', 'position');
}
public function down()
{
this->addColumn('post', 'position', this->integer());
}
}
```
There is also a generator which will produce join tables If the migration name is of the form "create_join_xxx_and_yyy"
```php
yii create/migration create_join_post_and_tag
```
generates
```php
class m150811_220037_create_join_post_and_tag extends Migration
{
public function up()
{
\$this->createTable('post_tag', [
'post_id' => this->integer(),
'tag_id' => this->integer(),
'PRIMARY KEY(post_id, tag_id)'
]);
this->createIndex('idx-post_tag-post_id', 'post_tag', 'post_id');
this->createIndex('idx-post_tag-tag_id', 'post_tag', 'tag_id');
this->addForeignKey('fk-post_tag-post_id', 'post_tag', 'post_id', 'post', 'id', 'CASCADE');
this->addForeignKey('fk-post_tag-tag_id', 'post_tag', 'tag_id', 'tag', 'id', 'CASCADE');
}
public function down()
{
\$this->dropTable('post_tag');
}
}
```
### Transactional Migrations <span id="transactional-migrations"></span> ### Transactional Migrations <span id="transactional-migrations"></span>
While performing complex DB migrations, it is important to ensure each migration to either succeed or fail as a whole While performing complex DB migrations, it is important to ensure each migration to either succeed or fail as a whole
@ -409,6 +599,18 @@ The migration command comes with a few command-line options that can be used to
or a path [alias](concept-aliases.md). The template file is a PHP script in which you can use a predefined variable or a path [alias](concept-aliases.md). The template file is a PHP script in which you can use a predefined variable
named `$className` to get the migration class name. named `$className` to get the migration class name.
* `generatorTemplateFile`: array (defaults to `[
'create' => '@yii/views/createMigration.php',
'drop' => '@yii/views/dropMigration.php',
'add' => '@yii/views/addMigration.php',
'remove' => '@yii/views/removeMigration.php',
'create_join' => '@yii/views/createJoinMigration.php'
]`), specifies template files for generating migration code automatically. See [Creating Migrations with Generators](#creating-migrations-with-generators)
for more details.
* `fields`: array (defaults to `[]`), specifies the fields column use to creating migration automatically. The format that it use when declaring any applicable schema it is
`COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR`, for example `--fields=name:string(12):notNull`, it specify string column with 12 size and not null.
The following example shows how you can use these options. The following example shows how you can use these options.
For example, if we want to migrate a `forum` module whose migration files For example, if we want to migrate a `forum` module whose migration files

View File

@ -41,6 +41,18 @@ abstract class BaseMigrateController extends Controller
* or a file path. * or a file path.
*/ */
public $templateFile; public $templateFile;
/**
* @var array the template file for generating migration code automatically.
* This can be either a path alias (e.g. "@app/migrations/template.php")
* or a file path.
* @since 2.0.7
*/
public $generatorTemplateFile;
/**
* @var array Fields to be generated
* @since 2.0.7
*/
public $fields;
/** /**
@ -51,7 +63,9 @@ abstract class BaseMigrateController extends Controller
return array_merge( return array_merge(
parent::options($actionID), parent::options($actionID),
['migrationPath'], // global for all actions ['migrationPath'], // global for all actions
($actionID === 'create') ? ['templateFile'] : [] // action create ($actionID === 'create')
? ['templateFile', 'templateFileGenerators', 'fields']
: [] // action create
); );
} }
@ -73,6 +87,7 @@ abstract class BaseMigrateController extends Controller
FileHelper::createDirectory($path); FileHelper::createDirectory($path);
} }
$this->migrationPath = $path; $this->migrationPath = $path;
$this->parseField();
$version = Yii::getVersion(); $version = Yii::getVersion();
$this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n"); $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
@ -475,11 +490,46 @@ abstract class BaseMigrateController extends Controller
throw new Exception('The migration name should contain letters, digits and/or underscore characters only.'); throw new Exception('The migration name should contain letters, digits and/or underscore characters only.');
} }
$name = 'm' . gmdate('ymd_His') . '_' . $name; $className = 'm' . gmdate('ymd_His') . '_' . $name;
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; $file = $this->migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
if ($this->confirm("Create new migration '$file'?")) { if ($this->confirm("Create new migration '$file'?")) {
$content = $this->renderFile(Yii::getAlias($this->templateFile), ['className' => $name]); if (preg_match('/^create_join_(.+)_and_(.+)$/', $name, $matches)) {
$content = $this->renderFile(Yii::getAlias($this->generatorTemplateFile['create_join']), [
'className' => $className,
'table' => mb_strtolower($matches[1]) . '_' . mb_strtolower($matches[2]),
'field_first' => mb_strtolower($matches[1]),
'field_second' => mb_strtolower($matches[2]),
]);
} elseif (preg_match('/^add_(.+)from_(.+)$/', $name, $matches)) {
$content = $this->renderFile(Yii::getAlias($this->generatorTemplateFile['add']), [
'className' => $className,
'table' => mb_strtolower($matches[2]),
'fields' => $this->fields
]);
} elseif (preg_match('/^drop_(.+)from_(.+)$/', $name, $matches)) {
$content = $this->renderFile(Yii::getAlias($this->generatorTemplateFile['remove']), [
'className' => $className,
'table' => mb_strtolower($matches[2]),
'fields' => $this->fields
]);
} elseif (preg_match('/^create_(.+)$/', $name, $matches)) {
$this->checkPrimaryKey();
$content = $this->renderFile(Yii::getAlias($this->generatorTemplateFile['create']), [
'className' => $className,
'table' => mb_strtolower($matches[1]),
'fields' => $this->fields
]);
} elseif (preg_match('/^drop_(.+)$/', $name, $matches)) {
$content = $this->renderFile(Yii::getAlias($this->generatorTemplateFile['drop']), [
'className' => $className,
'table' => mb_strtolower($matches[1]),
'fields' => $this->fields
]);
} else {
$content = $this->renderFile(Yii::getAlias($this->templateFile), ['className' => $className]);
}
file_put_contents($file, $content); file_put_contents($file, $content);
$this->stdout("New migration created successfully.\n", Console::FG_GREEN); $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
} }
@ -638,6 +688,46 @@ abstract class BaseMigrateController extends Controller
return $migrations; return $migrations;
} }
/**
* Parse the command line migration fields.
* @since 2.0.7
*/
protected function parseField()
{
if ($this->fields === null) {
$this->fields = [];
}
foreach ($this->fields as $index => $field) {
$chunks = preg_split('/\s?:\s?/', $field, null);
$property = array_shift($chunks);
foreach ($chunks as &$chunk) {
if (!preg_match('/(.+?)\(([^)]+)\)/', $chunk)) {
$chunk = $chunk . '()';
}
}
$this->fields[$index] = ['property' => $property, 'decorators' => implode('->', $chunks)];
}
}
/**
* Check fields option contain primaryKey, if fields do not contain primary key it is added
* @since 2.0.7
*/
protected function checkPrimaryKey()
{
$exitsPk = false;
foreach ($this->fields as $field) {
if ($field['decorators'] === 'primaryKey()') {
$exitsPk = true;
}
}
if (!$exitsPk) {
array_unshift($this->fields, ['property' => 'id', 'decorators' => 'primaryKey()']);
}
}
/** /**
* Returns the migration history. * Returns the migration history.
* @param integer $limit the maximum number of records in the history to be returned. `null` for "no limit". * @param integer $limit the maximum number of records in the history to be returned. `null` for "no limit".

View File

@ -63,6 +63,16 @@ class MigrateController extends BaseMigrateController
* @inheritdoc * @inheritdoc
*/ */
public $templateFile = '@yii/views/migration.php'; public $templateFile = '@yii/views/migration.php';
/**
* @inheritdoc
*/
public $generatorTemplateFile = [
'create' => '@yii/views/createMigration.php',
'drop' => '@yii/views/dropMigration.php',
'add' => '@yii/views/addColumnMigration.php',
'remove' => '@yii/views/dropColumnMigration.php',
'create_join' => '@yii/views/createJoinMigration.php'
];
/** /**
* @var Connection|array|string the DB connection object or the application component ID of the DB connection to use * @var Connection|array|string the DB connection object or the application component ID of the DB connection to use
* when applying migrations. Starting from version 2.0.3, this can also be a configuration array * when applying migrations. Starting from version 2.0.3, this can also be a configuration array

View File

@ -0,0 +1,30 @@
<?php
/**
* This view is used by console/controllers/MigrateController.php
* The following variables are available in this view:
*/
/* @var $className string the new migration class name */
/* @var $table string the name table */
/* @var $fields array the fields */
echo "<?php\n";
?>
use yii\db\Migration;
class <?= $className ?> extends Migration
{
public function up()
{
<?php foreach ($fields as $field): ?>
$this->addColumn(<?= "'$table', '" . $field['property'] . "', \$this->" . $field['decorators'] ?>);
<?php endforeach; ?>
}
public function down()
{
<?php foreach ($fields as $field): ?>
$this->dropColumn(<?= "'$table', '" . $field['property'] . "'" ?>);
<?php endforeach; ?>
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* This view is used by console/controllers/MigrateController.php
* The following variables are available in this view:
*/
/* @var $className string the new migration class name */
/* @var $table string the name table */
/* @var $field_first string the name field first */
/* @var $field_second string the name field second */
echo "<?php\n";
?>
use yii\db\Migration;
class <?= $className ?> extends Migration
{
public function up()
{
$this->createTable('<?= $table ?>', [
'<?= $field_first ?>_id' => $this->integer(),
'<?= $field_second ?>_id' => $this->integer(),
'PRIMARY KEY(<?= $field_first ?>_id, <?= $field_second ?>_id)'
]);
$this->createIndex('idx-<?= $table . '-' . $field_first ?>_id', '<?= $table ?>', '<?= $field_first ?>_id');
$this->createIndex('idx-<?= $table . '-' . $field_second ?>_id', '<?= $table ?>', '<?= $field_second ?>_id');
$this->addForeignKey('fk-<?= $table . '-' . $field_first ?>_id', '<?= $table ?>', '<?= $field_first ?>_id', '<?= $field_first ?>', 'id', 'CASCADE');
$this->addForeignKey('fk-<?= $table . '-' . $field_second ?>_id', '<?= $table ?>', '<?= $field_second ?>_id', '<?= $field_second ?>', 'id', 'CASCADE');
}
public function down()
{
$this->dropTable('<?= $table ?>');
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* This view is used by console/controllers/MigrateController.php
* The following variables are available in this view:
*/
/* @var $className string the new migration class name */
/* @var $table string the name table */
/* @var $fields array the fields */
echo "<?php\n";
?>
use yii\db\Migration;
class <?= $className ?> extends Migration
{
public function up()
{
$this->createTable('<?= $table ?>', [
<?php foreach ($fields as $field): ?>
<?php if ($field == end($fields)): ?>
'<?= $field['property'] ?>' => $this-><?= $field['decorators'] . "\n"?>
<?php else: ?>
'<?= $field['property'] ?>' => $this-><?= $field['decorators'] . ",\n"?>
<?php endif; ?>
<?php endforeach; ?>
]);
}
public function down()
{
$this->dropTable('<?= $table ?>');
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* This view is used by console/controllers/MigrateController.php
* The following variables are available in this view:
*/
/* @var $className string the new migration class name */
/* @var $table string the name table */
/* @var $fields array the fields */
echo "<?php\n";
?>
use yii\db\Migration;
class <?= $className ?> extends Migration
{
public function up()
{
<?php foreach ($fields as $field): ?>
$this->dropColumn(<?= "'$table', '" . $field['property'] . "'" ?>);
<?php endforeach; ?>
}
public function down()
{
<?php foreach ($fields as $field): ?>
$this->addColumn(<?= "'$table', '" . $field['property'] . "', \$this->" . $field['decorators'] ?>);
<?php endforeach; ?>
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* This view is used by console/controllers/MigrateController.php
* The following variables are available in this view:
*/
/* @var $className string the new migration class name */
/* @var $table string the name table */
/* @var $fields array the fields */
echo "<?php\n";
?>
use yii\db\Migration;
class <?= $className ?> extends Migration
{
public function up()
{
$this->dropTable('<?= $table ?>');
}
public function down()
{
$this->createTable('<?= $table ?>', [
<?php foreach ($fields as $field): ?>
<?php if ($field == end($fields)): ?>
'<?= $field['property'] ?>' => $this-><?= $field['decorators'] . "\n"?>
<?php else: ?>
'<?= $field['property'] ?>' => $this-><?= $field['decorators'] . ",\n"?>
<?php endif; ?>
<?php endforeach; ?>
]);
}
}

View File

@ -149,6 +149,375 @@ CODE;
$this->assertContains($migrationName, basename($files[0]), 'Wrong migration name!'); $this->assertContains($migrationName, basename($files[0]), 'Wrong migration name!');
} }
public function testGenerateDefaultMigration()
{
$migrationName = 'DefaultTest';
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [$migrationName]);
$files = FileHelper::findFiles($this->migrationPath);
$newLine = '\n';
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
}
public function down()
{
echo "{$class} cannot be reverted.{$newLine}";
return false;
}
/*
// Use safeUp/safeDown to run migration code within a transaction
public function safeUp()
{
}
public function safeDown()
{
}
*/
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
}
public function testGenerateCreateMigration()
{
$migrationName = 'create_test';
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
'title:string(10):notNull:unique:defaultValue("test")',
'body:text:notNull'
]
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->createTable('test', [
'id' => \$this->primaryKey(),
'title' => \$this->string(10)->notNull()->unique()->defaultValue("test"),
'body' => \$this->text()->notNull()
]);
}
public function down()
{
\$this->dropTable('test');
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
'title:primaryKey',
'body:text:notNull'
],
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->createTable('test', [
'title' => \$this->primaryKey(),
'body' => \$this->text()->notNull()
]);
}
public function down()
{
\$this->dropTable('test');
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
],
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->createTable('test', [
'id' => \$this->primaryKey()
]);
}
public function down()
{
\$this->dropTable('test');
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
}
public function testGenerateDropMigration()
{
$migrationName = 'drop_test';
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->dropTable('test');
}
public function down()
{
\$this->createTable('test', [
]);
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
'title:primaryKey',
'body:text:notNull'
],
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->dropTable('test');
}
public function down()
{
\$this->createTable('test', [
'title' => \$this->primaryKey(),
'body' => \$this->text()->notNull()
]);
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
}
public function testGenerateAddColumnMigration()
{
$migrationName = 'add_columns_from_test';
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
'title:string(10):notNull',
'body:text:notNull',
'create_at:dateTime'
]
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->addColumn('test', 'title', \$this->string(10)->notNull());
\$this->addColumn('test', 'body', \$this->text()->notNull());
\$this->addColumn('test', 'create_at', \$this->dateTime());
}
public function down()
{
\$this->dropColumn('test', 'title');
\$this->dropColumn('test', 'body');
\$this->dropColumn('test', 'create_at');
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
}
public function testGenerateDropColumnMigration()
{
$migrationName = 'drop_columns_from_test';
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
'title:string(10):notNull',
'body:text:notNull',
'create_at:dateTime'
]
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->dropColumn('test', 'title');
\$this->dropColumn('test', 'body');
\$this->dropColumn('test', 'create_at');
}
public function down()
{
\$this->addColumn('test', 'title', \$this->string(10)->notNull());
\$this->addColumn('test', 'body', \$this->text()->notNull());
\$this->addColumn('test', 'create_at', \$this->dateTime());
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
'fields' => [
'title:string(10):notNull',
'body:text:notNull',
'create_at:dateTime'
]
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->dropColumn('test', 'title');
\$this->dropColumn('test', 'body');
\$this->dropColumn('test', 'create_at');
}
public function down()
{
\$this->addColumn('test', 'title', \$this->string(10)->notNull());
\$this->addColumn('test', 'body', \$this->text()->notNull());
\$this->addColumn('test', 'create_at', \$this->dateTime());
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
}
public function testGenerateCreateJoinMigration()
{
$migrationName = 'create_join_post_and_tag';
$class = 'm' . gmdate('ymd_His') . '_' . $migrationName;
$this->runMigrateControllerAction('create', [
$migrationName,
]);
$files = FileHelper::findFiles($this->migrationPath);
$code = <<<CODE
<?php
use yii\db\Migration;
class {$class} extends Migration
{
public function up()
{
\$this->createTable('post_tag', [
'post_id' => \$this->integer(),
'tag_id' => \$this->integer(),
'PRIMARY KEY(post_id, tag_id)'
]);
\$this->createIndex('idx-post_tag-post_id', 'post_tag', 'post_id');
\$this->createIndex('idx-post_tag-tag_id', 'post_tag', 'tag_id');
\$this->addForeignKey('fk-post_tag-post_id', 'post_tag', 'post_id', 'post', 'id', 'CASCADE');
\$this->addForeignKey('fk-post_tag-tag_id', 'post_tag', 'tag_id', 'tag', 'id', 'CASCADE');
}
public function down()
{
\$this->dropTable('post_tag');
}
}
CODE;
$this->assertEqualsWithoutLE($code, file_get_contents($files[0]));
}
public function testUp() public function testUp()
{ {
$this->createMigration('test1'); $this->createMigration('test1');