diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 743ba8caad..8cdff3cc34 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -36,6 +36,9 @@ Yii Framework 2 Change Log - Bug #11541: Fixed default MySQL integer display width for unsigned primary key (h311ion, rob006, cebe) - Bug #12143: Fixed `yii\db\BaseActiveRecord::updateAttributes()` change `isNewRecord` state for the new model (klimov-paul) - Bug #12463: Fixed `yii\web\Request::getBodyParams()` does not pass full 'content-type' value to `yii\web\RequestParserInterface::parse()` (klimov-paul) +- Enh #384: Added ability to run migration from several locations via [[yii\console\controllers\BaseMigrateController::migrationNamespaces]] (klimov-paul) +- Enh #9469: Added support for namespaced migrations via [[yii\console\controllers\BaseMigrateController::migrationNamespaces]] (klimov-paul) +- Enh #11096: Added support for PSR-2 style migration naming for namespaced migrations (klimov-paul) - Enh #9708: Added `yii\console\controllers\AssetController::deleteSource` option allowing deletion of the source asset files after compression (pana1990, klimov-paul) - Enh #10243: Added `yii\data\Sort::setAttributeOrders()` method allowing manual setup of current sort (klimov-paul) - Enh #12440: Added `yii\base\Event::offAll()` method allowing clear all registered class-level event handlers (klimov-paul) diff --git a/framework/console/controllers/BaseMigrateController.php b/framework/console/controllers/BaseMigrateController.php index a1ab0bb328..2318aa72b1 100644 --- a/framework/console/controllers/BaseMigrateController.php +++ b/framework/console/controllers/BaseMigrateController.php @@ -8,6 +8,7 @@ namespace yii\console\controllers; use Yii; +use yii\base\InvalidConfigException; use yii\console\Exception; use yii\console\Controller; use yii\helpers\Console; @@ -33,8 +34,30 @@ abstract class BaseMigrateController extends Controller /** * @var string the directory storing the migration classes. This can be either * a path alias or a directory. + * + * You may set this field to `null` in case you have set up [[migrationNamespaces]] in order + * to disable usage of migrations without namespace. */ public $migrationPath = '@app/migrations'; + /** + * @var array list of namespaces, which are holding migration classes. + * + * Migration namespace should be available to be resolved as path alias if prefixed with `@`, e.g. if you specify + * namespace `app\migrations` code `Yii::getAlias('@app/migrations')` should be able to return file path + * to the directory this namespace refers to. + * + * For example: + * + * ```php + * [ + * 'app\migrations', + * 'some\extension\migrations', + * ] + * ``` + * + * @since 2.0.10 + */ + public $migrationNamespaces = []; /** * @var string the template file for generating new migrations. * This can be either a path alias (e.g. "@app/migrations/template.php") @@ -59,20 +82,30 @@ abstract class BaseMigrateController extends Controller * This method is invoked right before an action is to be executed (after all possible filters.) * It checks the existence of the [[migrationPath]]. * @param \yii\base\Action $action the action to be executed. - * @throws Exception if directory specified in migrationPath doesn't exist and action isn't "create". + * @throws InvalidConfigException if directory specified in migrationPath doesn't exist and action isn't "create". * @return boolean whether the action should continue to be executed. */ public function beforeAction($action) { if (parent::beforeAction($action)) { - $path = Yii::getAlias($this->migrationPath); - if (!is_dir($path)) { - if ($action->id !== 'create') { - throw new Exception("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}"); - } - FileHelper::createDirectory($path); + if (empty($this->migrationNamespaces) && empty($this->migrationPath)) { + throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.'); + } + + foreach ($this->migrationNamespaces as $key => $value) { + $this->migrationNamespaces[$key] = trim($value, '\\'); + } + + if ($this->migrationPath !== null) { + $path = Yii::getAlias($this->migrationPath); + if (!is_dir($path)) { + if ($action->id !== 'create') { + throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}"); + } + FileHelper::createDirectory($path); + } + $this->migrationPath = $path; } - $this->migrationPath = $path; $version = Yii::getVersion(); $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n"); @@ -278,10 +311,11 @@ abstract class BaseMigrateController extends Controller * them again. For example, * * ``` - * yii migrate/to 101129_185401 # using timestamp - * yii migrate/to m101129_185401_create_user_table # using full name - * yii migrate/to 1392853618 # using UNIX timestamp - * yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string + * yii migrate/to 101129_185401 # using timestamp + * yii migrate/to m101129_185401_create_user_table # using full name + * yii migrate/to 1392853618 # using UNIX timestamp + * yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string + * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name * ``` * * @param string $version either the version name or the certain time value in the past @@ -292,14 +326,16 @@ abstract class BaseMigrateController extends Controller */ public function actionTo($version) { - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $this->migrateToVersion('m' . $matches[1]); + if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) { + $this->migrateToVersion($namespaceVersion); + } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) { + $this->migrateToVersion($migrationName); } elseif ((string) (int) $version == $version) { $this->migrateToTime($version); } elseif (($time = strtotime($version)) !== false) { $this->migrateToTime($time); } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50)."); + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n the full namespaced name of a migration (e.g. app\\migrations\\M101129185401CreateUserTable),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50)."); } } @@ -309,8 +345,9 @@ abstract class BaseMigrateController extends Controller * No actual migration will be performed. * * ``` - * yii migrate/mark 101129_185401 # using timestamp - * yii migrate/mark m101129_185401_create_user_table # using full name + * yii migrate/mark 101129_185401 # using timestamp + * yii migrate/mark m101129_185401_create_user_table # using full name + * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name * ``` * * @param string $version the version at which the migration history should be marked. @@ -321,16 +358,18 @@ abstract class BaseMigrateController extends Controller public function actionMark($version) { $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; + if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) { + $version = $namespaceVersion; + } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) { + $version = $migrationName; } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)\nor the full name of a namespaced migration (e.g. app\\migrations\\M101129185401CreateUserTable)."); } // try mark up $migrations = $this->getNewMigrations(); foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { + if (strpos($migration, $version) === 0) { if ($this->confirm("Set migration history at $originalVersion?")) { for ($j = 0; $j <= $i; ++$j) { $this->addMigrationHistory($migrations[$j]); @@ -345,7 +384,7 @@ abstract class BaseMigrateController extends Controller // try mark down $migrations = array_keys($this->getMigrationHistory(null)); foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { + if (strpos($migration, $version) === 0) { if ($i === 0) { $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW); } else { @@ -364,6 +403,34 @@ abstract class BaseMigrateController extends Controller throw new Exception("Unable to find the version '$originalVersion'."); } + /** + * Checks if given migration version specification matches namespaced migration name. + * @param string $rawVersion raw version specification received from user input. + * @return string|false actual migration version, `false` - if not match. + * @since 2.0.10 + */ + private function extractNamespaceMigrationVersion($rawVersion) + { + if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*?)?$/is', $rawVersion, $matches)) { + return trim($rawVersion, '\\'); + } + return false; + } + + /** + * Checks if given migration version specification matches migration base name. + * @param string $rawVersion raw version specification received from user input. + * @return string|false actual migration version, `false` - if not match. + * @since 2.0.10 + */ + private function extractMigrationVersion($rawVersion) + { + if (preg_match('/^m?(\d{6}_?\d{6})(\D.*?)?$/is', $rawVersion, $matches)) { + return 'm' . $matches[1]; + } + return false; + } + /** * Displays the migration history. * @@ -465,8 +532,19 @@ abstract class BaseMigrateController extends Controller * yii migrate/create create_user_table * ``` * + * In order to generate namespaced migration you should specify namespace before migration's name. + * Note that backslash (`\`) usually is considered as a special char in console, so you need to escape argument + * properly to avoid shell error or incorrect behavior. + * For example: + * + * ``` + * yii migrate/create 'app\\migrations\\createUserTable' + * ``` + * + * In case [[migrationPath]] is not set and no namespace provided the first entry of [[migrationNamespaces]] will be used. + * * @param string $name the name of the new migration. This should only contain - * letters, digits and/or underscores. + * letters, digits, underscores and/or backslashes. * * Note: If the migration name is of a special form, for example create_xxx or * drop_xxx then the generated migration file will contain extra code, @@ -476,22 +554,75 @@ abstract class BaseMigrateController extends Controller */ public function actionCreate($name) { - if (!preg_match('/^\w+$/', $name)) { - throw new Exception('The migration name should contain letters, digits and/or underscore characters only.'); + if (!preg_match('/^[\w\\\\]+$/', $name)) { + throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.'); } - $className = 'm' . gmdate('ymd_His') . '_' . $name; - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $className . '.php'; + list($namespace, $className) = $this->generateClassName($name); + $migrationPath = $this->findMigrationPath($namespace); + + $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php'; if ($this->confirm("Create new migration '$file'?")) { $content = $this->generateMigrationSourceCode([ 'name' => $name, 'className' => $className, + 'namespace' => $namespace, ]); + FileHelper::createDirectory($migrationPath); file_put_contents($file, $content); $this->stdout("New migration created successfully.\n", Console::FG_GREEN); } } + /** + * Generates class base name and namespace from migration name from user input. + * @param string $name migration name from user input. + * @return array list of 2 elements: 'namespace' and 'class base name' + * @since 2.0.10 + */ + private function generateClassName($name) + { + $namespace = null; + $name = trim($name, '\\'); + if (strpos($name, '\\') !== false) { + $namespace = substr($name, 0, strrpos($name, '\\')); + $name = substr($name, strrpos($name, '\\') + 1); + } else { + if ($this->migrationPath === null) { + $migrationNamespaces = $this->migrationNamespaces; + $namespace = array_shift($migrationNamespaces); + } + } + + if ($namespace === null) { + $class = 'm' . gmdate('ymd_His') . '_' . $name; + } else { + $class = 'M' . gmdate('ymdHis') . ucfirst($name); + } + + return [$namespace, $class]; + } + + /** + * Finds the file path for the specified migration namespace. + * @param string|null $namespace migration namespace. + * @return string migration file path. + * @throws Exception on failure. + * @since 2.0.10 + */ + private function findMigrationPath($namespace) + { + if (empty($namespace)) { + return $this->migrationPath; + } + + if (!in_array($namespace, $this->migrationNamespaces, true)) { + throw new Exception("Namespace '{$namespace}' is not mentioned among `migrationNamespaces`"); + } + + return Yii::getAlias('@' . str_replace('\\', DIRECTORY_SEPARATOR, $namespace)); + } + /** * Upgrades with the specified migration class. * @param string $class the migration class name @@ -539,7 +670,6 @@ abstract class BaseMigrateController extends Controller $time = microtime(true) - $start; $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN); - return true; } else { $time = microtime(true) - $start; @@ -556,8 +686,11 @@ abstract class BaseMigrateController extends Controller */ protected function createMigration($class) { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); + $class = trim($class, '\\'); + if (strpos($class, '\\') === false) { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + } return new $class(); } @@ -593,7 +726,7 @@ abstract class BaseMigrateController extends Controller // try migrate up $migrations = $this->getNewMigrations(); foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { + if (strpos($migration, $version) === 0) { $this->actionUp($i + 1); return self::EXIT_CODE_NORMAL; @@ -603,7 +736,7 @@ abstract class BaseMigrateController extends Controller // try migrate down $migrations = array_keys($this->getMigrationHistory(null)); foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { + if (strpos($migration, $version) === 0) { if ($i === 0) { $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW); } else { @@ -624,25 +757,45 @@ abstract class BaseMigrateController extends Controller protected function getNewMigrations() { $applied = []; - foreach ($this->getMigrationHistory(null) as $version => $time) { - $applied[substr($version, 1, 13)] = true; + foreach ($this->getMigrationHistory(null) as $class => $time) { + $applied[trim($class, '\\')] = true; + } + + $migrationPaths = []; + if (!empty($this->migrationPath)) { + $migrationPaths[''] = $this->migrationPath; + } + foreach ($this->migrationNamespaces as $namespace) { + $migrationPaths[$namespace] = Yii::getAlias('@' . str_replace('\\', DIRECTORY_SEPARATOR, $namespace)); } $migrations = []; - $handle = opendir($this->migrationPath); - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { + foreach ($migrationPaths as $namespace => $migrationPath) { + if (!file_exists($migrationPath)) { continue; } - $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; - if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && !isset($applied[$matches[2]]) && is_file($path)) { - $migrations[] = $matches[1]; + $handle = opendir($migrationPath); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $migrationPath . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) { + $class = $matches[1]; + if (!empty($namespace)) { + $class = $namespace . '\\' . $class; + } + $time = str_replace('_', '', $matches[2]); + if (!isset($applied[$class])) { + $migrations[$time . '\\' . $class] = $class; + } + } } + closedir($handle); } - closedir($handle); - sort($migrations); + ksort($migrations); - return $migrations; + return array_values($migrations); } /** diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 2f492a1c16..31898f9475 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -50,6 +50,24 @@ use yii\helpers\Console; * yii migrate/down * ``` * + * Since 2.0.10 you can use namespaced migrations. In order to enable this feature you should configure [[migrationNamespaces]] + * property for the controller at application configuration: + * + * ```php + * return [ + * 'controllerMap' => [ + * 'migrate' => [ + * 'class' => 'yii\console\controllers\MigrateController', + * 'migrationNamespaces' => [ + * 'app\migrations', + * 'some\extension\migrations', + * ], + * //'migrationPath' => null, // allows to disable not namespaced migration completely + * ], + * ], + * ]; + * ``` + * * @author Qiang Xue * @since 2.0 */ @@ -164,8 +182,11 @@ class MigrateController extends BaseMigrateController */ protected function createMigration($class) { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); + $class = trim($class, '\\'); + if (strpos($class, '\\') === false) { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + } return new $class(['db' => $this->db]); } diff --git a/framework/views/addColumnMigration.php b/framework/views/addColumnMigration.php index ec56751022..edce9aa4d2 100644 --- a/framework/views/addColumnMigration.php +++ b/framework/views/addColumnMigration.php @@ -3,7 +3,8 @@ * 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 $className string the new migration class name without namespace */ +/* @var $namespace string the new migration class namespace */ /* @var $table string the name table */ /* @var $fields array the fields */ @@ -11,6 +12,9 @@ preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches); $columns = $matches[1]; echo " use yii\db\Migration; diff --git a/framework/views/createJunctionMigration.php b/framework/views/createJunctionMigration.php index 0aaf31e628..4df1735f9a 100644 --- a/framework/views/createJunctionMigration.php +++ b/framework/views/createJunctionMigration.php @@ -5,12 +5,16 @@ * @since 2.0.7 * @deprecated since 2.0.8 */ -/* @var $className string the new migration class name */ +/* @var $className string the new migration class name without namespace */ +/* @var $namespace string the new migration class namespace */ /* @var $table string the name table */ /* @var $field_first string the name field first */ /* @var $field_second string the name field second */ echo " use yii\db\Migration; diff --git a/framework/views/createTableMigration.php b/framework/views/createTableMigration.php index e757eb0bc5..619633f18b 100644 --- a/framework/views/createTableMigration.php +++ b/framework/views/createTableMigration.php @@ -3,12 +3,16 @@ * 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 $className string the new migration class name without namespace */ +/* @var $namespace string the new migration class namespace */ /* @var $table string the name table */ /* @var $fields array the fields */ /* @var $foreignKeys array the foreign keys */ echo " use yii\db\Migration; diff --git a/framework/views/dropColumnMigration.php b/framework/views/dropColumnMigration.php index e75d46b50b..5b78e817e3 100644 --- a/framework/views/dropColumnMigration.php +++ b/framework/views/dropColumnMigration.php @@ -3,13 +3,17 @@ * 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 $className string the new migration class name without namespace */ +/* @var $namespace string the new migration class namespace */ /* @var $table string the name table */ /* @var $fields array the fields */ preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches); $columns = $matches[1]; echo " use yii\db\Migration; diff --git a/framework/views/dropTableMigration.php b/framework/views/dropTableMigration.php index 4758b0da48..9e9ef78025 100644 --- a/framework/views/dropTableMigration.php +++ b/framework/views/dropTableMigration.php @@ -3,11 +3,15 @@ * 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 $className string the new migration class name without namespace */ +/* @var $namespace string the new migration class namespace */ /* @var $table string the name table */ /* @var $fields array the fields */ echo " use yii\db\Migration; diff --git a/framework/views/migration.php b/framework/views/migration.php index 52241db1a8..243b41f7da 100644 --- a/framework/views/migration.php +++ b/framework/views/migration.php @@ -3,9 +3,13 @@ * 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 $className string the new migration class name without namespace */ +/* @var $namespace string the new migration class namespace */ echo " use yii\db\Migration; diff --git a/tests/framework/console/controllers/MigrateControllerTest.php b/tests/framework/console/controllers/MigrateControllerTest.php index 02346feaaa..fe772576e9 100644 --- a/tests/framework/console/controllers/MigrateControllerTest.php +++ b/tests/framework/console/controllers/MigrateControllerTest.php @@ -50,4 +50,156 @@ class MigrateControllerTest extends TestCase $query = new Query(); return $query->from('migration')->all(); } + + protected function assertFileContent($expectedFile, $class) + { + $this->assertEqualsWithoutLE( + include Yii::getAlias("@yiiunit/data/console/migrate_create/$expectedFile.php"), + $this->parseNameClassMigration($class) + ); + } + + protected function assertCommandCreatedFile($expectedFile, $migrationName, $params = []) + { + $class = 'm' . gmdate('ymd_His') . '_' . $migrationName; + $params[0] = $migrationName; + $this->runMigrateControllerAction('create', $params); + $this->assertFileContent($expectedFile, $class); + } + + // Tests : + + public function testGenerateDefaultMigration() + { + $this->assertCommandCreatedFile('default', 'DefaultTest'); + } + + public function testGenerateCreateMigration() + { + $migrationNames = [ + 'create_test_table', + ]; + foreach ($migrationNames as $migrationName) { + $this->assertCommandCreatedFile('create_test', $migrationName); + + $this->assertCommandCreatedFile('create_fields', $migrationName, [ + 'fields' => 'title:string(10):notNull:unique:defaultValue("test"), + body:text:notNull, + price:money(11,2):notNull, + parenthesis_in_comment:string(255):notNull:comment(\'Name of set (RU)\')' + ]); + + $this->assertCommandCreatedFile('create_title_pk', $migrationName, [ + 'fields' => 'title:primaryKey,body:text:notNull,price:money(11,2)', + ]); + + $this->assertCommandCreatedFile('create_id_pk', $migrationName, [ + 'fields' => 'id:primaryKey, + address:string, + address2:string, + email:string', + ]); + + $this->assertCommandCreatedFile('create_foreign_key', $migrationName, [ + 'fields' => 'user_id:integer:foreignKey, + product_id:foreignKey:integer:unsigned:notNull, + order_id:integer:foreignKey(user_order):notNull, + created_at:dateTime:notNull', + ]); + + $this->assertCommandCreatedFile('create_prefix', $migrationName, [ + 'useTablePrefix' => true, + 'fields' => 'user_id:integer:foreignKey, + product_id:foreignKey:integer:unsigned:notNull, + order_id:integer:foreignKey(user_order):notNull, + created_at:dateTime:notNull', + ]); + } + + // @see https://github.com/yiisoft/yii2/issues/10876 + $this->assertCommandCreatedFile('create_products_from_store_table', 'create_products_from_store_table'); + + // @see https://github.com/yiisoft/yii2/issues/11461 + $this->assertCommandCreatedFile('create_title_with_comma_default_values', 'create_test_table', [ + 'fields' => 'title:string(10):notNull:unique:defaultValue(",te,st"), + body:text:notNull:defaultValue(",test"), + test:custom(11,2,"s"):notNull', + ]); + } + + public function testGenerateDropMigration() + { + $migrationNames = [ + 'drop_test_table', + ]; + foreach ($migrationNames as $migrationName) { + $this->assertCommandCreatedFile('drop_test', $migrationName); + + $this->assertCommandCreatedFile('drop_fields', $migrationName, [ + 'fields' => 'body:text:notNull,price:money(11,2)' + ]); + } + + // @see https://github.com/yiisoft/yii2/issues/10876 + $this->assertCommandCreatedFile('drop_products_from_store_table', 'drop_products_from_store_table'); + } + + public function testGenerateAddColumnMigration() + { + $migrationNames = [ + 'add_columns_column_to_test_table', + 'add_columns_columns_to_test_table', + ]; + foreach ($migrationNames as $migrationName) { + $this->assertCommandCreatedFile('add_columns_test', $migrationName, [ + 'fields' => 'title:string(10):notNull, + body:text:notNull, + price:money(11,2):notNull, + created_at:dateTime' + ]); + + $this->assertCommandCreatedFile('add_columns_fk', $migrationName, [ + 'fields' => 'user_id:integer:foreignKey, + product_id:foreignKey:integer:unsigned:notNull, + order_id:integer:foreignKey(user_order):notNull, + created_at:dateTime:notNull', + ]); + + $this->assertCommandCreatedFile('add_columns_prefix', $migrationName, [ + 'useTablePrefix' => true, + 'fields' => 'user_id:integer:foreignKey, + product_id:foreignKey:integer:unsigned:notNull, + order_id:integer:foreignKey(user_order):notNull, + created_at:dateTime:notNull', + ]); + } + } + + public function testGenerateDropColumnMigration() + { + $migrationNames = [ + 'drop_columns_column_from_test_table', + 'drop_columns_columns_from_test_table', + ]; + foreach ($migrationNames as $migrationName) { + $this->assertCommandCreatedFile('drop_columns_test', $migrationName, [ + 'fields' => 'title:string(10):notNull,body:text:notNull, + price:money(11,2):notNull, + created_at:dateTime' + ]); + } + } + + public function testGenerateCreateJunctionMigration() + { + $migrationNames = [ + 'create_junction_post_and_tag_tables', + 'create_junction_for_post_and_tag_tables', + 'create_junction_table_for_post_and_tag_tables', + 'create_junction_table_for_post_and_tag_table', + ]; + foreach ($migrationNames as $migrationName) { + $this->assertCommandCreatedFile('junction_test', $migrationName); + } + } } \ No newline at end of file diff --git a/tests/framework/console/controllers/MigrateControllerTestTrait.php b/tests/framework/console/controllers/MigrateControllerTestTrait.php index cf61cc624e..fd5f416dce 100644 --- a/tests/framework/console/controllers/MigrateControllerTestTrait.php +++ b/tests/framework/console/controllers/MigrateControllerTestTrait.php @@ -27,10 +27,15 @@ trait MigrateControllerTestTrait * @var string test migration path. */ protected $migrationPath; + /** + * @var string test migration namespace + */ + protected $migrationNamespace; public function setUpMigrationPath() { + $this->migrationNamespace = 'yiiunit\runtime\test_migrations'; $this->migrationPath = Yii::getAlias('@yiiunit/runtime/test_migrations'); FileHelper::createDirectory($this->migrationPath); if (!file_exists($this->migrationPath)) { @@ -43,26 +48,7 @@ trait MigrateControllerTestTrait FileHelper::removeDirectory($this->migrationPath); } - public function assertFileContent($expectedFile, $class) - { - $this->assertEqualsWithoutLE( - include Yii::getAlias( - "@yiiunit/data/console/migrate_create/$expectedFile.php" - ), - $this->parseNameClassMigration($class) - ); - } - - public function assertCommandCreatedFile( - $expectedFile, - $migrationName, - $params = [] - ) { - $class = 'm' . gmdate('ymd_His') . '_' . $migrationName; - $params[0] = $migrationName; - $this->runMigrateControllerAction('create', $params); - $this->assertFileContent($expectedFile, $class); - } + /** * @return array applied migration entries @@ -71,27 +57,29 @@ trait MigrateControllerTestTrait /** * Creates test migrate controller instance. + * @param array $config controller configuration. * @return BaseMigrateController migrate command instance. */ - protected function createMigrateController() + protected function createMigrateController(array $config = []) { $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); $class = $this->migrateControllerClass; $migrateController = new $class('migrate', $module); $migrateController->interactive = false; $migrateController->migrationPath = $this->migrationPath; - return $migrateController; + return Yii::configure($migrateController, $config); } /** * Emulates running of the migrate controller action. - * @param string $actionID id of action to be run. - * @param array $args action arguments. + * @param string $actionID id of action to be run. + * @param array $args action arguments. + * @param array $config controller configuration. * @return string command output. */ - protected function runMigrateControllerAction($actionID, array $args = []) + protected function runMigrateControllerAction($actionID, array $args = [], array $config = []) { - $controller = $this->createMigrateController(); + $controller = $this->createMigrateController($config); ob_start(); ob_implicit_flush(false); $controller->run($actionID, $args); @@ -130,6 +118,40 @@ CODE; return $class; } + /** + * @param string $name + * @param string|null $date + * @return string generated class name + */ + protected function createNamespaceMigration($name, $date = null) + { + if ($date === null) { + $date = gmdate('ymdHis'); + } + $class = 'M' . $date . ucfirst($name); + $baseClass = $this->migrationBaseClass; + $namespace = $this->migrationNamespace; + + $code = <<migrationPath . DIRECTORY_SEPARATOR . $class . '.php', $code); + return $class; + } + /** * Change class name migration to $class * @param string $class name class @@ -159,7 +181,7 @@ CODE; $appliedMigrations = $migrationHistory; foreach ($expectedMigrations as $expectedMigrationName) { $appliedMigration = array_shift($appliedMigrations); - if (strpos($appliedMigration['version'], $expectedMigrationName) === false) { + if (!fnmatch(strtr($expectedMigrationName, ['\\' => DIRECTORY_SEPARATOR]), strtr($appliedMigration['version'], ['\\' => DIRECTORY_SEPARATOR]))) { $success = false; break; } @@ -188,139 +210,7 @@ CODE; $this->assertContains($migrationName, basename($files[0]), 'Wrong migration name!'); } - public function testGenerateDefaultMigration() - { - $this->assertCommandCreatedFile('default', 'DefaultTest'); - } - - public function testGenerateCreateMigration() - { - $migrationNames = [ - 'create_test_table', - ]; - foreach ($migrationNames as $migrationName) { - $this->assertCommandCreatedFile('create_test', $migrationName); - - $this->assertCommandCreatedFile('create_fields', $migrationName, [ - 'fields' => 'title:string(10):notNull:unique:defaultValue("test"), - body:text:notNull, - price:money(11,2):notNull, - parenthesis_in_comment:string(255):notNull:comment(\'Name of set (RU)\')' - ]); - - $this->assertCommandCreatedFile('create_title_pk', $migrationName, [ - 'fields' => 'title:primaryKey,body:text:notNull,price:money(11,2)', - ]); - - $this->assertCommandCreatedFile('create_id_pk', $migrationName, [ - 'fields' => 'id:primaryKey, - address:string, - address2:string, - email:string', - ]); - - $this->assertCommandCreatedFile('create_foreign_key', $migrationName, [ - 'fields' => 'user_id:integer:foreignKey, - product_id:foreignKey:integer:unsigned:notNull, - order_id:integer:foreignKey(user_order):notNull, - created_at:dateTime:notNull', - ]); - - $this->assertCommandCreatedFile('create_prefix', $migrationName, [ - 'useTablePrefix' => true, - 'fields' => 'user_id:integer:foreignKey, - product_id:foreignKey:integer:unsigned:notNull, - order_id:integer:foreignKey(user_order):notNull, - created_at:dateTime:notNull', - ]); - } - - // @see https://github.com/yiisoft/yii2/issues/10876 - $this->assertCommandCreatedFile('create_products_from_store_table', 'create_products_from_store_table'); - - // @see https://github.com/yiisoft/yii2/issues/11461 - $this->assertCommandCreatedFile('create_title_with_comma_default_values', 'create_test_table', [ - 'fields' => 'title:string(10):notNull:unique:defaultValue(",te,st"), - body:text:notNull:defaultValue(",test"), - test:custom(11,2,"s"):notNull', - ]); - } - - public function testGenerateDropMigration() - { - $migrationNames = [ - 'drop_test_table', - ]; - foreach ($migrationNames as $migrationName) { - $this->assertCommandCreatedFile('drop_test', $migrationName); - - $this->assertCommandCreatedFile('drop_fields', $migrationName, [ - 'fields' => 'body:text:notNull,price:money(11,2)' - ]); - } - - // @see https://github.com/yiisoft/yii2/issues/10876 - $this->assertCommandCreatedFile('drop_products_from_store_table', 'drop_products_from_store_table'); - } - - public function testGenerateAddColumnMigration() - { - $migrationNames = [ - 'add_columns_column_to_test_table', - 'add_columns_columns_to_test_table', - ]; - foreach ($migrationNames as $migrationName) { - $this->assertCommandCreatedFile('add_columns_test', $migrationName, [ - 'fields' => 'title:string(10):notNull, - body:text:notNull, - price:money(11,2):notNull, - created_at:dateTime' - ]); - - $this->assertCommandCreatedFile('add_columns_fk', $migrationName, [ - 'fields' => 'user_id:integer:foreignKey, - product_id:foreignKey:integer:unsigned:notNull, - order_id:integer:foreignKey(user_order):notNull, - created_at:dateTime:notNull', - ]); - - $this->assertCommandCreatedFile('add_columns_prefix', $migrationName, [ - 'useTablePrefix' => true, - 'fields' => 'user_id:integer:foreignKey, - product_id:foreignKey:integer:unsigned:notNull, - order_id:integer:foreignKey(user_order):notNull, - created_at:dateTime:notNull', - ]); - } - } - - public function testGenerateDropColumnMigration() - { - $migrationNames = [ - 'drop_columns_column_from_test_table', - 'drop_columns_columns_from_test_table', - ]; - foreach ($migrationNames as $migrationName) { - $this->assertCommandCreatedFile('drop_columns_test', $migrationName, [ - 'fields' => 'title:string(10):notNull,body:text:notNull, - price:money(11,2):notNull, - created_at:dateTime' - ]); - } - } - - public function testGenerateCreateJunctionMigration() - { - $migrationNames = [ - 'create_junction_post_and_tag_tables', - 'create_junction_for_post_and_tag_tables', - 'create_junction_table_for_post_and_tag_tables', - 'create_junction_table_for_post_and_tag_table', - ]; - foreach ($migrationNames as $migrationName) { - $this->assertCommandCreatedFile('junction_test', $migrationName); - } - } + public function testUp() { @@ -329,7 +219,7 @@ CODE; $this->runMigrateControllerAction('up'); - $this->assertMigrationHistory(['base', 'test1', 'test2']); + $this->assertMigrationHistory(['m*_base', 'm*_test1', 'm*_test2']); } /** @@ -342,7 +232,7 @@ CODE; $this->runMigrateControllerAction('up', [1]); - $this->assertMigrationHistory(['base', 'test1']); + $this->assertMigrationHistory(['m*_base', 'm*_test1']); } /** @@ -356,7 +246,7 @@ CODE; $this->runMigrateControllerAction('up'); $this->runMigrateControllerAction('down', [1]); - $this->assertMigrationHistory(['base', 'test1']); + $this->assertMigrationHistory(['m*_base', 'm*_test1']); } /** @@ -370,7 +260,7 @@ CODE; $this->runMigrateControllerAction('up'); $this->runMigrateControllerAction('down', ['all']); - $this->assertMigrationHistory(['base']); + $this->assertMigrationHistory(['m*_base']); } /** @@ -413,7 +303,17 @@ CODE; $this->runMigrateControllerAction('mark', [$version]); - $this->assertMigrationHistory(['base', 'test1']); + $this->assertMigrationHistory(['m*_base', 'm*_test1']); + } + + public function testTo() + { + $version = '020202_000001'; + $this->createMigration('to1', $version); + + $this->runMigrateControllerAction('to', [$version]); + + $this->assertMigrationHistory(['m*_base', 'm*_to1']); } /** @@ -426,6 +326,148 @@ CODE; $this->runMigrateControllerAction('redo'); - $this->assertMigrationHistory(['base', 'test1']); + $this->assertMigrationHistory(['m*_base', 'm*_test1']); + } + + // namespace : + + /** + * @depends testCreate + */ + public function testNamespaceCreate() + { + // default namespace apply : + $migrationName = 'testDefaultNamespace'; + $this->runMigrateControllerAction('create', [$migrationName], [ + 'migrationPath' => null, + 'migrationNamespaces' => [$this->migrationNamespace] + ]); + $files = FileHelper::findFiles($this->migrationPath); + $fileContent = file_get_contents($files[0]); + $this->assertContains("namespace {$this->migrationNamespace};", $fileContent); + $this->assertRegExp('/class M[0-9]{12}' . ucfirst($migrationName) . '/s', $fileContent); + unlink($files[0]); + + // namespace specify : + $migrationName = 'test_namespace_specify'; + $this->runMigrateControllerAction('create', [$this->migrationNamespace . '\\' . $migrationName], [ + 'migrationPath' => $this->migrationPath, + 'migrationNamespaces' => [$this->migrationNamespace] + ]); + $files = FileHelper::findFiles($this->migrationPath); + $fileContent = file_get_contents($files[0]); + $this->assertContains("namespace {$this->migrationNamespace};", $fileContent); + unlink($files[0]); + + // no namespace: + $migrationName = 'test_no_namespace'; + $this->runMigrateControllerAction('create', [$migrationName], [ + 'migrationPath' => $this->migrationPath, + 'migrationNamespaces' => [$this->migrationNamespace] + ]); + $files = FileHelper::findFiles($this->migrationPath); + $fileContent = file_get_contents($files[0]); + $this->assertNotContains("namespace {$this->migrationNamespace};", $fileContent); + } + + /** + * @depends testUp + */ + public function testNamespaceUp() + { + $this->createNamespaceMigration('nsTest1'); + $this->createNamespaceMigration('nsTest2'); + + $this->runMigrateControllerAction('up', [], [ + 'migrationPath' => null, + 'migrationNamespaces' => [$this->migrationNamespace] + ]); + + $this->assertMigrationHistory([ + 'm*_*_base', + $this->migrationNamespace . '\\M*NsTest1', + $this->migrationNamespace . '\\M*NsTest2', + ]); + } + + /** + * @depends testNamespaceUp + * @depends testDownCount + */ + public function testNamespaceDownCount() + { + $this->createNamespaceMigration('down1'); + $this->createNamespaceMigration('down2'); + + $controllerConfig = [ + 'migrationPath' => null, + 'migrationNamespaces' => [$this->migrationNamespace] + ]; + $this->runMigrateControllerAction('up', [], $controllerConfig); + $this->runMigrateControllerAction('down', [1], $controllerConfig); + + $this->assertMigrationHistory([ + 'm*_*_base', + $this->migrationNamespace . '\\M*Down1', + ]); + } + + /** + * @depends testNamespaceUp + * @depends testHistory + */ + public function testNamespaceHistory() + { + $controllerConfig = [ + 'migrationPath' => null, + 'migrationNamespaces' => [$this->migrationNamespace] + ]; + + $output = $this->runMigrateControllerAction('history', [], $controllerConfig); + $this->assertContains('No migration', $output); + + $this->createNamespaceMigration('history1'); + $this->createNamespaceMigration('history2'); + $this->runMigrateControllerAction('up', [], $controllerConfig); + + $output = $this->runMigrateControllerAction('history', [], $controllerConfig); + $this->assertRegExp('/' . preg_quote($this->migrationNamespace) . '.*History1/s', $output); + $this->assertRegExp('/' . preg_quote($this->migrationNamespace) . '.*History2/s', $output); + } + + /** + * @depends testMark + */ + public function testNamespaceMark() + { + $controllerConfig = [ + 'migrationPath' => null, + 'migrationNamespaces' => [$this->migrationNamespace] + ]; + + $version = '010101000001'; + $this->createNamespaceMigration('mark1', $version); + + $this->runMigrateControllerAction('mark', [$this->migrationNamespace . '\\M' . $version], $controllerConfig); + + $this->assertMigrationHistory(['m*_base', $this->migrationNamespace . '\\M*Mark1']); + } + + /** + * @depends testTo + */ + public function testNamespaceTo() + { + $controllerConfig = [ + 'migrationPath' => null, + 'migrationNamespaces' => [$this->migrationNamespace] + ]; + + $version = '020202000020'; + $this->createNamespaceMigration('to1', $version); + + $this->runMigrateControllerAction('to', [$this->migrationNamespace . '\\M' . $version], $controllerConfig); + + $this->assertMigrationHistory(['m*_base', $this->migrationNamespace . '\\M*To1']); } } \ No newline at end of file