zen of coding

A deeper look at working with CakePHP shells

CakePHP shells are very useful, whenever one needs to extends the functionality of the application to be used in the console (or from command line). Besides the built-in CakePHP shells, like “bake” and “schema”, anyone can very easily extend their own application to run from the console.

Why is that a good thing or what is it generally used for?

Well, probably the #1 reason to write a shell is to allow certain aspects of your application to be executed by cron (behind the scenes), rather than through human intervention or web interface. This is especially useful, when large amounts of data need to be processed and there is no need for a human to stare at the screen and wait for any response.

The other option is, of course, to be able to automate some mundane tasks… just take a look at the power of “bake”.

To be honest, the only limitation as to what a given shell can do is really up to your imagination, but the bottom line is that having the ability to leverage the framework in such a way gives many developers a very powerful tool.

Since the manual is bit scarce (for now) on the information about actually working with shells, I wanted to provide some hints and tips here and hopefully get a little feedback to further explore the best practices to write shells.

1. General approach to shell construction

I hope you’ve read the quick example in the manual, and have a basic idea on how to setup a simple shell.

Let’s see how to proceed from there…
main() is the function that gets executed whenever the the shell is called.
One way to think of it is almost as a dispatcher method, since any shell that is somewhat complex should not contain the entire code within the main() function, obviously.
Also, most shells will probably accept a few arguments, which should trigger different actions within your application.

For example, we can pretend that we are building a “billing” shell, which goes through a few rounds of a billing cycle.
Our shell can, therefore, be invoked with something like:
#cake bill weekly

Here we are calling the BillShell and telling it to process “weekly” billing.

There are two approaches we can take here:
1. Is to create a weekly() method in the shell, which will get executed after the main()
2. Is to use the main() method to dispatch the “weekly” option to the appropriate part of our shell.

I find the second approach a little more safe (because weekly method can be accessed directly), however, that would really depend on your specific need.
Also, because #1 is so straight-forward, let’s consider the latter approach for this example:

function main() {

      $this->out(__('Starting Billing process...', true));
      $this->hr();

      if (!isset($this->args[0])) {
         $this->err(__('Please provide a valid billing round', true));
         $this->_stop();
      }

      $round = $this->args[0];

      switch($round) {

        case('weekly'):
          $this->out(__('Starting weekly round...', true));
          $this->__weeklyRound();
        break;

        case('monthly'):
          $this->out(__('Starting monthly round...', true));
          $this->__monthlyRound();
        break;

        case('quarterly'):
          $this->out(__('Starting quarterly round...', true));
          $this->__quarterlyRound();
        break;

        defau<
          $this->err(__('Please provide a valid billing round', true));
        $this->_stop();

      }
    }

To summarize, we are “catching” the passed option to our shell via the simple $round = $this->args[0];.
Of course, if nothing is provided or an unexpected option is provided we throw an error message.
We only expect one option to be passed, therefore $this->args[0] works perfectly well.
Then, we evaluate the passed option, and dispatch it to the right private method within our shell, based on the value.

2. Some useful shell methods

You’ve noticed a few methods, which are part of the cake’s core Shell, in use here… let me briefly cover some of them:

out() – Outputs some some message, text, etc.
err() – Outputs an error message.
error() – Does the same thing as above, but also stops execution of the shell.
hr() – Outputs a dashed line, which is nice to use as a separator.
in() – Makes your shell interactive, i.e. prompts the user for input, and returns it.

Here’s an example:

$choice = strtoupper($this->in(__('Which billing round would you like to run?', true), array('W', 'M', 'Q')));

We could’ve used the line above to prompt the user, rather than requiring a parameter to be passed in. The only downside here, is that obviously running this from cron would not work.

_stop() – Halts shell execution.

3. Loading models

Update (07/15/2009): thanks to Matt Curry, who pointed out that “If you declare your own initialize() method you have to make sure to call parent::initialize()“. This allows one to load models via $uses, just as in a regular Controller and access them in the same manner, without any App::import() trickery.

Unlike what the manual suggests, I find that it works best to load the models via App::import() rather than some other method.

So, to extend our example we’d be doing something like:

App::import('Model', 'Billing');

class BillShell extends Shell {

    //holds the instance of our Model
    var $Billing;

    function startup() {
        $this->Billing = new Billing();
    }

    //.... the rest of the shell code
    //......................................

First, we’ve imported our model.
Secondly, we’ve prepared a property to hold the model instance.
Lastly, we use a “special” startup() method to instantiate the model object.

As you might’ve guessed the startup() method is automatically invoked, whenever the shell is started.

4. Proceeding further

Now that we have our model loaded and instantiated, we can call any of its methods, or associated model methods from the shell.
Going back to our example, let’s take a look at our __weeklyRound() method:

function __weeklyRound() {
      $this->out(__('Initializing payment cycle', true));

      $numProcessed = $this->Billing->processInitialPayment();
      $this->out(__($numProcessed . ' payments processed', true));

      $data['BillingTransaction'] = $this->Billing->getProcessedInfo();
      $this->Billing->BillingTransaction->logTransaction($data['BillingTransaction']);
      $this->out(__(count($data['BillingTransaction']) . ' entries logged', true));

      $this->out(__('Billing cycle complete', true));
}

I hope this code is simple enough to read as an example.
Of course, because we are good boys and girls, our business logic is neatly tucked away inside the various model methods, therefore it becomes extremely easy and painless to reuse the model code directly from the shell. Another notch to the fat models, skinny controller mantra.

5. Review

To sum things up…

