Приветствую, хабражители!
Это моя первая статья на хабре, но помойму я все правильно написал.
Недавно начал работать с Zend Framework 2, и возникла потребность написать cli модуль работающий с миграциями базы данных.
В этой статье я опишу как создать модуль для Zend 2 для работы с ним из командной строки на примере модуля миграций, как написать тесты, как опубликовать модуль в packagist.org
Что такое миграции: Миграции базы данных — это система классов описывающая действия над базой данных и позволяющая выполнять эти действия.
Установка фрэймворка
Начнем с установки фрэймворка, в качестве каркаса возьмем ZendSkeletonApplication
Клонируем ZendSkeletonApplication, это скелет приложения.
cd projects_dir/
git clone git://github.com/zendframework/ZendSkeletonApplication.git
//переименуем в SampleZendModule
mv ZendSkeletonApplication SampleZendModule
//устанавливаем сам zendframework через композер
php composer.phar self-update
php composer.phar install
Подробнее о базовой установке и быстрый старт можно прочитать здесь
framework.zend.com/manual/2.0/en/index.html в разделе User Guide
Общее описание
Консольные задачи с Zend 2 пишутся по технологии MVC аналогично веб MVC, с использованием аналогичной системы роутинга, лишь немного отличающейся в связи со спецификой консольных параметров.
Роутер определяет какую команду нужно вызывать и вызывает нужный контроллер, передавая ему все данные.
Что характерно, для веб и консоли используются одни и теже контроллеры, различия пожалуй составляют только в использовании Zend\Console\Request вместо Zend\Http\Request и Zend\Console\Response вместо Zend\Http\Response, объект запроса и ответа соответственно.
Точкой взаимодействия с консольными командами является единая точка входа, та же что и отвечает за веб взаимодействие, т.е. обычно это /project/public/index.php
Создание каркаса модуля
Ввиду того что в Zend 2 все ещё нету консольных утилит для генерации кода, то создавать модуль придется руками.
Создаем следующую структуру каталогов от корня проекта
/project/
—/module/ — общая папка с модулями, по умолчанию там Application приложение которое должно быть обязательно
—-/knyzev/ — название группы модулей или разработчика, вообще можно и не указывать но если публикуешь на packagist.org, то он хочет составное название вида group/package
——/zend-db-migrations/ — это сам каталог модуля
———/config/ — папка для конфигов
———/src/ — основная папка с классами
———-/ZendDbMigrations/ — каталог соответствующий пространству имен
————/Controller/ — контроллеры
————/Library/ — библиотека для работы миграций
————Module.php — класс предоставляющий общую информацию о модуле
————README.md — описание модуля
————composer.json — описание модуля и зависимостей чтобы можно было опубликовать его на packagist.org
В Zend 2 приложение строится в виде модулей, каждый из которых может определять контроллеры, сервисы и т.д.
Конфигурация
Начнем с папки config, здесь нужно создать файл module.config.php содержащий конфиг, у меня получилось вот такое содержимое файла.
<?php return array( 'migrations' => array( 'dir' => dirname(__FILE__) . '/../../../../migrations', 'namespace' => 'ZendDbMigrations\Migrations', 'show_log' => true ), 'console' => array( 'router' => array( 'routes' => array( 'db_migrations_version' => array( 'type' => 'simple', 'options' => array( 'route' => 'db_migrations_version [--env=]', 'defaults' => array( 'controller' => 'ZendDbMigrations\Controller\Migrate', 'action' => 'version' ) ) ), 'db_migrations_migrate' => array( 'type' => 'simple', 'options' => array( 'route' => 'db_migrations_migrate [<version>] [--env=]', 'defaults' => array( 'controller' => 'ZendDbMigrations\Controller\Migrate', 'action' => 'migrate' ) ) ), 'db_migrations_generate' => array( 'type' => 'simple', 'options' => array( 'route' => 'db_migrations_generate [--env=]', 'defaults' => array( 'controller' => 'ZendDbMigrations\Controller\Migrate', 'action' => 'generateMigrationClass' ) ) ) ) ) ), 'controllers' => array( 'invokables' => array( 'ZendDbMigrations\Controller\Migrate' => 'ZendDbMigrations\Controller\MigrateController' ), ), 'view_manager' => array( 'template_path_stack' => array( __DIR__ . '/../view', ), ), );
В этом конфиге controllers и view_manager описывают где хранятся шаблоны и какие контроллеры будут вызываться, как я понял это сокращение, видимо можно обратится и напрямую, эти параметры являются стандартными для всех модулей.
Migrations — это настройки моего модуля задающие каталог хранения миграций, в моем случае это корневая директория проекта, namespace указанный в классах миграций и show_log определяющий вывод логов на консоль.
Console — это конфигурирование консольного роутинга, в Zend 2 определение параметров консоли происходит через систему роутинга аналогичную используемой в веб части
Подробнее о работе консольного роутинга можно прочитать тут
framework.zend.com/manual/2.0/en/modules/zend.console.routes.html
Про обычный http роутинг здесь
framework.zend.com/manual/2.0/en/modules/zend.mvc.routing.html
Итак, создаем роуты. В данном случае нам понадобится три роута
1. db_migrations_version — выводит инфу о текущей версии базы данных
2. db_migrations_migrate [] [—env=] — выполняет либо откатывает миграции базы данных
3. db_migrations_generate — генерирует заглушку для базы данных
Описание параметров роута:
'db_migrations_migrate' => array( 'type' => 'simple', 'options' => array( 'route' => 'db_migrations_migrate [<version>] [--env=]', 'defaults' => array( 'controller' => 'ZendDbMigrations\Controller\Migrate', 'action' => 'migrate' ) ) ),
type — тип маршрута,
options/route — название консольной команды с параметрами и опциями, если параметр необязательный он берется в квадратные скобки, подробное описание по ссылке выше.
options/defaults/controller — контроллер обрабатывающий маршрут
options/defaults/action — действие в контроллере
Контроллер
<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/ZendSkeletonApplication for the canonical source repository * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace ZendDbMigrations\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; use Zend\Console\Request as ConsoleRequest; use ZendDbMigrations\Library\Migration; use ZendDbMigrations\Library\MigrationException; use ZendDbMigrations\Library\GeneratorMigrationClass; use ZendDbMigrations\Library\OutputWriter; /** * Контроллер обеспечивает вызов команд миграций */ class MigrateController extends AbstractActionController { /** * Создать объект класса миграций * @return \Migrations\Library\Migration */ protected function getMigration(){ $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter'); $config = $this->getServiceLocator()->get('Configuration'); $console = $this->getServiceLocator()->get('console'); $output = null; if($config['migrations']['show_log']) { $output = new OutputWriter(function($message) use($console) { $console->write($message . "\n"); }); } return new Migration($adapter, $config['migrations']['dir'], $config['migrations']['namespace'], $output); } /** * Получить текущую версию миграции * @return integer */ public function versionAction(){ $migration = $this->getMigration(); return sprintf("Current version %s\n", $migration->getCurrentVersion()); } /** * Мигрировать */ public function migrateAction(){ $migration = $this->getMigration(); $version = $this->getRequest()->getParam('version'); if(is_null($version) && $migration->getCurrentVersion() >= $migration->getMaxMigrationNumber($migration->getMigrationClasses())) return "No migrations to execute.\n"; try{ $migration->migrate($version); return "Migrations executed!\n"; } catch (MigrationException $e) { return "ZendDbMigrations\Library\MigrationException\n" . $e->getMessage() . "\n"; } } /** * Сгенерировать каркасный класс для новой миграции */ public function generateMigrationClassAction(){ $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter'); $config = $this->getServiceLocator()->get('Configuration'); $generator = new GeneratorMigrationClass($config['migrations']['dir'], $config['migrations']['namespace']); $className = $generator->generate(); return sprintf("Generated class %s\n", $className); } }
Вот пример типичного контроллера, действие (Action), к которому привязывается маршрут роутинга имеет название вида [name]Action, Action — обязательная часть, а name название команды.
Получение параметров запроса производится через классы Zend/Console/Request, через наследуемый базовый класс контроллера
$this->getRequest()->getParam(‘version’) — так мы получили параметр version из роута db_migrations_migrate []
Все что возвращается из методов в виде plain text как в этом примере, будет обернуто в ViewModel и выведено прямо в консоль.
Для асинхронного вывода в консоль по мере работы приложения, нужно использовать Zend/Console/Response который доступен через сервис локатор $this->getServiceLocator()->get(‘console’), Поддерживает методы write, writeAt, writeLine. Подробное описание и параметры можно посмотреть в документации.
Module.php
<?php namespace ZendDbMigrations; use Zend\Mvc\ModuleRouteListener; use Zend\ModuleManager\Feature\AutoloaderProviderInterface; use Zend\ModuleManager\Feature\ConfigProviderInterface; use Zend\ModuleManager\Feature\ConsoleUsageProviderInterface; use Zend\Console\Adapter\AdapterInterface as Console; use Zend\ModuleManager\Feature\ConsoleBannerProviderInterface; class Module implements AutoloaderProviderInterface, ConfigProviderInterface, ConsoleUsageProviderInterface, ConsoleBannerProviderInterface { public function onBootstrap($e) { $e->getApplication()->getServiceManager()->get('translator'); $eventManager = $e->getApplication()->getEventManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); } public function getConfig() { return include __DIR__ . '/config/module.config.php'; } public function getAutoloaderConfig() { return array( 'Zend\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, ), ), ); } public function getConsoleBanner(Console $console){ return 'DB Migrations Module'; } public function getConsoleUsage(Console $console){ //description command return array( 'db_migrations_version' => 'Get current migration version', 'db_migrations_migrate [<version>]' => 'Execute migrate', 'db_migrations_generate' => 'Generate new migration class' ); } }
Файл Module.php предоставляет некоторую информацию о модуле, все файлы Module.php автоматически загружаются при каждом запуске с целью загрузки файлов конфигураций и других данных.
В данном случае класс Module будет выглядеть вот таким образом.
Стоит особо отметить, что для того, чтобы при вызове консольного скрипта без параметров он выводил список всех существующих команд, нужно добавить в модуль поддержку интерфейса ConsoleUsageProviderInterface и его реализацию, который вывод массива команд с описанием как в примере выше.
Так например при запуске команды
php public/index.php
будут выведены все команды которые возвращает метод getConsoleUsage нашего модуля.
Создание тестов PHPUnit
Тесты в MVC Zend 2 как правило размещаются в папке tests в корне проекта и полностью соответствуют структуре модуля.
Например
/project/
-/module/
—/knyzev/
—/zend-db-migrations/
—-/src/
——/ZendDbMigrations/
——/Controller/
——-/MigrateController.php
-/tests/
—/knyzev/
—/zend-db-migrations/
—-/src/
——/ZendDbMigrations/
——/Controller/
——-/MigrateControllerTest.php
И приведу пример тестов на класс MigrateController
<?php namespace Tests\ZendDbMigrations\Controller; use ZendDbMigrations\Controller\MigrateController; use Zend\Console\Request as ConsoleRequest; use Zend\Console\Response; use Zend\Mvc\MvcEvent; use Zend\Mvc\Router\RouteMatch; use PHPUnit_Framework_TestCase; use \Bootstrap; use Zend\Db\Adapter\Adapter; use Zend\Db\Metadata\Metadata; /** * Тестирование контроллера MigrateController */ class MigrateControllerTest extends PHPUnit_Framework_TestCase { protected $controller; protected $request; protected $response; protected $routeMatch; protected $event; protected $eventManager; protected $serviceManager; protected $dbAdapter; protected $connection; protected $metadata; protected $folderMigrationFixtures; /** * Настройки */ protected function setUp() { $bootstrap = \Zend\Mvc\Application::init(Bootstrap::getAplicationConfiguration()); $this->request = new ConsoleRequest(); $this->routeMatch = new RouteMatch(array('controller' => 'migrate')); $this->event = $bootstrap->getMvcEvent(); $this->event->setRouteMatch($this->routeMatch); $this->eventManager = $bootstrap->getEventManager(); $this->serviceManager = $bootstrap->getServiceManager(); $this->dbAdapter = $bootstrap->getServiceManager()->get('Zend\Db\Adapter\Adapter'); $this->connection = $this->dbAdapter->getDriver()->getConnection(); $this->metadata = new Metadata($this->dbAdapter); $this->folderMigrationFixtures = dirname(__FILE__) . '/../MigrationsFixtures'; $this->initController(); $this->tearDown(); } protected function tearDown(){ $this->dbAdapter->query('DROP TABLE IF EXISTS migration_version CASCADE;', Adapter::QUERY_MODE_EXECUTE); $this->dbAdapter->query('DROP TABLE IF EXISTS test_migrations CASCADE;', Adapter::QUERY_MODE_EXECUTE); $this->dbAdapter->query('DROP TABLE IF EXISTS test_migrations2 CASCADE;', Adapter::QUERY_MODE_EXECUTE); $iterator = new \GlobIterator($this->folderMigrationFixtures . '/tmp/*', \FilesystemIterator::KEY_AS_FILENAME); foreach ($iterator as $item) { if($item->isFile()) { unlink($item->getPath() . '/' . $item->getFilename()); } } chmod($this->folderMigrationFixtures . '/tmp', 0775); } protected function initController(){ $this->controller = new MigrateController(); $this->controller->setEvent($this->event); $this->controller->setEventManager($this->eventManager); $this->controller->setServiceLocator($this->serviceManager); } /** * Тест метода возвращающего номер версии */ public function testVersion() { $this->routeMatch->setParam('action', 'version'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode(), 'Status code is 200 OK!'); $this->assertInstanceOf('Zend\View\Model\ViewModel', $result, 'Method return object Zend\View\Model\ViewModel!'); $this->assertEquals("Current version 0\n", $result->getVariable('result'), 'Returt value is correctly!'); //добавляем информацию о версии $this->connection->execute('INSERT INTO migration_version (version) VALUES (12345678910)'); //проверяем $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals("Current version 12345678910\n", $result->getVariable('result'), 'Returt value is correctly!'); } /** * Тест запуска миграций если классов миграций нету */ public function testMigrateIfNotMigrations() { $this->routeMatch->setParam('action', 'migrate'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode(), 'Status code is 200 OK!'); $this->assertInstanceOf('Zend\View\Model\ViewModel', $result, 'Method return object Zend\View\Model\ViewModel!'); $this->assertEquals("No migrations to execute.\n", $result->getVariable('result'), 'Return correct info if no exists not executable migations!'); } /** * Тест запуска миграций если есть миграция */ public function testMigrationIfExistsMigrations(){ //тестируем запуск миграции при наличии новой миграции copy($this->folderMigrationFixtures . '/MigrationsGroup1/Version20121110210200.php', $this->folderMigrationFixtures . '/tmp/Version20121110210200.php'); $this->routeMatch->setParam('action', 'migrate'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode(), 'Status code is 200 OK!'); $this->assertEquals("Migrations executed!\n", $result->getVariable('result'), 'Return correct info if executed migrations!'); //проверяем что миграция действительно выполнена $this->assertTrue(in_array('test_migrations', $this->metadata->getTableNames()), 'Migration real executed!'); //тест запуска выполненной миграции и она является текущей версией $this->initController(); $this->routeMatch->setParam('action', 'migrate'); $this->routeMatch->setParam('version', 20121110210200); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode(), 'Status code is 200 OK!'); $this->assertContains("Migration version 20121110210200 is current version!\n", $result->getVariable('result'), 'Starting the migration with a current version works correctly!'); } /** * Тест запуска миграций с указанием версии */ public function testMigrateWithVersion() { copy($this->folderMigrationFixtures . '/MigrationsGroup2/Version20121111150900.php', $this->folderMigrationFixtures . '/tmp/Version20121111150900.php'); copy($this->folderMigrationFixtures . '/MigrationsGroup2/Version20121111153700.php', $this->folderMigrationFixtures . '/tmp/Version20121111153700.php'); $this->routeMatch->setParam('action', 'migrate'); $this->routeMatch->setParam('version', 20121111150900); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode(), 'Status code is 200 OK!'); $this->assertTrue(in_array('test_migrations', $this->metadata->getTableNames()), 'Migration 20121111150900 execucte ok!'); $this->assertFalse(in_array('test_migrations2', $this->metadata->getTableNames()), 'Migration 20121111153700 not execucte ok!'); } /** * Тест генерации заглушки для миграций */ public function testGenerateMigrationClass() { $this->routeMatch->setParam('action', 'generateMigrationClass'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode(), 'Status code is 200 OK!'); $this->assertInstanceOf('Zend\View\Model\ViewModel', $result, 'Method return object Zend\View\Model\ViewModel!'); $this->assertContains("Generated class ", $result->getVariable('result'), 'Return result info ok!'); $fileName = sprintf('Version%s.php', date('YmdHis', time())); $this->assertFileExists($this->folderMigrationFixtures . '/tmp/' . $fileName, 'Generate command real generated class!'); } }
Подробнее о структуре тестов можно почитать здесь
framework.zend.com/manual/2.0/en/user-guide/unit-testing.html
Тут есть нюанс, в зенд 2 не поддерживается работа с окружениями, поэтому нужно придумывать свой велосипед для работы с тестовой базой.
Composer.json и добавление модуля на packagist.org
Теперь нам осталось описать модуль в композер json и опубликовать его.
Создаем в корне модуля файл composer.json со следующей информацией
{ "name": "knyzev/zend-db-migrations", "description": "Module for managment database migrations.", "type": "library", "license": "BSD-3-Clause", "keywords": [ "database", "db", "migrations", "zf2" ], "homepage": "https://github.com/vadim-knyzev/ZendDbMigrations", "authors": [ { "name": "Vadim Knyzev", "email": "vadim.knyzev@gmail.com", "homepage": "http://vadim-knyzev.blogspot.com/" } ], "require": { "php": ">=5.3.3", "zendframework/zendframework": "2.*" }, "autoload": { "psr-0": { "ZendDbMigrations": "src/" }, "classmap": [ "./Module.php" ] } }
name — название модуля, оно же буде соответствовать названия папки модуля.
require — зависимости
Остальное можно скопировать и описать по подобию.
Далее регистрируем аккаунт на github.com, выбираем публичный репозиторий, вводим имя вида MyZendModule
На локальном компьютере инициируем гит репозиторий, и отправляем все на гитхаб
git init
git remote add origin github.com/knyzev/zend-db-migrations
git add -A
git commit -m «Init commit»
git push
На сайте packagist.org/ регистрируемся, выбираем submit package и добавляем ссылку на github, он автоматически проверит корректность composer.json и сообщит о проблемах если они есть.
Всё, теперь в новом проекте или кто-либо другой сможет в основном файле composer.json
просто добавить зависимость, например knyzev/zend-db-migrations
выполнить команды
php composer.phar self-update
php composer.phar update
И модуль будет автоматически установлен, останется только прописать его в config/application.config.php
Сравнение Symfony 2 + Doctrine 2 и Zend 2
Мне очень нравится Symfony 2 и Doctrine 2-й версии и после работы с аннотациями, полной поддержкой консоли (консольные команды на все случаи) и довольно удобным объявлением сервисов, ORM системой Doctrine, zend выглядит довольно мрачно и не уютно, ну это лично субъективное мнение, хотя возможно и работает местами быстрее и потребляет меньше памяти. Такое впечатление формируется в основном из-за недоделанности в сторону быстрого старта, т.е. все нужно конфигурировать и доделывать самому.
После того как немного поработал с Symfony стал подумывать о возможности перехода на Java Spring + Hibernate.
Сам модуль миграций описанный в этой статье можно посмотреть здесь
github.com/vadim-knyzev/ZendDbMigrations
Тесты не включены в модуль, т.к. по стандартам типовой структуры модуля zend 2, тесты размещаются в отдельной папке.
PS: Кто нибудь знает как добавить модуль на страницу информации о модулях на сайте зенда modules.zendframework.com/?
ссылка на оригинал статьи http://habrahabr.ru/post/159155/
Добавить комментарий