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

@ -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);
}
/**