Add migration namespace support (#12511)

namespace support added to `BaseMigrateController`
This commit is contained in:
Paul Klimov
2016-09-12 14:01:04 +03:00
committed by GitHub
parent c25296f2e2
commit 8aa0e85a07
11 changed files with 612 additions and 217 deletions

View File

@ -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 #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 #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) - 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 #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 #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) - Enh #12440: Added `yii\base\Event::offAll()` method allowing clear all registered class-level event handlers (klimov-paul)

View File

@ -8,6 +8,7 @@
namespace yii\console\controllers; namespace yii\console\controllers;
use Yii; use Yii;
use yii\base\InvalidConfigException;
use yii\console\Exception; use yii\console\Exception;
use yii\console\Controller; use yii\console\Controller;
use yii\helpers\Console; 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 * @var string the directory storing the migration classes. This can be either
* a path alias or a directory. * 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'; 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. * @var string the template file for generating new migrations.
* This can be either a path alias (e.g. "@app/migrations/template.php") * 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.) * This method is invoked right before an action is to be executed (after all possible filters.)
* It checks the existence of the [[migrationPath]]. * It checks the existence of the [[migrationPath]].
* @param \yii\base\Action $action the action to be executed. * @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. * @return boolean whether the action should continue to be executed.
*/ */
public function beforeAction($action) public function beforeAction($action)
{ {
if (parent::beforeAction($action)) { if (parent::beforeAction($action)) {
$path = Yii::getAlias($this->migrationPath); if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
if (!is_dir($path)) { throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
if ($action->id !== 'create') { }
throw new Exception("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
} foreach ($this->migrationNamespaces as $key => $value) {
FileHelper::createDirectory($path); $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(); $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");
@ -278,10 +311,11 @@ abstract class BaseMigrateController extends Controller
* them again. For example, * them again. For example,
* *
* ``` * ```
* yii migrate/to 101129_185401 # using timestamp * yii migrate/to 101129_185401 # using timestamp
* yii migrate/to m101129_185401_create_user_table # using full name * yii migrate/to m101129_185401_create_user_table # using full name
* yii migrate/to 1392853618 # using UNIX timestamp * yii migrate/to 1392853618 # using UNIX timestamp
* yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string * 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 * @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) public function actionTo($version)
{ {
if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
$this->migrateToVersion('m' . $matches[1]); $this->migrateToVersion($namespaceVersion);
} elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
$this->migrateToVersion($migrationName);
} elseif ((string) (int) $version == $version) { } elseif ((string) (int) $version == $version) {
$this->migrateToTime($version); $this->migrateToTime($version);
} elseif (($time = strtotime($version)) !== false) { } elseif (($time = strtotime($version)) !== false) {
$this->migrateToTime($time); $this->migrateToTime($time);
} else { } 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. * No actual migration will be performed.
* *
* ``` * ```
* yii migrate/mark 101129_185401 # using timestamp * yii migrate/mark 101129_185401 # using timestamp
* yii migrate/mark m101129_185401_create_user_table # using full name * 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. * @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) public function actionMark($version)
{ {
$originalVersion = $version; $originalVersion = $version;
if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
$version = 'm' . $matches[1]; $version = $namespaceVersion;
} elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
$version = $migrationName;
} else { } 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 // try mark up
$migrations = $this->getNewMigrations(); $migrations = $this->getNewMigrations();
foreach ($migrations as $i => $migration) { foreach ($migrations as $i => $migration) {
if (strpos($migration, $version . '_') === 0) { if (strpos($migration, $version) === 0) {
if ($this->confirm("Set migration history at $originalVersion?")) { if ($this->confirm("Set migration history at $originalVersion?")) {
for ($j = 0; $j <= $i; ++$j) { for ($j = 0; $j <= $i; ++$j) {
$this->addMigrationHistory($migrations[$j]); $this->addMigrationHistory($migrations[$j]);
@ -345,7 +384,7 @@ abstract class BaseMigrateController extends Controller
// try mark down // try mark down
$migrations = array_keys($this->getMigrationHistory(null)); $migrations = array_keys($this->getMigrationHistory(null));
foreach ($migrations as $i => $migration) { foreach ($migrations as $i => $migration) {
if (strpos($migration, $version . '_') === 0) { if (strpos($migration, $version) === 0) {
if ($i === 0) { if ($i === 0) {
$this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW); $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
} else { } else {
@ -364,6 +403,34 @@ abstract class BaseMigrateController extends Controller
throw new Exception("Unable to find the version '$originalVersion'."); 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. * Displays the migration history.
* *
@ -465,8 +532,19 @@ abstract class BaseMigrateController extends Controller
* yii migrate/create create_user_table * 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 * @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 * 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, * drop_xxx then the generated migration file will contain extra code,
@ -476,22 +554,75 @@ abstract class BaseMigrateController extends Controller
*/ */
public function actionCreate($name) public function actionCreate($name)
{ {
if (!preg_match('/^\w+$/', $name)) { if (!preg_match('/^[\w\\\\]+$/', $name)) {
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, underscore and/or backslash characters only.');
} }
$className = 'm' . gmdate('ymd_His') . '_' . $name; list($namespace, $className) = $this->generateClassName($name);
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $className . '.php'; $migrationPath = $this->findMigrationPath($namespace);
$file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
if ($this->confirm("Create new migration '$file'?")) { if ($this->confirm("Create new migration '$file'?")) {
$content = $this->generateMigrationSourceCode([ $content = $this->generateMigrationSourceCode([
'name' => $name, 'name' => $name,
'className' => $className, 'className' => $className,
'namespace' => $namespace,
]); ]);
FileHelper::createDirectory($migrationPath);
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);
} }
} }
/**
* 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. * Upgrades with the specified migration class.
* @param string $class the migration class name * @param string $class the migration class name
@ -539,7 +670,6 @@ abstract class BaseMigrateController extends Controller
$time = microtime(true) - $start; $time = microtime(true) - $start;
$this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN); $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
return true; return true;
} else { } else {
$time = microtime(true) - $start; $time = microtime(true) - $start;
@ -556,8 +686,11 @@ abstract class BaseMigrateController extends Controller
*/ */
protected function createMigration($class) protected function createMigration($class)
{ {
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; $class = trim($class, '\\');
require_once($file); if (strpos($class, '\\') === false) {
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
require_once($file);
}
return new $class(); return new $class();
} }
@ -593,7 +726,7 @@ abstract class BaseMigrateController extends Controller
// try migrate up // try migrate up
$migrations = $this->getNewMigrations(); $migrations = $this->getNewMigrations();
foreach ($migrations as $i => $migration) { foreach ($migrations as $i => $migration) {
if (strpos($migration, $version . '_') === 0) { if (strpos($migration, $version) === 0) {
$this->actionUp($i + 1); $this->actionUp($i + 1);
return self::EXIT_CODE_NORMAL; return self::EXIT_CODE_NORMAL;
@ -603,7 +736,7 @@ abstract class BaseMigrateController extends Controller
// try migrate down // try migrate down
$migrations = array_keys($this->getMigrationHistory(null)); $migrations = array_keys($this->getMigrationHistory(null));
foreach ($migrations as $i => $migration) { foreach ($migrations as $i => $migration) {
if (strpos($migration, $version . '_') === 0) { if (strpos($migration, $version) === 0) {
if ($i === 0) { if ($i === 0) {
$this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW); $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
} else { } else {
@ -624,25 +757,45 @@ abstract class BaseMigrateController extends Controller
protected function getNewMigrations() protected function getNewMigrations()
{ {
$applied = []; $applied = [];
foreach ($this->getMigrationHistory(null) as $version => $time) { foreach ($this->getMigrationHistory(null) as $class => $time) {
$applied[substr($version, 1, 13)] = true; $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 = []; $migrations = [];
$handle = opendir($this->migrationPath); foreach ($migrationPaths as $namespace => $migrationPath) {
while (($file = readdir($handle)) !== false) { if (!file_exists($migrationPath)) {
if ($file === '.' || $file === '..') {
continue; continue;
} }
$path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; $handle = opendir($migrationPath);
if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && !isset($applied[$matches[2]]) && is_file($path)) { while (($file = readdir($handle)) !== false) {
$migrations[] = $matches[1]; 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); ksort($migrations);
sort($migrations);
return $migrations; return array_values($migrations);
} }
/** /**

View File

@ -50,6 +50,24 @@ use yii\helpers\Console;
* yii migrate/down * 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
@ -164,8 +182,11 @@ class MigrateController extends BaseMigrateController
*/ */
protected function createMigration($class) protected function createMigration($class)
{ {
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; $class = trim($class, '\\');
require_once($file); if (strpos($class, '\\') === false) {
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
require_once($file);
}
return new $class(['db' => $this->db]); return new $class(['db' => $this->db]);
} }

View File

@ -3,7 +3,8 @@
* This view is used by console/controllers/MigrateController.php * This view is used by console/controllers/MigrateController.php
* The following variables are available in this view: * 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 $table string the name table */
/* @var $fields array the fields */ /* @var $fields array the fields */
@ -11,6 +12,9 @@ preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches);
$columns = $matches[1]; $columns = $matches[1];
echo "<?php\n"; echo "<?php\n";
if (!empty($namespace)) {
echo "\nnamespace {$namespace};\n";
}
?> ?>
use yii\db\Migration; use yii\db\Migration;

View File

@ -5,12 +5,16 @@
* @since 2.0.7 * @since 2.0.7
* @deprecated since 2.0.8 * @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 $table string the name table */
/* @var $field_first string the name field first */ /* @var $field_first string the name field first */
/* @var $field_second string the name field second */ /* @var $field_second string the name field second */
echo "<?php\n"; echo "<?php\n";
if (!empty($namespace)) {
echo "\nnamespace {$namespace};\n";
}
?> ?>
use yii\db\Migration; use yii\db\Migration;

View File

@ -3,12 +3,16 @@
* This view is used by console/controllers/MigrateController.php * This view is used by console/controllers/MigrateController.php
* The following variables are available in this view: * 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 $table string the name table */
/* @var $fields array the fields */ /* @var $fields array the fields */
/* @var $foreignKeys array the foreign keys */ /* @var $foreignKeys array the foreign keys */
echo "<?php\n"; echo "<?php\n";
if (!empty($namespace)) {
echo "\nnamespace {$namespace};\n";
}
?> ?>
use yii\db\Migration; use yii\db\Migration;

View File

@ -3,13 +3,17 @@
* This view is used by console/controllers/MigrateController.php * This view is used by console/controllers/MigrateController.php
* The following variables are available in this view: * 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 $table string the name table */
/* @var $fields array the fields */ /* @var $fields array the fields */
preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches); preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches);
$columns = $matches[1]; $columns = $matches[1];
echo "<?php\n"; echo "<?php\n";
if (!empty($namespace)) {
echo "\nnamespace {$namespace};\n";
}
?> ?>
use yii\db\Migration; use yii\db\Migration;

View File

@ -3,11 +3,15 @@
* This view is used by console/controllers/MigrateController.php * This view is used by console/controllers/MigrateController.php
* The following variables are available in this view: * 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 $table string the name table */
/* @var $fields array the fields */ /* @var $fields array the fields */
echo "<?php\n"; echo "<?php\n";
if (!empty($namespace)) {
echo "\nnamespace {$namespace};\n";
}
?> ?>
use yii\db\Migration; use yii\db\Migration;

View File

@ -3,9 +3,13 @@
* This view is used by console/controllers/MigrateController.php * This view is used by console/controllers/MigrateController.php
* The following variables are available in this view: * 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 "<?php\n"; echo "<?php\n";
if (!empty($namespace)) {
echo "\nnamespace {$namespace};\n";
}
?> ?>
use yii\db\Migration; use yii\db\Migration;

View File

@ -50,4 +50,156 @@ class MigrateControllerTest extends TestCase
$query = new Query(); $query = new Query();
return $query->from('migration')->all(); 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);
}
}
} }

View File

@ -27,10 +27,15 @@ trait MigrateControllerTestTrait
* @var string test migration path. * @var string test migration path.
*/ */
protected $migrationPath; protected $migrationPath;
/**
* @var string test migration namespace
*/
protected $migrationNamespace;
public function setUpMigrationPath() public function setUpMigrationPath()
{ {
$this->migrationNamespace = 'yiiunit\runtime\test_migrations';
$this->migrationPath = Yii::getAlias('@yiiunit/runtime/test_migrations'); $this->migrationPath = Yii::getAlias('@yiiunit/runtime/test_migrations');
FileHelper::createDirectory($this->migrationPath); FileHelper::createDirectory($this->migrationPath);
if (!file_exists($this->migrationPath)) { if (!file_exists($this->migrationPath)) {
@ -43,26 +48,7 @@ trait MigrateControllerTestTrait
FileHelper::removeDirectory($this->migrationPath); 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 * @return array applied migration entries
@ -71,27 +57,29 @@ trait MigrateControllerTestTrait
/** /**
* Creates test migrate controller instance. * Creates test migrate controller instance.
* @param array $config controller configuration.
* @return BaseMigrateController migrate command instance. * @return BaseMigrateController migrate command instance.
*/ */
protected function createMigrateController() protected function createMigrateController(array $config = [])
{ {
$module = $this->getMock('yii\\base\\Module', ['fake'], ['console']); $module = $this->getMock('yii\\base\\Module', ['fake'], ['console']);
$class = $this->migrateControllerClass; $class = $this->migrateControllerClass;
$migrateController = new $class('migrate', $module); $migrateController = new $class('migrate', $module);
$migrateController->interactive = false; $migrateController->interactive = false;
$migrateController->migrationPath = $this->migrationPath; $migrateController->migrationPath = $this->migrationPath;
return $migrateController; return Yii::configure($migrateController, $config);
} }
/** /**
* Emulates running of the migrate controller action. * Emulates running of the migrate controller action.
* @param string $actionID id of action to be run. * @param string $actionID id of action to be run.
* @param array $args action arguments. * @param array $args action arguments.
* @param array $config controller configuration.
* @return string command output. * @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_start();
ob_implicit_flush(false); ob_implicit_flush(false);
$controller->run($actionID, $args); $controller->run($actionID, $args);
@ -130,6 +118,40 @@ CODE;
return $class; 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 = <<<CODE
<?php
namespace {$namespace};
class {$class} extends \\{$baseClass}
{
public function up()
{
}
public function down()
{
}
}
CODE;
file_put_contents($this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php', $code);
return $class;
}
/** /**
* Change class name migration to $class * Change class name migration to $class
* @param string $class name class * @param string $class name class
@ -159,7 +181,7 @@ CODE;
$appliedMigrations = $migrationHistory; $appliedMigrations = $migrationHistory;
foreach ($expectedMigrations as $expectedMigrationName) { foreach ($expectedMigrations as $expectedMigrationName) {
$appliedMigration = array_shift($appliedMigrations); $appliedMigration = array_shift($appliedMigrations);
if (strpos($appliedMigration['version'], $expectedMigrationName) === false) { if (!fnmatch(strtr($expectedMigrationName, ['\\' => DIRECTORY_SEPARATOR]), strtr($appliedMigration['version'], ['\\' => DIRECTORY_SEPARATOR]))) {
$success = false; $success = false;
break; break;
} }
@ -188,139 +210,7 @@ CODE;
$this->assertContains($migrationName, basename($files[0]), 'Wrong migration name!'); $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() public function testUp()
{ {
@ -329,7 +219,7 @@ CODE;
$this->runMigrateControllerAction('up'); $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->runMigrateControllerAction('up', [1]);
$this->assertMigrationHistory(['base', 'test1']); $this->assertMigrationHistory(['m*_base', 'm*_test1']);
} }
/** /**
@ -356,7 +246,7 @@ CODE;
$this->runMigrateControllerAction('up'); $this->runMigrateControllerAction('up');
$this->runMigrateControllerAction('down', [1]); $this->runMigrateControllerAction('down', [1]);
$this->assertMigrationHistory(['base', 'test1']); $this->assertMigrationHistory(['m*_base', 'm*_test1']);
} }
/** /**
@ -370,7 +260,7 @@ CODE;
$this->runMigrateControllerAction('up'); $this->runMigrateControllerAction('up');
$this->runMigrateControllerAction('down', ['all']); $this->runMigrateControllerAction('down', ['all']);
$this->assertMigrationHistory(['base']); $this->assertMigrationHistory(['m*_base']);
} }
/** /**
@ -413,7 +303,17 @@ CODE;
$this->runMigrateControllerAction('mark', [$version]); $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->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']);
} }
} }