Saturday, September 25, 2010

Running Zend Framework From CLI

My aim was to crate an easy and simple way of running ZF MVC from command line. Solution should be able to run with any existent application, based on ZF version 1.8 or higher.

You should follow the standard zend framework directory placement structure to create your project or use any existent project that follows this structure in order to easy apply all direction from this post.


Create a script that will be an entry point for CLI

$ touch ./scripts/zf-cli.php

Here is the code from that script. Read the comments to understand how it works.

// should be removed starting from PHP version >= 5.3.0
defined('__DIR__') || define('__DIR__', dirname(__FILE__));

// initialize the application path, library and autoloading
defined('APPLICATION_PATH') ||
 define('APPLICATION_PATH', realpath(__DIR__ . '/../application'));

// NOTE: if you already have "library" directory available in your include path
// you don't need to modify the include_path right here
// so in that case you can leave last 4 lines commented
// to avoid receiving error message:
// Fatal error: Cannot redeclare class Zend_Loader in ....
// NOTE: anyway you can uncomment last 4 lines of this comments block
// to manually set the include path directory
// $paths = explode(PATH_SEPARATOR, get_include_path());
// $paths[] = realpath(__DIR__.'/../library'); 
// set_include_path(implode(PATH_SEPARATOR, $paths));
// unset($paths);

require_once 'Zend/Loader/Autoloader.php';
$loader = Zend_Loader_Autoloader::getInstance();

// we need this custom namespace to load our custom class
$loader->registerNamespace('Custom_');

// define application options and read params from CLI
$getopt = new Zend_Console_Getopt(array(
    'action|a=s' => 'action to perform in format of "module/controller/action/param1/param2/param3/.."',
    'env|e-s'    => 'defines application environment (defaults to "production")',
    'help|h'     => 'displays usage information',
));

try {
    $getopt->parse();
} catch (Zend_Console_Getopt_Exception $e) {
    // Bad options passed: report usage
    echo $e->getUsageMessage();
    return false;
}

// show help message in case it was requested or params were incorrect (module, controller and action)
if ($getopt->getOption('h') || !$getopt->getOption('a')) {
    echo $getopt->getUsageMessage();
    return true;
}

// initialize values based on presence or absence of CLI options
$env      = $getopt->getOption('e');
defined('APPLICATION_ENV')
 || define('APPLICATION_ENV', (null === $env) ? 'production' : $env);

// initialize Zend_Application
$application = new Zend_Application (
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);

// bootstrap and retrive the frontController resource
$front = $application->getBootstrap()
      ->bootstrap('frontController')
      ->getResource('frontController');

// magic starts from this line!
//
// we will use Zend_Controller_Request_Simple and some kind of custom code
// to emulate missed in Zend Framework ecosystem
// "Zend_Controller_Request_Cli" that can be found as proposal here:
// http://framework.zend.com/wiki/display/ZFPROP/Zend_Controller_Request_Cli
//
// I like the idea to define request params separated by slash "/"
// for ex. "module/controller/action/param1/param2/param3/.."
//
// NOTE: according to the current implementation param1,param2,param3,... are omited
//    only module/controller/action are used
//
// TODO: allow to omit "module", "action" params
//      and set them to "default" and "index" accordantly
//
// so lets split the params we've received from the CLI
// and pass them to the reqquest object
// NOTE: I think this functionality should be moved to the routing
$params = array_reverse(explode('/', $getopt->getOption('a')));
$module = array_pop($params);
$controller = array_pop($params);
$action = array_pop($params);
$request = new Zend_Controller_Request_Simple ($action, $controller, $module);

// set front controller options to make everything operational from CLI
$front->setRequest($request)
   ->setResponse(new Zend_Controller_Response_Cli())
   ->setRouter(new Custom_Controller_Router_Cli())
   ->throwExceptions(true);

// lets bootstrap our application and enjoy!
$application->bootstrap()
   ->run();

Hope you've noticed custom router used there. If you'll start this script without setting that dummy-router, the default router will be used by FrontController and it will try to process your request in a regular way, by trying to get the URI from request object. That will end up with the error message "PHP Fatal error: Call to undefined method Zend_Controller_Request_Simple::getRequestUri()".

As a workaround we need to have a custom router that will simply "do nothing" on routing.

$ mkdir -p ./library/Custom/Controller/Router
$ touch ./library/Custom/Controller/Router/Cli.php

In order to create one we extending Zend_Controller_Router_Abstract and implementing Zend_Controller_Router_Interface with empty-methods.

/**
 * This is a dummy-router that shouldn't do anything on routing
 */
class Custom_Controller_Router_Cli extends Zend_Controller_Router_Abstract implements Zend_Controller_Router_Interface {
 public function route(Zend_Controller_Request_Abstract $dispatcher){}
    public function assemble($userParams, $name = null, $reset = false, $encode = true){}
    public function getFrontController(){}
    public function setFrontController(Zend_Controller_Front $controller){}
    public function setParam($name, $value){}
    public function setParams(array $params){}
    public function getParam($name){}
    public function getParams(){}
    public function clearParams($name = null){}
    public function addRoute() {}
    public function setGlobalParam() {}
    public function addConfig(){}
    // TODO: possibly some additional methods should be added
}

Starting from this point you can run your application from CLI. Here is some simple usage example:

$ php ./scripts/zf-cli.php -a default/index/index -e development

Please give your feedback in comments.

Conclusion


Dealing with request and routing from CLI in Zend framework is always tricky. I've found a bunch of different solutions, but most of them are produced by hackers and can be treated as successful tricks. We still doesn't have an official approach of running MVC from CLI that will fully satisfy Zend Framework ideology. Please refer to the thoughts from the source [4] at the end of this article in order to fully understand the issue. It seams that quite promising component Zend_Controller_Request_Cli can move situation from this stuck point, but there is no solution for unified routing of CLI request parameters. As always volunteers are welcomed to make everything happen in open source.

Resources


I made some research to highlight all the interesting information available over the internet about this topic, so everyone recommended to read these articles:
  1. Using Zend Framework from the Command Line - article posted in 2008, pretty old and even the author in the comments indicates that some points described in this article are slightly outdated, I was inspired by the article to write this blog-post;
  2. Programmer's Reference Guide - Zend_Console_Getopt - beautiful component that makes CLI developer's life easier;
  3. Zend_Controller_Request_Cli Component Proposal - the proposal promising to resolve the issue with CLI for Zend Framework, but development is stuck a years ago and there is no any hope it will be started in near future. I think volunteer are welcomed to move this on.
  4. Zend Framework Quick Start - the latest available quick start article that shows us an example of CLI usage of the latest ZF version;
  5. The Mysteries Of Asynchronous Processing With PHP - Part 2: Making Zend Framework Applications CLI Accessible - most interesting article as it provides a robus solution for running ZF from the CLI;
    Also you can read a book "Zend Framework 1.8 Web Application Development" to become familiar with Zend_Application and other related staff.


    UPD: Added the notice about including library directory twice.