  • We’ve learned how to build a basic shell
  • We’ve considered some ways to process options, which can be passed to the shell and how to dispatch the option to trigger an appropriate method within our shell
  • We’ve learned some useful shell methods
  • We saw how to load and instantiate the models
  • We’ve reviewed the magic startup() method
  • We remembered how easy it is to be DRY, by keeping business logic in the model layer

Now armed with this little bit of knowledge you should be on your way to creating awesome console applications ;)

P.S. One thing I didn’t mention is another “magic” method initialize(), which works very similar to startup() as it automatically runs, whenever the shell is called. From looking at other shells it is generally used to display the welcome message and to inform the user about some basic shell state and functionality. Although I don’t know if there is any specific golden rule for the best approach here…

  • gwoo

    you can have var $uses in shells. Also instead of the big switch statement inside of “main” you can have methods in your shells that will be called if they exist.

  • @gwoo

    thanks, for the feedback.

    $uses simiply did not work for me. following the example in the manual, I kept getting the “non-object” errors … and i’ve tried a few things to get it to work.

    you are right about the switch, there’s certainly a number of ways to approach the dispatch.

  • Pingback: Teknoid’s Blog: A deeper look at working with CakePHP shells | Webs Developer()

  • Pingback: Teknoid’s Blog: A deeper look at working with CakePHP shells | DreamNest - Technology | Web | Net()

  • Regarding uses not working:
    If you declare your own initialize() method you have to make sure to call parent::initialize(). I mess this up all the time.

  • @Matt Curry

    Thank you, we have a winner!
    ;)

  • Pingback: CakePHP : signets remarquables du 14/07/2009 au 15/07/2009 | Cherry on the...()

  • Really nicely done teknoid!
    Is Matt Curry referring to the Model::intitialize() ? or the shell initialize() ?

    I use the help() method a lot. You can specify your own custom helkp message for the other commandline users (or for yourself) :)

    Someting I don’t like is that I can’t use parameters with ‘-‘ prepended: -userid 1 -round weekly.
    Maybe you have a solution for that?

    Cheers!

  • teknoid

    @primeminister

    He referred to Shell::initialize() that enables the

    $uses

    to work correctly.

    I haven’t tested prepending params with “-“, but if I find some way… I’ll let you know.

    Cheers ;)>

  • Pingback: CakePHP Digest #19 – The Holy Shit Do I Have A Ton Of Links Edition | PseudoCoder.com()

  • Still trying to extend my shell class with another base shell class but the initialize() method has stopped working: http://bin.cakephp.org/view/1834867989

  • Thanks to francky06l:
    … primeminister: seems before calling “initialize()”, there is a test is_a(“Shell”), but subShell is a “BaseSchell” not a shell … might be the reason

  • starchild

    Hi,
    Is it possible to read cache from a shell script?
    I used the above example to call my methods from my model
    but my cache::read does not get executed.

  • teknoid

    @starchild

    You should be able to.
    Try importing the cache lib first.

  • starchild

    Thanks for your response Teknoid,

    I tried that and also:
    App::import(‘Lib’, ‘cache’ . DS . ‘memcache’, false);
    I get Fatal error: Cannot redeclear class MemcacheEngine

    But when I ran Cache:settings()
    My Shell was using the default Cache:config ‘File’
    so I copied my custom Cache::config ‘Memcache’ to the
    Shell startup() method. Now when I run the Shell, I get:
    Fatal error: Call to member function get() on non-object.
    ————————————————————
    Ok what I am doing is setting up a view counter for products,
    colors, sizes. I use Cache::read() and Cache::increment()
    to acomplish my counts, using Ids as the $key. So far so good.

    Every 24hrs I fetch the values of all the $keys, set up an array
    and do a saveAll to a db table. Then do a Cache::delete() to
    reset my counter. It works as it suppose to if I call it from a
    controller action but needs to be called from a Shell so I can
    run a cronjob.

    #This is executed on the Shell
    function statDump($type, $keys = array()) {
    $conf = ‘view_cache’;
    Cache::config($conf, array(
    ‘prefix’ => ‘view-count-for-‘ . strtolower($type) .’-‘
    ));

    $build_data = array();
    $stat_dump = array();
    foreach ($keys as $key) {
    $build_data[‘foreign_key’] = $key;
    $build_data[‘type’] = $type;
    $build_data[‘views’] = Cache::read($key, $conf);
    if(!empty($build_data[‘views’])){
    $stat_dump[] = $build_data;
    }
    }
    $this->data[‘ViewStat’] = $stat_dump;
    if(!empty($this->data[‘ViewStat’])){

    if($this->saveAll($this->data[‘ViewStat’])){

    foreach ($keys as $key) {
    Cache::delete($key, $conf);
    }
    }
    }
    }

    #This is called by the Shell
    function productStats() {
    $productIds = $this->find(‘all’,
    array(‘fields’ => array(‘Product.id’), ‘contain’ => false));
    $keys = array();
    foreach($productIds as $id){
    $keys[] = $id[‘Product’][‘id’];
    }
    return $this->ViewStat->statDump(‘Product’, $keys);
    }

    #This is the Shell call
    function __processProductStats() {
    $this->out(__(‘Initializing product stat dump’, true));
    $this->Product->productStats();
    $this->out(__(‘product stat dump complete’, true));
    }

  • teknoid

    @starchild

    Sorry, that’s quite a bit of code to decipher in the blog. Based on the error Cache object should be available. And setting of file vs. memecache should come form the core.php

    I encourage to check in with the IRC channel or google group to get more answers.

    • starchild

      That’s ok Teknoid,

      I figured out that Shell is calling for the File engine
      even though I changed the ‘default’ config to ‘Memcache’

      I created a ticket @ the lighthouse.

      Thanks anyway. Your tutorials are always a big help.

%d bloggers like this: