Writing a CLI module for Zend Framework 2

Welcome!
I recently started working with Zend Framework 2, and there is a need to write a cli module working with the database migrations.
In this article I will describe how to create a module for Zend 2 to work with it from the command line, for example, module migrations, how to write tests, how to publish a module in packagist.org
What is migration: a database is a system of classes describing the actions on the database and allows you to perform these actions.
Installation of the framework
Let's start with the installation of the framework, the frame we will take ZendSkeletonApplication
Clone the ZendSkeletonApplication, it is an application skeleton.
cd projects_dir/
git clone git://github.com/zendframework/ZendSkeletonApplication.git
//rename SampleZendModule
mv ZendSkeletonApplication SampleZendModule
//set itself zendframework via composer
php composer.phar self-update
php composer.phar install
Read more about basic installation and quick start can be found here
framework.zend.com/manual/2.0/en/index.html User Guide section
General description
Console problem with Zend 2 written by technology like web MVC MVC using a similar system of routing, only slightly different in connection with specific console parameters.
The router determines which team you need to call and calls the appropriate controller, passing it all the data.
Tellingly, for web and console uses the same controllers, the differences are probably only in Zend\Console\Request instead of Zend\Http\Request and Zend\Console\Response instead of Zend\Http\Response, object request and response, respectively.
The point of interaction with the console commands is a single point of entry the same as that responsible for web interaction, i.e. usually /project/public/index.php
creating a skeleton module
Due to the fact that in Zend 2 still no console tools to generate code, create a module have hands.
Create the following directory structure from the root of the project
/project/
--/module/ shared folder with the modules by default there Application app which you must
----/knyzev/ — name of the group of modules, or developers in General can not specify but if you post on packagist.org he wants the composite name of the form group/package
------/zend-db-migrations/ is the directory of the module
--------/config/ — folder for configuration
--------/src/ main folder with classes
----------/ZendDbMigrations/ directory corresponding to the namespace
------------/Controller/ controllers
------------/Library/ — library migration
------------Module.php class provides General information about the module
------------README.md — description of module
------------composer.a json description of the module and dependencies to be able to publish it on packagist.org
Zend 2 app is built in modules, each of which can define controllers, services, etc.
Configuration
Let's start with the config folder, it should create the file module.config.php contains config, I did it here is the contents of the file.
the
<?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=]',
'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',
),
),
);
In this config controllers and view_manager describe where your templates are stored and which controllers will be called, as I understand it, this reduction apparently can be accessed and directly, these settings are standard for all modules.
Migrations are settings for my module specifies the storage directory migrations, in my case it is the root directory of the project specified namespace in all the classes and migrations show_log defines output logs to the console.
Console is a console configuring routing in Zend 2 determination of the parameters of the console takes place through a system of routing similar to the one used in the web part
Read more about console routing can be found here
framework.zend.com/manual/2.0/en/modules/zend.console.routes.html
About conventional http routing here
framework.zend.com/manual/2.0/en/modules/zend.mvc.routing.html
So, create ranting. In this case, we need three of roat
1. db_migrations_version — displays info about the current version of the database
2. db_migrations_migrate [] [--env=] — executes or rolls back database migration
3. db_migrations_generate — generates a stub for a database
Description parameters routes:
the
'db_migrations_migrate' => array(
'type' => 'simple',
'options' => array(
'route' => 'db_migrations_migrate [<version>] [--env=]',
'defaults' => array(
'controller' => 'ZendDbMigrations\Controller\Migrate',
'action' => 'migrate'
)
)
),
type — the type of the route
options/route — name console command with parameters and options parameter is optional if it is followed by a detailed description at the link above.
options/defaults/controller — the controller handles the route
options/defaults/action — the action in the controller
Controller
the
<?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
*/
ZendDbMigrations namespace\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;
/**
* The controller provides the commands migration
*/
class MigrateController extends AbstractActionController
{
/**
* Create a class object migrations
* @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);
}
/**
* Get the current version migration
* @return integer
*/
public function versionAction(){
$migration = $this->getMigration();
return sprintf("Current version: %s\n", $migration- > getCurrentVersion());
}
/**
* Migrate
*/
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";
}
}
/**
* To generate skeleton class for new migration
*/
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);
}
Here is an example of a typical controller action (Action) to bind the routing route has the name [name]Action, Action required, and name the command name.
Getting request parameters is done using the classes Zend/Console/Request, through the inherited base class controller
$this->getRequest()->getParam('version') — so we got the version parameter of the roat db_migrations_migrate []
All that is returned from methods in the form of plain text as in this example, will be wrapped in ViewModel and displayed directly to the console.
For asynchronous output to the console as the app runs, you need to use Zend/Console/Response which is available via the service locator $this->getServiceLocator()->get('console') Supports methods write, writeAt, writeLine. A detailed description and parameters can be found in the documentation.
Module.php
the
<?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 the current migration version'
'db_migrations_migrate [<version>]' => 'Execute migrate',
'db_migrations_generate' => 'Generate new migration class'
);
}
}
File Module.php provides some information about the module, all files Module.php automatically loaded at each startup to download the configuration files and other data.
In this case, the class Module will look like this.
It should be noted that in order to call a console script without any parameters, it displays the list of all available commands to add to the module interface support ConsoleUsageProviderInterface and its implementation, which display an array of commands with the description as in the example above.
So for example if you run the command
php public/index.php
displays all command which returns the method getConsoleUsage our module.
Create PHPUnit tests
The tests in MVC Zend 2 as a rule are placed in the tests folder in the root of the project and fully comply with the structure module.
For example,
/project/
-/module/
--/knyzev/
---/zend-db-migrations/
----/src/
-----/ZendDbMigrations/
------/Controller/
-------/MigrateController.php
- a/tests/
--/knyzev/
---/zend-db-migrations/
----/src/
-----/ZendDbMigrations/
------/Controller/
-------/MigrateControllerTest.php
And here is an example test class MigrateController
the
<?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 inherits from phpunit_framework_testcase;
use \Bootstrap;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Metadata\Metadata;
/**
* Testing of the controller MigrateController
*/
MigrateControllerTest extends class inherits from 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;
/**
* Settings
*/
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 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);
}
/**
* Test method that returns the version number
*/
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!');
//add version information
$this->connection->execute('INSERT INTO migration_version (version) VALUES (12345678910)');
//check
$result = $this->controller->dispatch($this->request);
$response = $this->controller->getResponse();
$this->assertEquals("Current version: 12345678910\n", $result->getVariable('result'), 'Returt value is correctly!');
}
/**
* Test run the migration if the migration classes no
*/
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 executable migations not exists!');
}
/**
* Test run the migration if there are migration
*/
public function testMigrationIfExistsMigrations(){
//test run of migration and new migration
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!');
//check that migration is really done
$this->assertTrue(in_array('test_migrations', $this->metadata->getTableNames()), 'real Migration executed!');
//test start of the migration and it is the current version
$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!');
}
/**
* Test run of the migration by specifying version
*/
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 execucte not ok!');
}
/**
* Generate test stubs for migrations
*/
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 generated real class!');
}
}
Read more about the structure of the tests can be read here.
There's a caveat, in Zend 2 is not supported to work with environment, so you need to come up with their own bike to work with the test database.
Composer.json and add the module to packagist.org
Now we describe the module to the composer json and publish it.
Create in the root of the module file composer.json with the following information
the
{
"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 — the name of the module, it will match the folder name of the module.
require — dependencies
Else you can copy and describe the image.
Next, register an account on github.com, choose a public repository, enter the name of the species MyZendModule
On your local computer and initiate a git repository and put it all on github
git init
git remote add origin github.com/knyzev/zend-db-migrations
git add -A
git commit-m "Init commit"
git push
On packagist.org registered, select submit package and add a link to github, it will automatically check the correctness of the composer.json and will report problems if any.
Everything is now in a new project or anyone else to be able to basically file composer.json
just add the dependency, for example knyzev/zend-db-migrations
execute commands
php composer.phar self-update
php composer.phar update
And the module is automatically installed, will only prescribe it in config/application.config.php
Comparison of Symfony 2 + Doctrine 2 and Zend framework 2
I really like Symfony 2 and Doctrine 2-th version, with annotation, full support for the console (console commands for all cases) and a fairly easy Declaration of services, the ORM system Doctrine, zend looks pretty bleak and not comfortable, well, it's a personally subjective opinion, though perhaps sometimes works faster and consumes less memory. The impression is formed mainly due to incompleteness in the direction of the quick start, i.e. all need to be configured and to finish it yourself.
After some work with Symfony, I began to think about the possibility of switching to Java Spring + Hibernate.
The module of the migrations described in this article can be seen here
github.com/vadim-knyzev/ZendDbMigrations
Tests not included in the module, because by the standards of the typical structure of a module zend 2, tests are placed in a separate folder.
PS: somebody knows how to add module to the page of information about the modules on the site. modules.zendframework.com?
framework.zend.com/manual/2.0/en/index.html
github.com/vadim-knyzev/ZendDbMigrations
vadim-knyzev.blogspot.com
Комментарии
Отправить комментарий