📜 ⬆️ ⬇️

Write CLI module for Zend Framework 2

image
Greetings

Recently I started working with Zend Framework 2, and I needed to write a cli module that works with database migrations.

In this article I will describe how to create a module for Zend 2 to work with it from the command line using the example of the migration module, how to write tests, how to publish a module in packagist.org
')
What are migrations: Database migrations are a class system that describes actions on a database and allows you to perform these actions.

Installing the framework


Let's start with the installation of the framework, take ZendSkeletonApplication as the framework

We clone ZendSkeletonApplication, it is an application skeleton.
cd projects_dir /
git clone git: //github.com/zendframework/ZendSkeletonApplication.git
// rename to SampleZendModule
mv ZendSkeletonApplication SampleZendModule
// install zendframework itself through the composer
php composer.phar self-update
php composer.phar install

Read more about the basic installation and quick start here.
framework.zend.com/manual/2.0/en/index.html in the User Guide

general description


Console tasks with Zend 2 are written using MVC technology similarly to web MVC, using a similar routing system, which is only slightly different due to the specifics of console parameters.

The router determines which command to call and calls the required controller, passing all the data to it.

Which is typical, for the web and the console one and the same controllers are used, the differences are probably only in the use of Zend \ Console \ Request instead of Zend \ Http \ Request and Zend \ Console \ Response instead of Zend \ Http \ Response, the request and response object, respectively.

The point of interaction with console commands is a single entry point, the same as responsible for web interaction, i.e. usually it is /project/public/index.php

Creating a module framework


Since Zend 2 still doesn’t have console utilities for generating code, you’ll have to create a module by hand.

Create the following directory structure from the project root
/ project /
- / module / - shared folder with modules, by default there is an Application which must be required
---- / knyzev / - the name of the group of modules or the developer, in general, you can not specify, but if you publish on packagist.org, then he wants a composite name of the type group / package
------ / zend-db-migrations / - this is the module directory itself
-------- / config / - folder for configs
-------- / src / - main folder with classes
---------- / ZendDbMigrations / - directory corresponding to the namespace
------------ / Controller / - controllers
------------ / Library / - library for work migrations
------------ Module.php - class that provides general information about the module
------------README.md - module description
------------ composer.json - description of the module and dependencies so that it can be published on packagist.org

In Zend 2, an application is built as modules, each of which can define controllers, services, etc.

Configuration

Let's start with the config folder, here you need to create a file module.config.php containing the config, I got this file contents.

<?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', ), ), ); 


In this configuration, controllers and view_manager describe where the templates are stored and which controllers will be called, as I understood this abbreviation, apparently you can turn directly, these parameters are standard for all modules.

Migrations are the settings of my module that set the migration storage directory, in my case it is the project root directory, the namespace specified in the migration classes and show_log determining the output of logs to the console.

Console is the configuration of console routing, in Zend 2, the definition of console parameters occurs through a routing system similar to that used in the web part

More information about the work of console routing can be found here.
framework.zend.com/manual/2.0/en/modules/zend.console.routes.html

About the usual http routing here
framework.zend.com/manual/2.0/en/modules/zend.mvc.routing.html

So, we create routes. In this case, we need three routes.
1. db_migrations_version - displays info about the current database version
2. db_migrations_migrate [] [--env =] - performs or rolls back the database migration
3. db_migrations_generate - generates a stub for the database

Description of the parameters of the route:
 'db_migrations_migrate' => array( 'type' => 'simple', 'options' => array( 'route' => 'db_migrations_migrate [<version>] [--env=]', 'defaults' => array( 'controller' => 'ZendDbMigrations\Controller\Migrate', 'action' => 'migrate' ) ) ), 


type - type of route
options / route - the name of the console command with parameters and options, if the parameter is optional, it is enclosed in square brackets, a detailed description of the link above.
options / defaults / controller - controller processing route
options / defaults / action - action in the controller

Controller

 <?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); } } 


Here is an example of a typical controller, the Action (Action) to which the routing route is attached has a name of the form [name] Action, Action is an obligatory part, and name is a command name.

The request parameters are retrieved through the Zend / Console / Request classes, through the inherited base controller class
$ this-> getRequest () -> getParam ('version') - so we got the version parameter from the route db_migrations_migrate []

Anything returned from plain text methods like in this example will be wrapped in the ViewModel and output directly to the console.

For asynchronous output to the console as the application runs, you need to use Zend / Console / Response which is available through the service locator $ this-> getServiceLocator () -> get ('console'). Supports the write, writeAt, writeLine methods. Detailed description and parameters can be found in the documentation.

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' ); } } 


The Module.php file provides some information about the module; all Module.php files are automatically loaded each time it is launched to load configuration files and other data.

In this case, the Module class will look like this.

It is worth noting that in order to call a console script without parameters, it lists all existing commands, you need to add support for the ConsoleUsageProviderInterface interface and its implementation, which is the output of an array of commands with the description as in the example above.

So for example when running a command
php public / index.php
all the commands returned by the getConsoleUsage method of our module will be displayed.

Creating PHPUnit tests

Tests in MVC Zend 2 are usually placed in the tests folder in the project root and fully correspond to the module structure.

for example
/ project /
- / module /
- / knyzev /
--- / zend-db-migrations /
---- / src /
----- / ZendDbMigrations /
------ / Controller /
------- / MigrateController.php
- / tests /
- / knyzev /
--- / zend-db-migrations /
---- / src /
----- / ZendDbMigrations /
------ / Controller /
------- / MigrateControllerTest.php

And I will give an example of tests for the class 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!'); } } 


Read more about the structure of the tests can be read here.
framework.zend.com/manual/2.0/en/user-guide/unit-testing.html

There is a nuance here, in Zend 2, work with environments is not supported, so you need to invent your bike to work with the test base.

Composer.json and adding a module to packagist.org


Now we have to describe the module in the json composer and publish it.
Create the file composer.json in the module root with the following information
 { "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 also match the name of the module folder.
require - dependencies
The rest can be copied and described in a similar way.

Next, register an account on github.com , choose a public repository, enter the name of the form MyZendModule
On the local computer, initiate the git repository, and send everything to the github
git init
git remote add origin github.com/knyzev/zend-db-migrations
git add -A
git commit -m "init commit"
git push

On the website packagist.org we register, select the submit package and add a link to github, it will automatically check the correctness of composer.json and report problems if any.

Everything, now in a new project or someone else can in the main file composer.json
just add a dependency, for example knyzev / zend-db-migrations
execute commands
php composer.phar self-update
php composer.phar update
And the module will be automatically installed, it remains only to register it in config / application.config.php

Comparing Symfony 2 + Doctrine 2 and Zend 2


I really like Symfony 2 and Doctrine 2 nd version and after working with annotations, full console support (console commands for all cases) and quite convenient announcement of services, Doctrine ORM system, zend looks rather gloomy and not comfortable, well, this is a personal subjective opinion , although it is possible and works in places faster and consumes less memory. This impression is formed mainly due to the unfinishedness towards a quick start, i.e. all you need to configure and finish yourself.
After working with Symfony for a bit, I started thinking about the possibility of switching to Java Spring + Hibernate.

The migration module itself described in this article can be found here.
github.com/vadim-knyzev/ZendDbMigrations
Tests are not included in the module, since According to the standards of the typical structure of the module zend 2, tests are placed in a separate folder.

PS: Does anyone know how to add a module to the module information page on the modules.zendframework.com site?

framework.zend.com/manual/2.0/en/index.html
github.com/vadim-knyzev/ZendDbMigrations
vadim-knyzev.blogspot.com

Source: https://habr.com/ru/post/159155/


All Articles