protected/modules/yupe/components/Migrator.php

Summary

Maintainability
D
2 days
Test Coverage
<?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;
    }
}