« back

CakePHP: Running shells via CronJobs

Posted Dec 14th 2009, 14:03 by PaulGardner

Are you a developer using CakePHP based in the North East of England? If so, get in touch if you would be interested in being part of a group of developers who meet up once or twice a month to bounce ideas off one another.

The Console

The CakePHP Console allows you to access your MVC classes via a cron job or other command-line script.  If you have baked your models/controllers/views you have already used it in conjunction with the bake library kindly provided by the Cake Dev Team.

But did you know this is also the most secure way to periodically run scripts in your application? 

When you run the following from your command line

$ cd /my/cake/app_folder
$ ../cake/console/cake

you will see something like

Hello user,
 
Welcome to CakePHP v1.2 Console
---------------------------------------------------------------
Current Paths:
 -working: /path/to/cake/
 -root: /path/to/cake/
 -app: /path/to/cake/app/
 -core: /path/to/cake/
 
Changing Paths:
your working path should be the same as your application path
to change your path use the '-app' param.
Example: -app relative/path/to/myapp or -app /absolute/path/to/myapp
 
Available Shells:
 
 app/vendors/shells/:
         - none
 
 vendors/shells/:
         - none
 
 cake/console/libs/:
         acl
         api
         bake
         console
         extract
 
To run a command, type 'cake shell_name [args]'
To get help on a specific command, type 'cake shell_name help'

The last list of items shows the libraries shipped with Cake core.  This tutorial describes how to add a shell to the 'app/vendors/shells' list and call it from a cron job.

The Shell

In /app/vendors/shells create a file named alerts.php with the following code:

<?php
class AlertsShell extends Shell {

  function main() {
    ClassRegistry::init('Comment')->reminders();
  }

}
?>

All this file does is calls the reminders() function from a Comment model.  I am using the ClassRegistry::init() function here as I think it's cleaner (only calls the model when you need it and will only instantiate it once), but you could specify var $uses = array('Comment'); if you wanted.

The Model

In our model we now need to create the reminders() function:

function reminders() {
  // approved and unedited
  $data = $this->find('all', array(
    'conditions' => array(
      'Comment.status' => 'approved',
      'Comment.created = Comment.modified',
      'DATE_ADD(Comment.created, INTERVAL 3 HOUR) >=' => date('Y-m-d')
    )
  ));
  if(!empty($data)) {
    foreach($data AS $row):
      $approvedComments[] = "<a href='/admin/comments/index/".$row['BlogPost']['id']."#comment".$row['Comment']['id']."'>".$row['BlogPost']['title']." #".$row['Comment']['id']."</a>";
    endforeach;
    $saveData['Message'][] = array(
      'subject' => 'New auto-approved comments',
      'body' => '<p>Just a quick note to remind you that some auto-approved comments were added recently which if you have not already reviewed you should do so now.</p>
      <ul><li>'.join('</li><li>', $approvedComments).'</li></ul>'
    );
  }

  // pending
  $data = $this->find('all', array(
    'conditions' => array(
      'Comment.status' => 'pending'
    )
  ));
  if(!empty($data)) {
    foreach($data AS $row):
      $pendingComments[] = "<a href='/admin/comments/index/".$row['BlogPost']['id']."#comment".$row['Comment']['id']."'>".$row['BlogPost']['title']." #".$row['Comment']['id']."</a>";
    endforeach;
    $saveData['Message'][] = array(
      'subject' => 'Comments still pending',
      'body' => '<p>Just a quick note to remind you that you have some pending comments which you should review and approve or mark as spam.</p>
      <ul><li>'.join('</li><li>', $pendingComments).'</li></ul>'
    );
  }

  ClassRegistry::init('Message')->saveAll($saveData['Message'], array('atomic'=>false));
}

This is a function I am using to provide alert messages to a site owner who is using MilesJ's Commentia Behaviour.  When executed it:

  1. Checks for automatically approved comments which have not been edited
  2. Checks for any comments with a pending status

Once it has ran these checks and added any messages into a $saveData array the last line then uses ClassRegistry::init() to call Message->saveAll().

The Console Revisited

To test the above was working I then went back to me command-line and ran

/home/serverUser/domains/domain.org.uk/public_html/cake/console/cake -app "/home/serverUser/domains/domain.org.uk/public_html/app" alerts

You would have to alter the paths to match your server setup and where your app is located, but the above is what the cron job will run and if successful you should simply see:

Welcome to CakePHP v1.2.4.8284 Console
---------------------------------------------------------------
App : app
Path: /home/serverUser/domains/domain.org.uk/public_html/app
---------------------------------------------------------------

 

If there are any errors encoutered whilst running your shell they will be displayed on screen for you to deal with, something that is very difficult to see if you jump stright to running this from a cron job, so this step is worthwhile.

The Cron Job

The next task is to create a cron job, now I have Direct Admin on our server so this is an easy task, but whatever system you have access to you need to use it to create a cronjob similar too:

 

* */3 * * * /home/serverUser/domains/domain.org.uk/public_html/cake/console/cake -app "/home/serverUser/domains/domain.org.uk/public_html/app" alerts

This is nearly identical to the command I ran from the command-line to test my shell, but has 5 parameters at the start to set your minutes, hours, day of month, month, and day of week settings.  I have the second parameter set to */3 to say I want my cron job to run every 3 hours.

Finishing Off

And finally there are a few extra things which need attention before this would work for me.

  1. Change line #30 of /cake/console/cake file to include the full path to the php cli command
    30: exec /full_path/php -q ${LIB}cake.php -working "${APP}" "$@"
  2. Change the permissions of /cake/console/cake so it can be executed (is used 754)

And that should hopefully be it .. any questions? leave a comment below.

Tags: tutorial cakephp shell cronjob

4 Comments

  1. Feb 3rd, 18:13 by Akif

    Nice article, i didn't work untill i added the full path to the cake file as explained. For me it was: /usr/local/bin/php

    Thanks for the article!

  2. Feb 3rd, 18:25 by PaulGardner

    @Akif: Glad you could make sense of the article. I am new to writing tutorials so it's great to know that someone has found this useful.

  3. Feb 3rd, 20:09 by Akif

    Yeah keep up to good work, you will get more appriciation over time :)

    I have a question: is it possible to call an action from a specific controller from the shell? And if yes, would this break any rules/conventions? I have a sychronise action which is made and works, i would like to execute this in the cronjob...

    PS: whats your Twitter? Mine @aquive

  4. Feb 3rd, 20:49 by PaulGardner

    @akif: I have not found a way to call specific controller actions in this manner, but please do not take this is a definitive indication that it cannot be done as I am by no means a CakePHP expert.

    However, I would imagine this would break the MVC design pattern as controllers exist to pull data from your models using your business logic and then process that data to be passed to a view. As such I am unsure if it would be correct to call a controller action from a shell.

    What is it your trying to achieve that has led you to wanting to call a controller atcion from a shell?

    P.S. the fact you are now following me on twitter means you found the twitter link in my 'keep in touch' section, have followed you and made you the first person listed in my cakephp-lovers list (http://twitter.com/WebbedIT/cakephp-lovers)

Leave a comment

Why not leave a comment for the author and others to read?

Get In Touch

Keep In Touch

Twitter Updates

    follow us on Twitter