Using Zend Framework from the Command Line

14 Aug

I’ve been using the Zend Framework since roughly Beta 0.8 and have been thoroughly impressed with its evolution.  Besides web sites I also like using php from the command line for things like cron jobs, shell scripts and the like.  While it isn’t always the best suited language for such tasks, my familiarity with php often lets me get the job done faster than with another less familiar language.  A few days ago I sat down and figured out how to get full access to the Zend Framework, including MVC, from the command line.  Here’s how I did it…

This project is solely for cli use, it will contain a number of scripts running in the same “framework.” There is no web interface for this at all.  I’m going to use a modified modular conventional file system to allow me to easily addon more scripts as I need them and still share the same infrastructure.   My file system looks like this:

.-- project root
|-- application
|   |-- apis
|   |-- config
|   |-- layouts
|   |-- library
|   |   |-- Webf
|   |   `-- Zend
|   |-- models
|   |-- modules
|   |   |-- default
|   |   |   |-- config
|   |   |   |-- controllers
|   |   |   |-- models
|   |   |   `-- views
|   |   |       |-- filters
|   |   |       |-- helpers
|   |   |       |-- layouts
|   |   |       `-- scripts
|   |   `-- moduleX
|   |       |-- config
|   |       |-- controllers
|   |       |-- models
|   |       `-- views
|   |           |-- filters
|   |           |-- helpers
|   |           |-- layouts
|   |           `-- scripts
|   |-- tests
|   `bootstrap.php
|-- data
|   |-- cache
|   |-- indexes
|   |-- locales
|   |-- logs
|   |-- sessions
|   `-- uploads
|-- docs
|-- tmp
'zfcli.php

* Not all of these directory are needed for this application.

Typically you would have a public (htdocs, www, etc..) folder for the web root, however since this isn’t a web project I’m using zfcli.php as my “bootstrap” file, which is located in the project root. This file will glue Zend_Console_Getopt together with Zend_Controller_Request_Simple to get arguments from the command line into the request object. This will allow us to use ZF to its full potential from the command line.

Lets go ahead an create a simple IndexController and view so we have something to shoot for:

./application/modules/default/controllers/IndexController.php

class IndexController extends Zend_Controller_Action
{
    public function indexAction() {}
}

./application/modules/default/views/scripts/index/index.phtml

echo "default:index:index\n\n";

I’ll assume you understand the above, if not please check google for some getting started with Zend Framework tutorials.

Ok, we’re ready to get going.  First up we need the bootstrap file:

./zfcli.php

error_reporting(E_ALL|E_STRICT);
ini_set('display_errors',true);
$pthRoot = dirname(__FILE__);
set_include_path('.' . PATH_SEPARATOR . $pthRoot . '/application/library'
    . PATH_SEPARATOR . $pthRoot . '/application/models'
    . PATH_SEPARATOR . get_include_path());
date_default_timezone_set('America/Chicago');

require_once('Zend/Loader.php');
Zend_Loader::registerAutoload();

try {
    $opts = new Zend_Console_Getopt(
        array(
            'help|h' => 'Displays usage information.',
            'action|a=s' => 'Action to perform in format of module.controller.action',
            'verbose|v' => 'Verbose messages will be dumped to the default output.',
            'development|d' => 'Enables development mode.',
        )
    );
    $opts->parse();
} catch (Zend_Console_Getopt_Exception $e) {
    exit($e->getMessage() ."\n\n". $e->getUsageMessage());
}

if(isset($opts->h)) {
    echo $opts->getUsageMessage();
    exit;
}

if(isset($opts->a)) {
    $reqRoute = array_reverse(explode('.',$opts->a));
    @list($action,$controller,$module) = $reqRoute;
    $request = new Zend_Controller_Request_Simple($action,$controller,$module);
    $front = Zend_Controller_Front::getInstance();

    $front->setRequest($request);
    $front->setRouter(new Webf_Controller_Router_Cli());

    $front->setResponse(new Zend_Controller_Response_Cli());

    $front->throwExceptions(true);
    $front->addModuleDirectory($pthRoot . '/application/modules/');

    $front->dispatch();
}

Here we have setup Zend_Console_Getopt with several possible parameters, -a being the one we will use to get the module.controller.action route for the MVC.  After setting up Getopt, we parse() the command line options.  Should an exception be encountered we want to catch it, display the error message and then show the usage message.  Using -h will also show us the usage message.

