Миграции баз данных — интеграция с вашим приложением

    Данная статья посвящена практическому использованию библиотеки Migraton, появившейся в обновлении CodeIgniter версии 2.1.0. Настоятельно рекомендую вам перед ознакомлением с данным материалом прочесть первую часть статьи, в которой говорится непосредственно о создании миграций.


    Постановка задачи и ее решение


    Для начала договоримся, что приложение должно будет обновляться через интерфейс админки, только по логину, а до этого, соответственно, на запросы на любую из страниц будет отдавать 503 ответ.
    Промежуточные версии, как и откаты нам пока не нужны, так что сайт будет обновляться сразу до последней версии.


    Добавляем оповещение для посетителей

    В проекте, для которого я делал интеграцию миграций, все контроллеры также наследуют не базовый CI_Controller, а расширенный MY_Controller, который содержит в своем конструкторе некие служебные действия, в том числе и с базой данных. Поэтому для файла %site_path%/application/core/MY_Controller.php нам будет необходимо добавить несколько строчек:
    <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
    class MY_Controller extends CI_Controller {
    	protected $db_update = FALSE; // it's TRUE if site's database  needs update, FALSE otherwise
    
    	public function __construct() {
    		parent::__construct();
    		$is_admin_page = is_a($this, 'Admin');
    		$this->db_update = $this->migration->get_fs_version()!=$this->migration->get_db_version();
    		if ($this->db_update) {
    			if (!$is_admin_page)
    				show_error('Пожалуйста, попробуйте перезайти через несколько минут', 503, 'Простите, на сайте в настоящее время проходят технические работы');
    			else return TRUE;
    		}
    		### Your super overpowered code goes here..
    	}
    }
    

    Примечание: как и в прошлой статье, все файлы с исходным кодом приведены в конце статьи

    Я думаю, что код в целом понятен, стоит лишь заметить, что ошибку мы будем показывать везде, кроме страницы админки (контроллер Admin, который унаследует MY_Controller). Но для админки не будем выполнять конструктор дальше, т.к. он может содержать в себе обращения к базе данных, которая имеет устаревшую структуру. Кстати, как вы видите, сначала в конструкторе вызывается код родительского конструктора, что реализует паттерн Decorator.

    Создаем скрипт обновления через админку

    В моем случае вся админка находилась в одном контроллере Admin, так что и мы для простоты примера возьмем такой же случай.
    Этот контроллер админки содержит несколько разноплановых методов, которые позволяют управлять содержимым сайта. Не очень хорошее и элегантное решение, если честно — все же настоятельно советую разносить это по разным контроллерам или даже использовать HMVC-плагин для CodeIgniter (на хабре уже был неплохой пример его использования).

    Итак, у нас есть задача сделать так, чтобы можно было авторизироваться в админке, и на любой из страниц выводилось предложение обновить базу данных. Для этого отлично подойдет волшебный метод _remap, который будет вызываться при обращении к любому из методов контроллера, и принимать решение, передать управление запрашиваемому методу или показать кукиш сделать какое-либо другое действие:
    	public function _remap($method, $params = array()) {
    		if (!$this->m_user->authorised() && $method != 'index') { 
    			header('Location:/admin/'); //If user isn't autorised, redirect him to the login form
    		}
    		if (!$this->db_update || (!$this->m_user->authorised() && $method== 'index') || $method=='logout' || $method=='update_db') {
    			return call_user_func_array(array($this, $method), $params); // Calls requested method if it is ok to do so
    		}
    		else {
    			$this->data['body'] = '<h1>Внимание!</h1>
    				Необходимо <a href="/admin/update_db">обновить базу данных</a>'; // Show update database link
    			$this->load->view('admin/default.phtml', $this->data);
    		}
    	}
    

    Тут стоит пояснить, что m_user — модель, которая содержит в себе все методы для работы с пользователями админки (предлагаю вам реализовать самостоятельно), метод index умеет показывать форму входа для незалогиненных пользователей, ну а logout понятно что делает.
    Кроме того, создадим там же метод для обновления базы:
    	public function update_db() {
    		$this->data['body'] = '<h1>Обновление базы данных успешно завершено</h1>';
    		if ( ! $this->migration->current()) {
    			show_error($this->migration->error_string());
    		}	
    		$this->load->view('admin/default.phtml', $this->data);
    	}
    


    Правим конфиг и тестируем

    Наконец, осталось поправить конфиг, и мы сможем воспользоваться всем тем, что написали для нашего проекта. Для этого добавим в автозагрузку класс Migration. Теперь строка с авто инициализацией библиотек в моем случае выглядит следующим образом (файл %site_path%/application/config/autoload.php):
    	$autoload['libraries'] = array('database', 'session', 'migration');
    

    Еще нам необходимо проверить в файле %site_path%/application/config/migration.php, ключены ли миграции, посмотреть правильно ли указаны к ним путь и версия базы данных, требуемой для корректной работы нашего кода (все точно так же, как и в прошлой статье):
    	$config['migration_enabled'] = TRUE;
    	$config['migration_version'] = 1;
    	$config['migration_path'] = APPPATH . 'migrations/';
    

    Обратите внимание, что я указал 1-ю версию, что подразумевает, что мы уже сделали функционал рассылок, миграцию для которого мы писали в предыдущем туториале.

    Теперь вы можете перейти на любую страницу вашего сайта, не относящуюся к админке и должны будете увидеть сообщение об ошибке, т.к. версия базы данных у нас сейчас нулевая (при первой инициализации библиотека Migration создала в вашей базе табличку migrations и указала там версию 0), а версия кода в конфиге указана первая. Перейдя же в админку и пройдя авторизацию, вы увидите предложение обновить базу данных, и нажав на ссылку получите свежее приложение, без необходимости выполнять все запросы вручную!

    Бонус — скрипт обновления через CLI


    В качестве бонуса, рассмотрим создание скрипта для обновления через cli, который позволит нам автоматизировать миграции при выгрузке их на сайт, например из систем контроля версий с помощью хуков.
    К счастью, CodeIgniter в своих последних версиях имеет возможность запускаться из командной строки, что называется из коробки. Поэтому для начала создадим контроллер %site_path%/application/controllers/cli.php с private переменной $args, которая будет содержать все аргументы, с которыми был вызван скрипт, кроме названия контроллера и его метода (отсечем их в конструкторе):
    <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
    class Cli extends MY_Controller {
    	private $args = array(); // Contains CLI arguments, except controller name and its method
    	
    	public function __construct() {
            parent::__construct();
    		if(!$this->input->is_cli_request()) {
    			show_404();
    		}
    		$this->output->enable_profiler(FALSE);
    		$this->args = array_slice($_SERVER['argv'], 3);
    
    	}
    	// Other code goes here..
    }
    

    Кстати говоря, советую еще обратить внимание на место в конструкторе, где проверяется, пришел ли запрос через CLI. Мы же не хотим чтобы через сайт можно было выполнить служебные методы! Как видите, я еще и профайлер отключил тут, т.к. на тестовых серверах он у меня по умолчанию везде включен, а в CLI он явно был бы лишним.

    Наконец, напишем метод для миграций:
    public function migration() {
    	if ( !is_array($this->args) || empty($this->args)) {
    		print ( "Usage: php index.php cli migration [OPTIONS]\n\n" );
    		print ( "Options are:\n" );
    		print ( "-l, --last\t\tupdate database to the latest version\n" );
    		print ( "-c, --current\t\t show current versions of database and code\n" );
    		exit;
    	}
    	for ( $i=0; $i<count($this->args); $i++ ) {
    		$arg = $this->args[$i];
    		if ( $arg=="-l" || $arg=="--last" ) {
    			print "Updating your database to the latest version..\n";
    			if (!$this->migration->current()) {
    				print $this->migration->error_string().'\n';
    				exit;
    			}
    			else print "Update complete!\n";
    		}
    		elseif ( $arg=="-c" || $arg=="--current" ) {
    			print 'Current code version is:\t'. $this->migration->get_fs_version().'\n';
    			print 'Current database version is:\t'.$this->migration->get_db_version().'\n';
    		}
    	}
    }
    

    Для его вызова нужно всего лишь набрать в командной строке "php /%index_dir%/index.php cli migration" без дополнительных атрибутов, и скрипт вам любезно подскажет доступные для использования опции. Ну а если набрать "php /%index_dir%/index.php cli migration -l", то метод попытается обновить вашу бд, и выдаст вам результат.
    Конечно, этот кусок кода всего-лишь пример, который выполняет лишь сами основы, но общее представление о использовании CLI и миграций он даёт, и вам не составит труда добавить, например, опцию '-r ' для апдейта базы до указанной ревизии (что кстати будет простеньким домашним заданием для вас).


    Заключение


    Теперь у нас есть хоть и простой, но отлаженный механизм обновления базы данных, интегрированный в наш проект, который кроме всего прочего умеет предупреждать пользователей о текущем обновлении. Согласитесь, это лучше, чем если бы они увидели баги, связанные с нестыковкой версий базы и кода, или еще какое-либо непотребство. Кроме того, мы теперь можем добавить в нашу любимую систему контроля версий хук, который будет при выгрузке новой версии сайта автоматически обновлять базу.

    Скачать архив с полными исходниками

    Первая часть: Миграции баз данных — обзор библиотеки и ее использование
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      0
      Спасибо. Следующий шаг — генерилка админки, и тогда уж точно от RoR не отличить :)
        +3
        $is_admin_page = (strtolower(get_class($this))=='admin')? TRUE: FALSE;


        Это что такое? :-)

        Почему не:
        $is_admin_page = strtolower(get_class($this))=='admin'
          0
          Мда, действительно не порядок. Сейчас поправлю.
          Тяжело одним программистом работать в компании, никто на такие косяки нелепые не укажет, если сам мимо пропустишь…
            +2
            $is_admin_page = $this instanceof Admin;
            логичнее (и семантичнее) тут была бы по-моему
            или
            $is_admin_page = is_a($this, 'Admin');
            если есть вероятность, что у админки могут быть и наследники.
              0
              Хотя instanceof и наследников обработает нормально.
                0
                Ой, еще один косяк, спасибо!
                Исправил на вариант с is_a()

            Only users with full accounts can post comments. Log in, please.