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.

    30 comments:

    1. I like your article and the idea to have access to me models and controllers on the command line. I have a problem, though:

      I followed all the steps you described and it tells me the following:

      $ php ./scripts/zfcli.php -a default/index/index -e development

      Fatal error: Cannot redeclare class Zend_Loader in /srv/www/lerngemein.de/library/Zend/Loader.php on line 31


      Any suggestions? I use 1.10 and the setup generally works fine. I can execute "php ./public/index.php" to receive the php code, generated by the layout and index/index view.

      ReplyDelete
    2. sorry for the noise. Zend Framework was twice in my include_path ... my bad =)

      ReplyDelete
    3. Hello, Joseph! Thank you for post.
      A get the same error as luckyduck.

      $ php ./scripts/zfcli.php -a default/index/index -e development

      Fatal error: Cannot redeclare class Zend_Loader in /srv/www/lerngemein.de/library/Zend/Loader.php on line 31

      What shell i do?

      ReplyDelete
    4. Hi, sadhak

      check your include_path, possibly you've included Zend Framework twice.

      I'll update the article right now to have a slight notice about this common issue.

      Also please refer to the resources section and discover the last link [5] for more elegant solution.

      Anyway I plan to implement some better way of running ZF apps from CLI in the future and will do a separate blog post about that.

      Feel free to contact me with any other kind of questions and issue you have as I'm interested to digg into this topic deeper.

      ReplyDelete
    5. Hi,

      How can I dump out some msg while my script is still running?

      I tried echo, var_dump, Zend_Log, nothing worked.

      Thanks!

      Li

      ReplyDelete
    6. yep, I've noticed that kind of issue too.
      Its depends on the Zend MVC design, where output is captured into Zend_Http_Responce object and rendered only at the end of the dispatch loop.

      Its not a huge problem for cron-scripts but can be serious issue for regular CLI apps and real-time debug output.

      I'm still thinking about the solution to send data directly to output, just like in Ruby'on'Rails.

      Will try to figure out something in next couple days.

      ReplyDelete
    7. I like this approach very much, it's the best solution I could find. However, it took I while to get it work:

      I had the "Cannot redeclare..."-problem mentioned before, and I fixed it by commenting only the following line in the code posted above:

      $paths = explode(PATH_SEPARATOR, get_include_path());

      Additionally, I had to remove the folloing line in my application.ini:

      ;includePaths.library = APPLICATION_PATH "/../library"

      Done this, everything works perfectly.

      Thanks a lot, Rudi

      ReplyDelete
    8. Hi, Rudi

      Thanks for your feedback.

      Provided script depends on the specific environment and sure it can be improved.

      My main idea was to show the people on how front controller can be extracted as a resource and used to set the CLI parameters.

      I'll try to update the listed source code to improve it somehow according to your comments.

      ReplyDelete
    9. Hi I new working with Zend, I can`t run the example you can please let me know if you have the source for see what I do wrong.

      Thanks.

      ReplyDelete
    10. Hi, Sebastián

      Can you please provide the error message you have with this code, or any other details that will allow me to identify the problem. Have you modified the code? Do you have a Zend Framework installed in your system?

      ReplyDelete
    11. Hi, Nika

      that is interesting... Can you please provide debug backtrace of this error? Just need to understand where the "Cli::addConfig()" method is called.


      Anyway as this class is just a mock object and it is designed actually just to do *nothing* - you can try and add a dummy method like:


      public function addConfig(){}


      in /library/Custom/Controller/Router/Cli.php

      Also I've updated the code of this class to add some other methods that can be potentially called.

      Let me look at your code and I'll be able to tell you the true cause of this issue.

      ReplyDelete
    12. Ok, I've played with your code on SVN and as I already mentioned in my previous comment - you just need to add the following line of code:

      public function addConfig(){}

      in /library/Custom/Controller/Router/Cli.php

      You need to mimic a router that will look just like real one, so we should create all dummy methods your application needs.

      I've also updated my code in this article with this line too.

      Looked around your code and think that it can be a good idea to move all CLI related calls into separate controller.
      That will allow you to control who accesses the functionality and prevent running that controller action from WEB.

      For extra convenience, turn of the render of layout for CLI controller/action, to receive only pure action output.

      for example with:

      public function init() {
      $this->_helper->layout->disableLayout();
      }

      Let me know if you will need anything else.

      ReplyDelete
    13. Hi,

      Firstly thanks for an informative post: clear, concise and easy to follow!

      I'm trying to integrate your code into an existing Zend Framework project of mine and seem to be having issues with the autoloader.

      When I run the code using my development environment command line (Windows 7 & Wamp & PHP 5.3.3 & Zend Framework 1.11.7) I receive the following error:

      PHP Fatal error: Class 'Zend_Console_Getopt' not found in D:\Wamp\www\demo\scripts\zf-cli.php on line 29

      Fatal error: Class 'Zend_Console_Getopt' not found in D:\Wamp\www\demo\scripts\zf-cli.php on line 29

      My library path should be set correctly as the Zend_Autoloader class is found on the require_once() but the autoloader doesn't appear to be a able to find other classes in the library.

      For reference the command I am using is:

      d:\wamp\bin\php\php5.3.3\php.exe -f "d:\wamp\www\demo\scripts\zf-cli.php" -a default/index/index -e development

      I am a rookie when it comes to php on the command line so any pointers you may have to help me resolve this would be most appreciated!

      ReplyDelete
    14. Hi, Harry

      First of all, just to make sure, check if file:..../library/Zend/Console/Getopt.php
      physically exists in your filesystem.
      If it doesn't - then your Zend install is incomplete and you should download it.

      Otherwise, it looks like "library" directory with Zend Framework is not in your include_path.

      I'm not familiar with Wamp, but note that webserver and cli environments can have a different "php.ini" configuration. So if everything works from the browser, it doesn't means it will work from the CLI. You should manually configure the CLI options.

      The easiest way is to define everything locally right in the bootstrap script. Look into the source code from this article and you will find the commented lines for manual include path configuration:

      "NOTE: anyway you can uncomment last 4 lines of this comments block"

      Read the comments and uncomment some lines (depends on your environment) and make sure your "library" directory is included into include path.

      Use for example var_dump($paths) to view path items.

      so if you will have
      "..../library/Zend/Console/Getopt.php"
      and "..../library/" will be included into your include path - your problem should be resolved!

      ReplyDelete
    15. Thanks for the great post and related comments.
      I was pulling my hair (what I have left) trying to figure out how to use a model from outside Zend Application in a cron job. Nightmare.

      If you need to post to the console I was able to use $STDOUT.

      $STDOUT = fopen("php://stdout", "w");
      fwrite($STDOUT, "Server started.\n Socket bind successful.\n");
      fclose($STDOUT);

      ReplyDelete
    16. Ha!!! Cool! Thanks for the magnificent idea about using the php://stdout.

      Will update the article to include this solution. Possibly will be useful for lots of people.

      ReplyDelete
    17. My long-term solution is similar to @Jim DelloStritto's solution, but a tad more "ZF-ish".

      $formatter = new Zend_Log_Formatter_Simple('%message%' . PHP_EOL);
      $writer = new Zend_Log_Writer_Stream('php://stdout');
      $writer->setFormatter($formatter);
      $logger = new Zend_Log($writer);
      $logger->info("Hello World!");

      I'll add the logger to my Zend_Registry and use it wherever it's needed.

      ReplyDelete
    18. Hi, i'm getting PHP Fatal error: Call to undefined method Zend_Controller_Request_Simple::getRequestUri() error. I came to understand with your explanation. Could you please give tips to solve this issue?

      ReplyDelete
    19. Unknown, The logger idea is great. Was looking for a quick and easy way to implement a logger. Thanks for adding.

      ReplyDelete
    20. Great job on this thank you very much, put a flattr button on this page and i'll send you some beer money for your efforts.

      My App had a custom router so i had to surround my ->setRouter() call in my Bootstrap class with if(!$front->getRouter() instanceof Custom_Controller_Router_Cli){

      Also anyone getting $request quirkiness with previously working code should look at Zend_Controller_Request_Abstract for alternative methods to those in Zend_Controller_Request_Http (as we are no longer using that for CLI)

      ReplyDelete
    21. Good script.
      Thank You.

      Robert.

      ReplyDelete
    22. I don't have modules folder, and when i try and load something it always loads the default page.. Any ideas???

      Cheers

      ReplyDelete
    23. This comment has been removed by the author.

      ReplyDelete
    24. Such a nice blog, you really provide me some great information. i am going to bookmark this blog, share with my friends and will definitely visit here again. Thanks for your article.
      Jimmy Wilson-Bane Apparel

      ReplyDelete
    25. Hi, tried with above implementation to execute cli scripts in zend. I am able to take control defined action on executing the script.

      ReplyDelete
      Replies
      1. Sorry, I am not able to take control to defined action.

        Delete
      2. I receive exception message
        ["message":protected]=>
        string(157) "Session must be started before any output has been sent to the br
        owser; output started in C:\Zend\Apache2\htdocs\tools.test.qa\library\Mygen
        \Util.php/54"
        ["string":"Exception":private]=>
        string(0) ""
        ["code":protected]=>
        int(0)
        ["file":protected]=>
        string(64) "C:\Zend\Apache2\htdocs\tools.test.qa\library\Zend\Session.php"
        ["line":protected]=>
        int(443)
        ["trace":"Exception":private]=>
        array(6) {

        Please advice.

        Delete