From 9afd240ab6e4f6b51d274e413b7879cae452c590 Mon Sep 17 00:00:00 2001 From: Daniel Gomez Pan Date: Fri, 20 Nov 2015 21:35:58 +0300 Subject: [PATCH] Fixes #9465: ./yii migrate/create now generates code based on migration name and --fields --- docs/guide/db-migrations.md | 202 ++++++++++ .../controllers/BaseMigrateController.php | 98 ++++- .../console/controllers/MigrateController.php | 10 + framework/views/addColumnMigration.php | 30 ++ framework/views/createJoinMigration.php | 37 ++ framework/views/createMigration.php | 34 ++ framework/views/dropColumnMigration.php | 30 ++ framework/views/dropMigration.php | 34 ++ .../MigrateControllerTestTrait.php | 371 +++++++++++++++++- 9 files changed, 841 insertions(+), 5 deletions(-) create mode 100644 framework/views/addColumnMigration.php create mode 100644 framework/views/createJoinMigration.php create mode 100644 framework/views/createMigration.php create mode 100644 framework/views/dropColumnMigration.php create mode 100644 framework/views/dropMigration.php 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 extends Migration +{ + public function up() + { + + $this->addColumn(" . $field['decorators'] ?>); + + } + + public function down() + { + + $this->dropColumn(); + + } +} 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 extends Migration +{ + public function up() + { + $this->createTable('', [ + '_id' => $this->integer(), + '_id' => $this->integer(), + 'PRIMARY KEY(_id, _id)' + ]); + + $this->createIndex('idx-_id', '', '_id'); + $this->createIndex('idx-_id', '', '_id'); + + $this->addForeignKey('fk-_id', '', '_id', '', 'id', 'CASCADE'); + $this->addForeignKey('fk-_id', '', '_id', '', 'id', 'CASCADE'); + } + + public function down() + { + $this->dropTable(''); + } +} 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 extends Migration +{ + public function up() + { + $this->createTable('', [ + + + '' => $this-> + + '' => $this-> + + + ]); + } + + public function down() + { + $this->dropTable(''); + } +} 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 extends Migration +{ + public function up() + { + + $this->dropColumn(); + + } + + public function down() + { + + $this->addColumn(" . $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 extends Migration +{ + public function up() + { + $this->dropTable(''); + } + + public function down() + { + $this->createTable('', [ + + + '' => $this-> + + '' => $this-> + + + ]); + } +} 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 +}