diff --git a/docs/guide/db-migrations.md b/docs/guide/db-migrations.md
index c57ca59208..5349a07e1e 100644
--- a/docs/guide/db-migrations.md
+++ b/docs/guide/db-migrations.md
@@ -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]].
+## Creating Migrations with Generators
+
+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
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
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.
For example, if we want to migrate a `forum` module whose migration files
diff --git a/framework/console/controllers/BaseMigrateController.php b/framework/console/controllers/BaseMigrateController.php
index 012c1a54db..d8f3c729af 100644
--- a/framework/console/controllers/BaseMigrateController.php
+++ b/framework/console/controllers/BaseMigrateController.php
@@ -41,6 +41,18 @@ abstract class BaseMigrateController extends Controller
* or a file path.
*/
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(
parent::options($actionID),
['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);
}
$this->migrationPath = $path;
+ $this->parseField();
$version = Yii::getVersion();
$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.');
}
- $name = 'm' . gmdate('ymd_His') . '_' . $name;
- $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php';
+ $className = 'm' . gmdate('ymd_His') . '_' . $name;
+ $file = $this->migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
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);
$this->stdout("New migration created successfully.\n", Console::FG_GREEN);
}
@@ -638,6 +688,46 @@ abstract class BaseMigrateController extends Controller
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.
* @param integer $limit the maximum number of records in the history to be returned. `null` for "no limit".
diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php
index 99523b39dc..8e6f833b3f 100644
--- a/framework/console/controllers/MigrateController.php
+++ b/framework/console/controllers/MigrateController.php
@@ -63,6 +63,16 @@ class MigrateController extends BaseMigrateController
* @inheritdoc
*/
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
* when applying migrations. Starting from version 2.0.3, this can also be a configuration array
diff --git a/framework/views/addColumnMigration.php b/framework/views/addColumnMigration.php
new file mode 100644
index 0000000000..4135ae5a37
--- /dev/null
+++ b/framework/views/addColumnMigration.php
@@ -0,0 +1,30 @@
+
+
+use yii\db\Migration;
+
+class = $className ?> extends Migration
+{
+ public function up()
+ {
+
+ $this->addColumn(= "'$table', '" . $field['property'] . "', \$this->" . $field['decorators'] ?>);
+
+ }
+
+ public function down()
+ {
+
+ $this->dropColumn(= "'$table', '" . $field['property'] . "'" ?>);
+
+ }
+}
diff --git a/framework/views/createJoinMigration.php b/framework/views/createJoinMigration.php
new file mode 100644
index 0000000000..6a8c7f309a
--- /dev/null
+++ b/framework/views/createJoinMigration.php
@@ -0,0 +1,37 @@
+
+
+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 ?>');
+ }
+}
diff --git a/framework/views/createMigration.php b/framework/views/createMigration.php
new file mode 100644
index 0000000000..4532af3e79
--- /dev/null
+++ b/framework/views/createMigration.php
@@ -0,0 +1,34 @@
+
+
+use yii\db\Migration;
+
+class = $className ?> extends Migration
+{
+ public function up()
+ {
+ $this->createTable('= $table ?>', [
+
+
+ '= $field['property'] ?>' => $this->= $field['decorators'] . "\n"?>
+
+ '= $field['property'] ?>' => $this->= $field['decorators'] . ",\n"?>
+
+
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('= $table ?>');
+ }
+}
diff --git a/framework/views/dropColumnMigration.php b/framework/views/dropColumnMigration.php
new file mode 100644
index 0000000000..a307bf9f85
--- /dev/null
+++ b/framework/views/dropColumnMigration.php
@@ -0,0 +1,30 @@
+
+
+use yii\db\Migration;
+
+class = $className ?> extends Migration
+{
+ public function up()
+ {
+
+ $this->dropColumn(= "'$table', '" . $field['property'] . "'" ?>);
+
+ }
+
+ public function down()
+ {
+
+ $this->addColumn(= "'$table', '" . $field['property'] . "', \$this->" . $field['decorators'] ?>);
+
+ }
+}
diff --git a/framework/views/dropMigration.php b/framework/views/dropMigration.php
new file mode 100644
index 0000000000..72f072453a
--- /dev/null
+++ b/framework/views/dropMigration.php
@@ -0,0 +1,34 @@
+
+
+use yii\db\Migration;
+
+class = $className ?> extends Migration
+{
+ public function up()
+ {
+ $this->dropTable('= $table ?>');
+ }
+
+ public function down()
+ {
+ $this->createTable('= $table ?>', [
+
+
+ '= $field['property'] ?>' => $this->= $field['decorators'] . "\n"?>
+
+ '= $field['property'] ?>' => $this->= $field['decorators'] . ",\n"?>
+
+
+ ]);
+ }
+}
diff --git a/tests/framework/console/controllers/MigrateControllerTestTrait.php b/tests/framework/console/controllers/MigrateControllerTestTrait.php
index ec8315bffa..aa8dfaf8c6 100644
--- a/tests/framework/console/controllers/MigrateControllerTestTrait.php
+++ b/tests/framework/console/controllers/MigrateControllerTestTrait.php
@@ -149,6 +149,375 @@ CODE;
$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 = <<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 = <<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 = <<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 = <<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 = <<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 = <<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 = <<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 = <<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 = <<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 = <<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()
{
$this->createMigration('test1');
@@ -255,4 +624,4 @@ CODE;
$this->assertMigrationHistory(['base', 'test1']);
}
-}
\ No newline at end of file
+}