protected/modules/yupe/components/Migrator.php
<?php
/**
* Migrator class file.
*
* @category YupeComponent
* @package yupe.modules.yupe.components
* @author Alexander Tischenko <tsm@glavset.ru>
* @license BSD https://raw.github.com/yupe/yupe/master/LICENSE
* @version 0.5.3
* @link http://www.yupe.ru
*/
namespace yupe\components;
use Yii;
use CDbCacheDependency;
use ErrorException;
use CException;
use CDbConnection;
use TagsCache;
use CHtml;
/**
* Class Migrator
* @package yupe\components
*/
class Migrator extends \CApplicationComponent
{
/**
* @var string
*/
public $connectionID = 'db';
/**
* @var string
*/
public $migrationTable = 'migrations';
/**
* @var CDbConnection
*/
private $_db;
/**
* Инициализируем класс:
*
* @return parent:init()
**/
public function init()
{
// check for table
$db = $this->getDbConnection();
if ($db->schema->getTable($db->tablePrefix.$this->migrationTable) === null) {
$this->createMigrationHistoryTable();
}
return parent::init();
}
/**
* Обновление до актуальной миграции:
*
* @param string $module - required module
*
* @return bool if migration updated
**/
public function updateToLatest($module)
{
if (($newMigrations = $this->getNewMigrations($module)) !== []) {
if (Yii::app()->hasComponent('cache')) {
Yii::app()->getComponent('cache')->flush();
}
Yii::log(
Yii::t(
'YupeModule.yupe',
'Updating DB of {module} to latest version',
['{module}' => $module]
)
);
foreach ($newMigrations as $migration) {
if ($this->migrateUp($module, $migration) === false) {
return false;
}
}
if (Yii::app()->hasComponent('cache')) {
Yii::app()->getComponent('cache')->flush();
}
} else {
Yii::log(
Yii::t(
'YupeModule.yupe',
'There is no new migrations for {module}',
['{module}' => $module]
)
);
}
return true;
}
/**
* Проверяем на незавершённые миграции:
*
* @param string $module - required module
* @param bool $class - migration class
*
* @return bool is updated to migration
**/
public function checkForBadMigration($module, $class = false)
{
echo Yii::t('YupeModule.yupe', "Checking for pending migrations").'<br />';
$db = $this->getDbConnection();
$data = $db->cache(
3600,
new CDbCacheDependency('select count(id) from {{migrations}}')
)->createCommand()
->selectDistinct('version, apply_time')
->from('{{migrations}}')
->order('id DESC')
->where(
'module = :module',
[
':module' => $module,
]
)
->queryAll();
if (($data !== []) || ((strpos($class, '_base') !== false) && ($data[] = [
'version' => $class,
'apply_time' => 0,
]))
) {
foreach ($data as $migration) {
if ($migration['apply_time'] == 0) {
try {
echo Yii::t(
'YupeModule.yupe',
'Downgrade {migration} for {module}.',
[
'{module}' => $module,
'{migration}' => $migration['version'],
]
).'<br />';
Yii::log(
Yii::t(
'YupeModule.yupe',
'Downgrade {migration} for {module}.',
[
'{module}' => $module,
'{migration}' => $migration['version'],
]
)
);
if ($this->migrateDown($module, $migration['version']) !== false) {
$db->createCommand()->delete(
$db->tablePrefix.$this->migrationTable,
[
$db->quoteColumnName('version')."=".$db->quoteValue($migration['version']),
$db->quoteColumnName('module')."=".$db->quoteValue($module),
]
);
} else {
Yii::log(
Yii::t(
'YupeModule.yupe',
'Can\'t downgrade migrations {migration} for {module}.',
[
'{module}' => $module,
'{migration}' => $migration['version'],
]
)
);
echo Yii::t(
'YupeModule.yupe',
'Can\'t downgrade migrations {migration} for {module}.',
[
'{module}' => $module,
'{migration}' => $migration['version'],
]
).'<br />';
return false;
}
} catch (ErrorException $e) {
Yii::log(
Yii::t(
'YupeModule.yupe',
'There is an error: {error}',
[
'{error}' => $e,
]
)
);
echo Yii::t(
'YupeModule.yupe',
'There is an error: {error}',
[
'{error}' => $e,
]
);
}
}
}
} else {
Yii::log(
Yii::t(
'YupeModule.yupe',
'No need to downgrade migrations for {module}',
['{module}' => $module]
)
);
echo Yii::t(
'YupeModule.yupe',
'No need to downgrade migrations for {module}',
['{module}' => $module]
).'<br />';
}
return true;
}
/**
* Обновляем миграцию:
*
* @param string $module - required module
* @param string $class - name of migration class
*
* @return bool is updated to migration
**/
protected function migrateUp($module, $class)
{
$db = $this->getDbConnection();
ob_start();
ob_implicit_flush(false);
echo Yii::t('YupeModule.yupe', "Checking migration {class}", ['{class}' => $class]);
Yii::app()->getCache()->clear('getMigrationHistory');
$start = microtime(true);
$migration = $this->instantiateMigration($module, $class);
// Вставляем запись о начале миграции
$db->createCommand()->insert(
$db->tablePrefix.$this->migrationTable,
[
'version' => $class,
'module' => $module,
'apply_time' => 0,
]
);
$result = $migration->up();
Yii::log($msg = ob_get_clean());
if ($result !== false) {
// Проставляем "установлено"
$db->createCommand()->update(
$db->tablePrefix.$this->migrationTable,
['apply_time' => time()],
"version = :ver AND module = :mod",
[':ver' => $class, 'mod' => $module]
);
$time = microtime(true) - $start;
Yii::log(
Yii::t(
'YupeModule.yupe',
"Migration {class} applied for {s} seconds.",
['{class}' => $class, '{s}' => sprintf("%.3f", $time)]
)
);
} else {
$time = microtime(true) - $start;
Yii::log(
Yii::t(
'YupeModule.yupe',
"Error when running {class} ({s} seconds.)",
['{class}' => $class, '{s}' => sprintf("%.3f", $time)]
)
);
throw new CException(
Yii::t(
'YupeModule.yupe',
'Error was found when installing: {error}',
[
'{error}' => $msg,
]
)
);
}
}
/**
* Даунгрейд миграции:
*
* @param string $module - required module
* @param string $class - name of migration class
*
* @return bool is downgraded from migration
**/
public function migrateDown($module, $class)
{
Yii::log(Yii::t('YupeModule.yupe', "Downgrade migration {class}", ['{class}' => $class]));
$db = $this->getDbConnection();
$start = microtime(true);
$migration = $this->instantiateMigration($module, $class);
ob_start();
ob_implicit_flush(false);
$result = $migration->down();
Yii::log($msg = ob_get_clean());
Yii::app()->getCache()->clear('getMigrationHistory');
if ($result !== false) {
$db->createCommand()->delete(
$db->tablePrefix.$this->migrationTable,
[
'AND',
$db->quoteColumnName('version')."=".$db->quoteValue($class),
[
'AND',
$db->quoteColumnName('module')."=".$db->quoteValue($module),
],
]
);
$time = microtime(true) - $start;
Yii::log(
Yii::t(
'YupeModule.yupe',
"Migration {class} downgrated for {s} seconds.",
['{class}' => $class, '{s}' => sprintf("%.3f", $time)]
)
);
return true;
} else {
$time = microtime(true) - $start;
Yii::log(
Yii::t(
'YupeModule.yupe',
"Error when downgrading {class} ({s} сек.)",
['{class}' => $class, '{s}' => sprintf("%.3f", $time)]
)
);
throw new CException(
Yii::t(
'YupeModule.yupe',
'Error was found when installing: {error}',
[
'{error}' => $msg,
]
)
);
}
}
/**
* Check each modules for new migrations
*
* @param string $module - required module
* @param string $class - class of migration
*
* @return mixed version and apply time
*/
protected function instantiateMigration($module, $class)
{
$file = Yii::getPathOfAlias("application.modules.".$module.".install.migrations").'/'.$class.'.php';
include_once $file;
$migration = new $class();
$migration->setDbConnection($this->getDbConnection());
return $migration;
}
/**
* Connect to DB
*
* @return db connection or make exception
*/
protected function getDbConnection()
{
if ($this->_db !== null) {
return $this->_db;
} else {
if (($this->_db = Yii::app()->getComponent($this->connectionID)) instanceof CDbConnection) {
return $this->_db;
}
}
throw new CException(
Yii::t('YupeModule.yupe', 'Parameter connectionID is wrong')
);
}
/**
* Check each modules for new migrations
*
* @param string $module - required module
* @param integer $limit - limit of array
*
* @return mixed version and apply time
*/
public function getMigrationHistory($module, $limit = 20)
{
$db = $this->getDbConnection();
$allData = Yii::app()->getCache()->get('getMigrationHistory');
if ($allData === false || !isset($allData[$module])) {
Yii::app()->getCache()->clear('getMigrationHistory');
$data = $db->cache(
3600,
new CDbCacheDependency('select count(id) from {{migrations}}')
)->createCommand()
->select('version, apply_time')
->from('{{migrations}}')
->order('id DESC')
->where('module = :module', [':module' => $module])
->limit($limit)
->queryAll();
$allData[$module] = $data;
Yii::app()->getCache()->set(
'getMigrationHistory',
$allData,
3600,
new TagsCache('yupe', 'installedModules', 'getModulesDisabled', 'getMigrationHistory', $module)
);
} else {
$data = $allData[$module];
}
return CHtml::listData($data, 'version', 'apply_time');
}
/**
* Create migration history table
*
* @return nothing
*/
protected function createMigrationHistoryTable()
{
$db = $this->getDbConnection();
Yii::log(
Yii::t(
'YupeModule.yupe',
'Creating table for store migration versions {table}',
['{table}' => $this->migrationTable]
)
);
$options = Yii::app()->getDb()->schema instanceof \CMysqlSchema ? 'ENGINE=InnoDB DEFAULT CHARSET=utf8' : '';
$db->createCommand()->createTable(
$db->tablePrefix.$this->migrationTable,
[
'id' => 'pk',
'module' => 'string NOT NULL',
'version' => 'string NOT NULL',
'apply_time' => 'integer',
],
$options
);
$db->createCommand()->createIndex(
"idx_migrations_module",
$db->tablePrefix.$this->migrationTable,
"module",
false
);
}
/**
* Check for new migrations for module
*
* @param string $module - required module
*
* @return mixed new migrations
*/
protected function getNewMigrations($module)
{
$applied = [];
foreach ($this->getMigrationHistory($module, -1) as $version => $time) {
if ($time) {
$applied[substr($version, 1, 13)] = true;
}
}
$migrations = [];
if (($migrationsPath = Yii::getPathOfAlias("application.modules.".$module.".install.migrations")) && is_dir(
$migrationsPath
)
) {
$handle = opendir($migrationsPath);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $migrationsPath.'/'.$file;
if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file(
$path
) && !isset($applied[$matches[2]])
) {
$migrations[] = $matches[1];
}
}
closedir($handle);
sort($migrations);
}
return $migrations;
}
/**
* Check each modules for new migrations
*
* @param array $modules - list of modules
*
* @return mixed new migrations
*/
public function checkForUpdates(array $modules)
{
$updates = [];
foreach ($modules as $mid => $module) {
if ($a = $this->getNewMigrations($mid)) {
$updates[$mid] = $a;
}
}
return $updates;
}
/**
* Return db-installed modules list
*
* @return mixed db-installed
**/
public function getModulesWithDBInstalled()
{
$db = $this->getDbConnection();
$modules = [];
$m = $db->cache(
3600,
new CDbCacheDependency('select count(id) from {{migrations}}')
)->createCommand()
->select('module')
->from('{{migrations}}')
->order('module DESC')
->group('module')
->queryAll();
foreach ($m as $i) {
$modules[] = $i['module'];
}
return $modules;
}
/**
* Return installed modules id
*
* @return array
*/
public function getInstalledModulesList()
{
$modules = [];
foreach (Yii::app()->getModules() as $id => $config) {
$modules[] = $id;
}
return $modules;
}
}