Then last if{} block is where we get the unification going.  By exploding on “.” we get an array of the module, controller and action, reversing the order makes it easier to deal with missing modules.  Break this new array out into individual variables then plug them into Zend_Controller_Request_Simple. if you don’t use the @ in front of the list() statement you will get warnings when there is no module supplied, since I want to use default in these cases I just make it not report these errors, the router will automatically look for the “default” module.

Next we instantiate the front controller and tell it to use the request we just built.  Next up we need a router that can be used in the CLI environment.  Notice I’ve made a custom router, more on that in a bit. We also need to use the Zend_Controller_Response_Cli response object, tell it where the modules are, tell it to throw exceptions.  Finally we dispatch the controller and our scripts are off and running.

./application/library/Webf/Controller/Router/Cli.php

require_once ('Zend/Controller/Router/Interface.php');
require_once ('Zend/Controller/Router/Abstract.php');

class Webf_Controller_Router_Cli extends Zend_Controller_Router_Abstract implements Zend_Controller_Router_Interface {

    public function assemble($userParams, $name = null, $reset = false, $encode = true) { }

    public function route(Zend_Controller_Request_Abstract $dispatcher) {}
}

The router is really very simple, though it took me a bit to figure this out.  In order to ensure compatibility we need to extend the Router_Abstract and implement the Router_Interface.  If, like me, you use a Zend IDE, like Zend Studio for Eclipse, and make use of the class wizards you can end up with a short list of methods to implement.  In fact you only need two and they can be stubs.

That’s it, you’re in business.  Try running php zfcli.php -a index.index and see what happens.  You should see default:index:index just above your prompt ;)

About these ads

27 Responses to “Using Zend Framework from the Command Line”

  1. Alex August 18, 2008 at 9:07 pm #

    Your blog is interesting!

    Keep up the good work!

  2. Riki Risnandar September 2, 2008 at 9:16 pm #

    Thanks !
    straight and very easy to follow. Finally i can run the cli mode in zend framework within minutes :)

  3. David Mintz September 25, 2008 at 3:18 pm #

    Very nice!

    Maybe — just maybe — you should consider exiting from error/exception conditions with a non-zero status in case you’re called by another script that needs to know the exit status.

  4. webfractor September 26, 2008 at 2:38 pm #

    I agree, I am waiting for the exception objects to get error numbers, then I would exit with the exception code. Until then I have edited the script to exit with the exception message. Perhaps not ideal, but better then using an arbitrary value in my opinion.

  5. James Eastman October 24, 2008 at 3:04 pm #

    webfractor:

    Great article on Zend Framework and its us in the CLI. I’ve been able to use this guidance to get my CLI efforts off to a great start. I’m now trying to integrate DB and table usage and I am having problems. I can get the connection to the DB fine but when I try to use a Model I get rather voluminous errors. Here’s what I have so far ->

    At the bottom of the last If statement in zfcli.php I’ve added ::

    // load configuration
    $config = new Zend_Config_Ini(‘/home/rt_reporter/application/config.ini’, ‘general’);
    $registry = Zend_Registry::getInstance();
    $registry->set(‘config’, $config);
    // setup database
    try {
    $db = Zend_Db::factory($config->db);
    } catch (PDOException $e){
    die($e->getMessage);

    }

    Zend_Db_Table::setDefaultAdapter($db);

    This works as expected and no exception is thrown/caught. Since the table I want to access is a table called Users I created a file in /application/models called Users.php and in that file I placed.

    class Users extends Zend_Db_Table
    {
    protected $_name = ‘Users';
    }

    Then, to get records back and use them for StdOut display I placed the following in IndexController.php ->

    $Users = new Users();
    $this->view->Users = $Users->fetchAll();

    When i try to run this I get the following ->

    [user@box ~]$ ./zfcli.php -a index.index
    PHP Fatal error: Uncaught exception ‘Zend_Db_Table_Exception’ with message ‘No adapter found for Users’ in /php/includes/library/Zend/Db/Table/Abstract.php:548
    Stack trace:
    #0 /php/includes/library/Zend/Db/Table/Abstract.php(531): Zend_Db_Table_Abstract->_setupDatabaseAdapter()
    #1 /php/includes/library/Zend/Db/Table/Abstract.php(268): Zend_Db_Table_Abstract->_setup()
    #2 /home/BlahBlahBlah/application/modules/default/controllers/IndexController.php(6): Zend_Db_Table_Abstract->__construct()
    #3 /php/includes/library/Zend/Controller/Action.php(494): IndexController->indexAction()
    #4 /php/includes/library/Zend/Controller/Dispatcher/Standard.php(285): Zend_Controller_Action->dispatch(‘indexAction’)
    #5 /php/includes/library/Zend/Controller/Front.php(934): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Simple), Object(Zend_Controller_Response_Cli))
    #6 /home/BlahBlahBlah/zfcli.php(50): Zend_Controller_Front->dispatch()
    #7 {main}
    thrown in /php/includes/library/Zend/Db/Table/Abstract.php on line 548

    Fatal error: Uncaught exception ‘Zend_Db_Table_Exception’ with message ‘No adapter found for Users’ in /php/includes/library/Zend/Db/Table/Abstract.php:548
    Stack trace:
    #0 /php/includes/library/Zend/Db/Table/Abstract.php(531): Zend_Db_Table_Abstract->_setupDatabaseAdapter()
    #1 /php/includes/library/Zend/Db/Table/Abstract.php(268): Zend_Db_Table_Abstract->_setup()
    #2 /home/BlahBlahBlah/application/modules/default/controllers/IndexController.php(6): Zend_Db_Table_Abstract->__construct()
    #3 /php/includes/library/Zend/Controller/Action.php(494): IndexController->indexAction()
    #4 /php/includes/library/Zend/Controller/Dispatcher/Standard.php(285): Zend_Controller_Action->dispatch(‘indexAction’)
    #5 /php/includes/library/Zend/Controller/Front.php(934): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Simple), Object(Zend_Controller_Response_Cli))
    #6 /home/BlahBlahBlah/zfcli.php(50): Zend_Controller_Front->dispatch()
    #7 {main}
    thrown in /php/includes/library/Zend/Db/Table/Abstract.php on line 548
    [user@box ~]$

    What might I be doing wrong here? Thanks in advance for the article and for the help.

    -J

  6. webfractor October 25, 2008 at 7:24 pm #

    First thing I notice is that Users extended Zend_Db_Table. I usually extend Zend_Db_Table_Abstract for my models. Not sure if that is what is causing your issue.

    A simple test to see if your db adapter is setup correctly and can be accessed is the include this line right before you register the default adapter.

    $db->getConnection();

    Zend_Db_Table::setDefaultAdapter($db);

    Since the Zend_Db::factory() doesn’t actually connect to the db it just configures the appropriate adapter, calling getConnection() will actually establish the connection to the db. This is a good way to see if your configuration works. I wouldn’t leave it in there for production as its unnecessary processing but to debug it is handy.

  7. Andrew November 28, 2008 at 12:15 am #

    What about passing arguments to the controllers, to make it more like a command line interface?

    I have done the following which is a good start IMO (but I don’t know much about PHP or Zend)…

    In zfcli.php, change
    $request = new Zend_Controller_Request_Simple($action,$controller,$module);
    to include the $params argument which is $opts->getRemainingArgs(), so the changed line reads
    $request = new Zend_Controller_Request_Simple($action,$controller,$module,$opts->getRemainingArgs());

    I’d appreciate a better way to do this, probably parsing $opts->getRemainingArgs() to allow keying the resulting array with something other than the numerical index. An example might be

    $ php zfcli.php -a index.index arg1=val1 arg2=val2

    then in the indexAction I can get the args as ‘arg1′ and arg2′.

    Oh, and I’m getting at the params in the action with $this->_request->getParams() which I don’t think is quite the right way. I’d appreciate any guidance on this.

    Thanks for an awesome tip :)

    Andrew

  8. Andrew November 28, 2008 at 12:31 am #

    OK, I’m using this for argument parsing now…

    $args = Array();
    foreach ($opts->getRemainingArgs() as $arg) {
    $tmp = split(‘=’, $arg, 2);
    if (count($tmp) == 1) {
    $args[] = $tmp[0];
    }
    else {
    $args[$tmp[1]] = $tmp[0];
    }
    }

  9. Slavi November 30, 2008 at 6:47 am #

    Hi,

    Thanks so much for that example how to use ZF in such way.

    Why do you need to pass additional parameters e.g. -h -a etc ?

    Slavi

  10. Slavi November 30, 2008 at 7:40 am #

    Hi again,

    Here is a shorter alternative.
    You don’t need to create anything but add a single line of code. Hack TM :)

    The script would be called e.g.:
    php index.php /default/test/index

    Put the following line above the $front->dispatch(); method

    if (!empty($argv[1])) {
    $_SERVER['REQUEST_URI'] = $argv[1];
    }

    Slavi

    • BVH September 29, 2009 at 11:02 pm #

      Thanks a lot. This way really helps!!!

  11. arslan April 20, 2009 at 10:30 am #

    How your script ensures that index/index get called from cli interface and not from web interface. Still my cron job action is exposed to access from browser

    How can i prevent user from executing cron job from their browser ??

    • webFRACtor April 20, 2009 at 4:14 pm #

      The best way is make them separate structures. If you use *nix on your host you can sym link the library structures to use the same lib for several “installs.” Otherwise, you can treat the cron setup like another vhost. Additionally you can hard code a module based on whether any getOpts are set. There are a number of ways to keep the cron calls safe from the browser. Personally I separate my structures all together, it’s just easier.

  12. josh June 4, 2009 at 3:47 pm #

    thanks a million , I set up a zend cli !!!!!!!!!!!!!!!! yuppi

  13. Hector Virgen June 29, 2009 at 11:48 pm #

    When using your custom router, do you have problems with assembling URLs? For example, $view->url() uses the router’s assemble method to create the URL.

    • Jed June 30, 2009 at 1:55 am #

      I don’t use the CLI apps to make URLs at all so I don’t know if there would be issues with that or not.

  14. Diorgenes Felipe Grzesiuk August 25, 2009 at 5:15 pm #

    Hi,

    I’m using your exemple for building my application, but i have some problems when i have access one crontroller that call one model…

    How to do this? Can you make one case these in your exemple?

    Thanks

  15. Steve Hollis September 17, 2009 at 1:53 pm #

    Fantastic guide on the principles behind using ZF with CLI.

    The only changes I made were to incorporate this code into my existing bootstap (which is not accessible to the public). You can detect whether or not the request is CLI easily enough by checking for the existence of the $argc and $argv variables (or the $_SERVER[ 'argX' ] equivalents).

    This saves me repeating the code which sets up my application, but whether or not this is a good idea for you will depend entirely on your app.

    Thanks again.

  16. Jed September 29, 2009 at 11:11 pm #

    It’s been over a year since I wrote this. It’s nice to see some people are still getting some use out of it, however ZF has grown considerably since I wrote this. As I am not currently working with ZF I will not be updating any of the code here. Please feel free to submit updates or changes that may work better with the newer ZF versions. I will periodically approve comments.

    Thanks for all the feedback and good luck with your projects!

  17. onin October 16, 2009 at 6:53 am #

    hi jed,

    we need your help here.
    currently the your samples isn’t working on the latest zend framework.

    please update. your page is the most searchable when in comes to zend framework cli tutorial. lots of people needs your help man.

    thanks.

    • Jed April 13, 2010 at 12:37 am #

      I have not looked at ZF since I wrote this so I am not familiar with any changes that have occurred. There are many resources on the web that can lend assistance. I would recommend Matthew Weier O’Phinney ‘s blog, #ZFTalk, or http://www.zfforums.com

      Thanks

  18. peter April 12, 2010 at 11:33 am #

    How to call actions in a different controller or even a different module?

    Thanks!

  19. Joseph Chereshnovsky September 27, 2010 at 11:53 am #

    Hey, guys!

    Checkout my post about running MVC from CLI using the latest Zend Framework version, inspired by this article.

    http://webdevbyjoss.blogspot.com/2010/09/running-zend-framework-application-from.html

    • Jed October 3, 2010 at 9:03 pm #

      Thanks for the cite in your article. I’m glad to see someone in the community picked this up and gave a fresh update.

      Good luck on your projects!

Trackbacks/Pingbacks

  1. Zend Framework Wochenrückblick 39/2008 » Ralfs Zend Framework und PHP Blog - September 27, 2008

    [...] Zend Frameworks ohne MVC auf. Die Diskussion über Cronjobs mit dem Zend Framework brachte diesen interessanten Link hervor. Die Umfrage nach der Verwendung von Smarty brachte unter anderem hervor, dass viele Smarty [...]

  2. Zend Framework - Tutorial 2. Teil - Von der Anfrage zum Controller | christian-renner.eu - January 31, 2009

    [...] Dies ist ratsam, wenn man HTTP-Requests im Vorfeld bearbeiten möchte oder andere Protokolle (CLI, PHP-GTK) einsetzen möchte. In diesen Fällen erweitert man die Basisklasse [...]

  3. Cork Web Design ::: Spiralli Business Solutions, Mallow, Co. Cork - April 21, 2009

    [...] but nothing is generally available now. I was up the creek without a paddle…. Until I found this. The author built a bootstrapper with a difference: “This file will glue Zend_Console_Getopt [...]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: