mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-14 06:11:35 +08:00
Add migration namespace support (#12511)
namespace support added to `BaseMigrateController`
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user