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…

%d bloggers like this: