Running a Zend Framework action from command line
Asked Answered
R

10

64

I would like to run a Zend Framework action to generate some files, from command line. Is this possible and how much change would I need to make to my existing Web project that is using ZF?

Thanks!

Rebarebah answered 24/2, 2010 at 10:48 Comment(0)
R
64

It's actually much easier than you might think. The bootstrap/application components and your existing configs can be reused with CLI scripts, while avoiding the MVC stack and unnecessary weight that is invoked in a HTTP request. This is one advantage to not using wget.

Start your script as your would your public index.php:

<?php

// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH',
              realpath(dirname(__FILE__) . '/../application'));

// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV',
              (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV')
                                         : 'production'));

require_once 'Zend/Application.php';
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/config.php'
);

//only load resources we need for script, in this case db and mail
$application->getBootstrap()->bootstrap(array('db', 'mail'));

You can then proceed to use ZF resources just as you would in an MVC application:

$db = $application->getBootstrap()->getResource('db');

$row = $db->fetchRow('SELECT * FROM something');

If you wish to add configurable arguments to your CLI script, take a look at Zend_Console_Getopt

If you find that you have common code that you also call in MVC applications, look at wrapping it up in an object and calling that object's methods from both the MVC and the command line applications. This is general good practice.

Radford answered 24/2, 2010 at 20:54 Comment(3)
Thanks for the extensive answer. How would you suggest that I use views in such CLI application? For example, suppose I want to generate a CSV or an XML file through a view? Can I take advantage of the full MCV pattern in this case?Rebarebah
A Zend_View is really just a templating component, and it can be used in isolation. Literally $view = new Zend_View(); $view->var = 'some data'; and then $view->render('/path/to/script.phtml'); You can also bootstrap a view as normal to set paths/helpers/options and retrieve it from the application. If you wish to simply replace a few details in an XML document, you could use a view. For generating highly dynamic documents, I would just suggest SimpleXML or XMLWriter without a viewRadford
Thanks - I've now posted a blog article with roughly the same content at davidcaunt.co.uk/2010/02/25/…Radford
E
68

UPDATE

You can have all this code adapted for ZF 1.12 from https://github.com/akond/zf-cli if you like.

While the solution #1 is ok, sometimes you want something more elaborate. Especially if you are expecting to have more than just one CLI script. If you allow me, I would propose another solution.

First of all, have in your Bootstrap.php

protected function _initRouter ()
{
    if (PHP_SAPI == 'cli')
    {
        $this->bootstrap ('frontcontroller');
        $front = $this->getResource('frontcontroller');
        $front->setRouter (new Application_Router_Cli ());
        $front->setRequest (new Zend_Controller_Request_Simple ());
    }
}

This method will deprive dispatching control from default router in favour of our own router Application_Router_Cli.

Incidentally, if you have defined your own routes in _initRoutes for your web interface, you would probably want to neutralize them when in command-line mode.

protected function _initRoutes ()
{
    $router = Zend_Controller_Front::getInstance ()->getRouter ();
    if ($router instanceof Zend_Controller_Router_Rewrite)
    {
        // put your web-interface routes here, so they do not interfere
    }
}

Class Application_Router_Cli (I assume you have autoload switched on for Application prefix) may look like:

class Application_Router_Cli extends Zend_Controller_Router_Abstract
{
    public function route (Zend_Controller_Request_Abstract $dispatcher)
    {
        $getopt = new Zend_Console_Getopt (array ());
        $arguments = $getopt->getRemainingArgs ();
        if ($arguments)
        {
            $command = array_shift ($arguments);
            if (! preg_match ('~\W~', $command))
            {
                $dispatcher->setControllerName ($command);
                $dispatcher->setActionName ('cli');
                unset ($_SERVER ['argv'] [1]);

                return $dispatcher;
            }

            echo "Invalid command.\n", exit;

        }

        echo "No command given.\n", exit;
    }


    public function assemble ($userParams, $name = null, $reset = false, $encode = true)
    {
        echo "Not implemented\n", exit;
    }
}

Now you can simply run your application by executing

php index.php backup

In this case cliAction method in BackupController controller will be called.

class BackupController extends Zend_Controller_Action
{
    function cliAction ()
    {
        print "I'm here.\n";
    }
}

You can even go ahead and modify Application_Router_Cli class so that not "cli" action is taken every time, but something that user have chosen through an additional parameter.

And one last thing. Define custom error handler for command-line interface so you won't be seeing any html code on your screen

In Bootstrap.php

protected function _initError ()
{
    $error = $frontcontroller->getPlugin ('Zend_Controller_Plugin_ErrorHandler');
    $error->setErrorHandlerController ('index');

    if (PHP_SAPI == 'cli')
    {
        $error->setErrorHandlerController ('error');
        $error->setErrorHandlerAction ('cli');
    }
}

In ErrorController.php

function cliAction ()
{
    $this->_helper->viewRenderer->setNoRender (true);

    foreach ($this->_getParam ('error_handler') as $error)
    {
        if ($error instanceof Exception)
        {
            print $error->getMessage () . "\n";
        }
    }
}
Eridanus answered 16/1, 2011 at 17:46 Comment(4)
This worked perfectly for me. I created a separate CLI bootstrap so I could load only the vital components without any of the usual web view initialization. ThanksDiastasis
Is it right that I had to create an Application/router/Cli.php in /library ?Empyreal
Yes, it is. Don't forget to turn on autoload for Application prefix in application.ini via autoloadernamespaces[] = ApplicationEridanus
I typically set my APPLICATION_ENV in my .htaccess and this seems to bypass that. Where would be the best place to set that with this code?Tricky
R
64

It's actually much easier than you might think. The bootstrap/application components and your existing configs can be reused with CLI scripts, while avoiding the MVC stack and unnecessary weight that is invoked in a HTTP request. This is one advantage to not using wget.

Start your script as your would your public index.php:

<?php

// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH',
              realpath(dirname(__FILE__) . '/../application'));

// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV',
              (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV')
                                         : 'production'));

require_once 'Zend/Application.php';
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/config.php'
);

//only load resources we need for script, in this case db and mail
$application->getBootstrap()->bootstrap(array('db', 'mail'));

You can then proceed to use ZF resources just as you would in an MVC application:

$db = $application->getBootstrap()->getResource('db');

$row = $db->fetchRow('SELECT * FROM something');

If you wish to add configurable arguments to your CLI script, take a look at Zend_Console_Getopt

If you find that you have common code that you also call in MVC applications, look at wrapping it up in an object and calling that object's methods from both the MVC and the command line applications. This is general good practice.

Radford answered 24/2, 2010 at 20:54 Comment(3)
Thanks for the extensive answer. How would you suggest that I use views in such CLI application? For example, suppose I want to generate a CSV or an XML file through a view? Can I take advantage of the full MCV pattern in this case?Rebarebah
A Zend_View is really just a templating component, and it can be used in isolation. Literally $view = new Zend_View(); $view->var = 'some data'; and then $view->render('/path/to/script.phtml'); You can also bootstrap a view as normal to set paths/helpers/options and retrieve it from the application. If you wish to simply replace a few details in an XML document, you could use a view. For generating highly dynamic documents, I would just suggest SimpleXML or XMLWriter without a viewRadford
Thanks - I've now posted a blog article with roughly the same content at davidcaunt.co.uk/2010/02/25/…Radford
S
8

Just saw this one get tagged in my CP. If you stumbled onto this post and are using ZF2, it's gotten MUCH easier. Just edit your module.config.php's routes like so:

/**
 * Router
 */

'router' => array(
    'routes' => array(

        // .. these are your normal web routes, look further down
    ),
),

/**
 * Console Routes
 */
'console' => array(
    'router' => array(
        'routes' => array(

            /* Sample Route */
            'do-cli' => array(
                'options' => array(
                    'route'    => 'do cli',
                    'defaults' => array(
                        'controller' => 'Application\Controller\Index',
                        'action'     => 'do-cli',
                    ),
                ),
            ),
        ),    
    ),
),

Using the config above, you would define doCliAction in your IndexController.php under your Application module. Running it is cake, from the command line:

php index.php do cli

Done! Way smoother.

Sky answered 11/2, 2013 at 2:2 Comment(1)
Good find, here is a link to the documentation framework.zend.com/manual/2.0/en/modules/…Staats
S
6

akond's solution above is on the best track, but there are some subtleties that may may his script not work in your environment. Consider these tweaks to his answer:

Bootstrap.php

protected function _initRouter()
{
    if( PHP_SAPI == 'cli' )
    {
        $this->bootstrap( 'FrontController' );
        $front = $this->getResource( 'FrontController' );
        $front->setParam('disableOutputBuffering', true);
        $front->setRouter( new Application_Router_Cli() );
        $front->setRequest( new Zend_Controller_Request_Simple() );
    }
}

Init error would probably barf as written above, the error handler is probably not yet instantiated unless you've changed the default config.

protected function _initError ()
{
    $this->bootstrap( 'FrontController' );
    $front = $this->getResource( 'FrontController' );
    $front->registerPlugin( new Zend_Controller_Plugin_ErrorHandler() );
    $error = $front->getPlugin ('Zend_Controller_Plugin_ErrorHandler');
    $error->setErrorHandlerController('index');

    if (PHP_SAPI == 'cli')
    {
        $error->setErrorHandlerController ('error');
        $error->setErrorHandlerAction ('cli');
    }
}

You probably, also, want to munge more than one parameter from the command line, here's a basic example:

class Application_Router_Cli extends Zend_Controller_Router_Abstract
{
    public function route (Zend_Controller_Request_Abstract $dispatcher)
    {
        $getopt     = new Zend_Console_Getopt (array ());
        $arguments  = $getopt->getRemainingArgs();

        if ($arguments)
        {
            $command = array_shift( $arguments );
            $action  = array_shift( $arguments );
            if(!preg_match ('~\W~', $command) )
            {
                $dispatcher->setControllerName( $command );
                $dispatcher->setActionName( $action );
                $dispatcher->setParams( $arguments );
                return $dispatcher;
            }

            echo "Invalid command.\n", exit;

        }

        echo "No command given.\n", exit;
    }


    public function assemble ($userParams, $name = null, $reset = false, $encode = true)
    {
        echo "Not implemented\n", exit;
    }
}

Lastly, in your controller, the action that you invoke make use of the params that were orphaned by the removal of the controller and action by the CLI router:

public function echoAction()
{
    // disable rendering as required
    $database_name     = $this->getRequest()->getParam(0);        
    $udata             = array();

    if( ($udata = $this->getRequest()->getParam( 1 )) )
        $udata         = explode( ",", $udata );

    echo $database_name;
    var_dump( $udata );
}

You could then invoke your CLI command with:

php index.php Controller Action ....

For example, as above:

php index.php Controller echo database123 this,becomes,an,array

You'll want to implement a more robust filtering/escaping, but, it's a quick building block. Hope this helps!

Sky answered 10/8, 2012 at 13:15 Comment(1)
Could you explain why you disable output buffering? $front->setParam('disableOutputBuffering', true);Anselm
T
0

One option is that you could fudge it by doing a wget on the URL that you use to invoke the desirable action

Tobitobiah answered 24/2, 2010 at 11:0 Comment(0)
T
0

You cant use -O option of wget to save the output. But wget is clearly NOT the solution. Prefer using CLI instead.

Turgor answered 1/4, 2011 at 15:41 Comment(0)
B
0

akond idea works great, except the error exception isnt rendered by the error controller.

public function cliAction() {
  $this->_helper->layout->disableLayout();
  $this->_helper->viewRenderer->setNoRender(true);

  foreach ($this->_getParam('error_handler') as $error) {
    if ($error instanceof Exception) {
      print "cli-error: " . $error->getMessage() . "\n";
    }
  }
}

and In Application_Router_Cli, comment off the echo and die statement

public function assemble($userParams, $name = null, $reset = false, $encode = true) {
//echo "Not implemented\n";
}
Bushranger answered 26/12, 2012 at 5:10 Comment(0)
H
-1

You can just use PHP as you would normally from the command line. If you call a script from PHP and either set the action in your script you can then run whatever you want.

It would be quite simple really. Its not really the intended usage, however this is how it could work if you wanted to.

For example

 php script.php 

Read here: http://php.net/manual/en/features.commandline.php

Harlanharland answered 24/2, 2010 at 12:10 Comment(0)
C
-1

You can use wget command if your OS is Linux. For example:

wget http://example.com/controller/action

See http://linux.about.com/od/commands/l/blcmdl1_wget.htm

UPDATE:

You could write a simple bash script like this:

if wget http://example.com/controller/action
    echo "Hello World!" > /home/wasdownloaded.txt
else
    "crap, wget timed out, let's remove the file."
    rm /home/wasdownloaded.txt
fi

Then you can do in PHP:

if (true === file_exists('/home/wasdownloaded.txt') {
    // to check that the 
}

Hope this helps.

Cortege answered 24/2, 2010 at 12:59 Comment(2)
wget is a possibility, but then your scripts are vulnerable to timeoutsRadford
@David You could write a bash script with if else that would save the return value of wget to a file and check with php that that fie exists. But that seems a little overcomplicated, I agree. Nevertheless, I am actually going to update my answer. Maybe it will help.Cortege
O
-2

I have used wget command

wget http://example.com/module/controller/action -O /dev/null

-O /dev/null if you dont want to save the output

Overlap answered 14/12, 2011 at 14:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.