Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

symfony cron task logging example

cron task logging

Some time ago I wrote about a magnificent plugin managing cron task runtime, sfTaskLoggerPlugin and I promised to provide some real world examples of the plugin's usage. The time has come, so let's go on!

archiving outdated products

I'll start with an easy functionality which is executed each day using a cron task. ProductManager class is just a high-level model class, which is separated from ORM business logic, since it has nothing to do with Doctrine directly. The archiveProducts method fetches all products that meet some specific criteria (in this case - products that are not active no more). The first lines use the ORM-level layer to fetch Doctrine records. Then, all products are again checked against another criteria, inside a loop, and if any pass, they're permanently archived. The result of this method is an array of archived product IDs - this is very important data for the task logger. Take a look at the following code:

class ProductManager
{
  static public function archiveProducts()
  {
    $products = Doctrine::getTable('Product')
      ->findProductsQuery()
      ->execute();
 
    $result = array();
    foreach ($products as $product)
    {
      $availability_history = $product->getAvailabilityHistory()->getLast();
      if ($availability_history->shouldBeArchived())
      {
        $product->archive();
        $result[] = $product['id'];
      }
    }
    return $result;
  }
 
...

The model layer is done, now it's time to prepare the controller layer - and this is the main thing here. Let's start with implementing the command line symfony task. We create a lib/task/archiveProductsTask.class.php file:

<?php
 
require_once(dirname(__FILE__) . '/../../plugins/sfTaskLoggerPlugin/lib/task/sfBaseTaskLoggerTask.class.php');
 
class archiveProductsTask extends sfBaseTaskLoggerTask
{
}

We will put all necessary task stuff here. Note that our new custom task archiveProductsTask class extends the plugin sfBaseTaskLoggerTask class. And plugin sfBaseTaskLoggerTask class extends the base symfony sfBaseTask class (without sfTaskLoggerPlugin, a custom task would just directly extend sfBaseTask). Thanks to it, you may adapt the sfBaseTaskLoggerTask class to meet your needs, if you want some custom mechanisms to be available in all your custom tasks.

We define the constants that would be used to state the return code of a task run:

const ERROR_CODE_FAILURE = -1;
const ERROR_CODE_SUCCESS = 1;

Now let's define the symfony task standards - task name, namespace and descriptions, which are available in the command line:

protected function configure()
  {
    parent::configure();
 
    $this->namespace = 'cron';
    $this->name = 'archiveProducts';
    $this->briefDescription = 'Mark products as archived';
    $this->detailedDescription = <<<EOF
The [cron:archiveProducts|INFO] task finds all products that were
for a given amount of time and sets their status to archival. Call it
with:
 
  [php symfony cron:archiveProducts|INFO].
EOF;
  }

Having this class defined, you may run:

./symfony
in your project root directory to find, that a new command cron:archiveProducts is now available. Simply, run:
./symfony cron:archiveProducts

And here comes the task logger part of the task implementation. Just to remind - each time the cron:archiveProducts is executed, a single task_logger record is inserted into the database. This record holds all important information about the task runtime, these are:

  • task name
  • task arguments
  • task options
  • records processed
  • records not processed
  • start time
  • end time
  • task is still running
  • task has successfuly finished
  • error code
  • comment
and some more. Of course, you may modify it and do whatever you want (add new or remove existing information) - these are just the most basic information each task execution should log somewhere.

Let's implement the comment generator - you may either generate a plain text comment or even HTML code (if you want lists or links to be nicely displayed). You need to consider it yourself, since big amounts of HTML, generated every day or even more frequently, will use lots of disk space in MySQL, so that your database grows bigger and bigger each day. Think about that ;).

protected function generateComment($result)
  {
    $comment = '';
    $count = count($result);
    if ($count > 0)
    {
      $comment = 'Products changed into archival<ul>';
      foreach ($result as $product)
      {
        $comment .= '<li>'.$product.'</li>';
      }
      $comment .= '</ul>';
    }
    $this->task->setCountProcessed($count);
    return $comment;
  }

And finally, let's implement the rest of task logger logic. The most important function here is the doProcess. Everything is done inside the try-catch block. First, the model layer is called to do the data processing stuff. Once the data is processed, we may interpret the results. We generate the comments (basing on the result array), we set the error code and set the is_ok field if no exceptions were raised. If any problem was encountered, is_ok has to be set to false, using setNOk() method:

protected function doProcess($arguments = array(), $options = array())
  {
    try
    {
      $result = ProductManager::archiveProducts();
      $this->task->setComments($this->generateComment($result));
      $this->task->setErrorCode(self::ERROR_CODE_SUCCESS);
      $this->setOk();
    } catch (Exception $e)
    {
      $this->task->setErrorCode(self::ERROR_CODE_FAILURE);
      $this->setNOk($e);
    }
  }

Our new custom task class looks like this now:

require_once(dirname(__FILE__) . '/../../plugins/sfTaskLoggerPlugin/lib/task/sfBaseTaskLoggerTask.class.php');
 
class archiveProductsTask extends sfBaseTaskLoggerTask
{
  const ERROR_CODE_FAILURE = -1;
  const ERROR_CODE_SUCCESS = 1;
 
  protected function configure()
  {
    parent::configure();
 
    $this->namespace = 'cron';
    $this->name = 'archiveProducts';
    $this->briefDescription = 'Mark products as archived';
    $this->detailedDescription = <<<EOF
The [cron:archiveProducts|INFO] task finds all products that were
for a given amount of time and sets their status to archival. Call it
with:
 
  [php symfony cron:archiveProducts|INFO].
EOF;
  }
 
  protected function generateComment($result)
  {
    $comment = '';
    $count = count($result);
    if ($count > 0)
    {
      $comment = 'Produkty zmienione na archiwalne<ul>';
      foreach ($result as $product)
      {
        $comment .= '<li>'.$product.'</li>';
      }
      $comment .= '</ul>';
    }
    $this->task->setCountProcessed($count);
    return $comment;
  }
 
  protected function doProcess($arguments = array(), $options = array())
  {
    try
    {
      $result = ProductManager::archiveProducts();
      $this->task->setComments($this->generateComment($result));
      $this->task->setErrorCode(self::ERROR_CODE_SUCCESS);
      $this->setOk();
    } catch (Exception $e)
    {
      $this->task->setErrorCode(self::ERROR_CODE_FAILURE);
      $this->setNOk($e);
    }
  }
}

one example is enough. Think about the new possibilities

I'm not going to give dozens of similar examples of code implementation, since the task logger plugin logic would be mostly the same.

If you have a big symfony 1.x application, I strognly encourage you to install the sfTaskLoggerPlugin and redefine some of your task, so that they would be logged each time they're executed. After some time take a look at your task logs - you'll see how many information you can get:

  • analyse your task runtime performance
  • analyse how many times task have failed
  • analyse how many records are being processed
  • analyse what time the task processes the most data and what time there is no processed data
  • implement another mechanism to monitor all your task logs and alarm, when any task fails (check the is_running against is_ok task_logger record fields)
and so on. Give it a try! ;)

symfony event example

Symfony events


see this article in Polish (symfony event na przykładzie sfDoctrineGuardLoginHistoryPlugin)

In a nutshell, events are the mechanisms which connect implemented functionalities with kinda' label (just a chosen name), e.g. "xyz". During the script execution there goes a message: "attention, everyone! xyz!" - and right after that, all functionalities linked to this label are executed. All this is automatic, but you have to define (in a configuration file of the whole project or of a plugin) which events are linked with which functionalities.


For example, a customer submitting an order in an E-commerce application forces the following things to happen:

  • saving all order data in the database
  • sending an E-mail containing all important order data (including payment, shipment, etc.) to the customer
  • assigning the customer to a chosen customer group, depending on the type of assortment he has just bought
  • creating a reminder for the customer service in order to persuade the customer to buy something again - the reminder becomes active for a specific employee e.g. one month after the first order has been submitted
Thanks to this example, we can see how many different mechanisms can be triggered by performing an action. Now the question is - hot to implement it?
  1. of course, we can create elegant methods (in OOP style) and call them each time when dealing with a customer that submits an order
  2. or we can use our custom events to handle this: first we define our events, we link them to corresponding methods in appropriate classes - and finally, in each place where the customer submits the order, we register the event (what will trigger the rest)


In my opinion, the biggest advantage of the second solution is the convenience and flexibility. After we've implemented our custom events and methods, triggering the entire mechanism in a new place in the code is just one line ("attention, everyone! xyz!"). Moreover, we gain the clearness of the code, seen from the top level of the entire project (especially when executing one action has to trigger many other actions to be executed - and this happens very often in big projects). The first solution - which is just a chain of method calls, one after another - can make the businness logic very sophisticated and difficult to understand at a glance. And it will surely make the business logic harder to improve. And finally, each event is some kind of a gate that leads you to anything else in the project. It's just like placing a door somewhere in the wall - you can enter the room very fast and easily, otherwise you would need to ruin the wall to get in.


events in sfDoctrineGuardLoginHistoryPlugin


Symfony has lots of built-in events (symfony 1.4 docs). One of them, user.change_authentication - has been used in sfDoctrineGuardLoginHistoryPlugin. The event is connected to a custom method in the config/sfDoctrineGuardLoginHistoryPluginConfiguration configuration file:

public function initialize()
  {
    $this->dispatcher->connect('user.change_authentication', array('UserLoginHistoryTable', 'writeLoginHistory'));
  }

According to the docs, an event is notified (it takes place) whenever the user authentication status changes. This event is connected with only one method, UserLoginHistoryTable::writeLoginHistory(), which saves login/logout entry in the database for any user:

static public function writeLoginHistory(sfEvent $event)
  {
    $sessionUser = $event->getSubject();
    $params = $event->getParameters();
    if(true === $params['authenticated'])
    {
      $userId = $sessionUser->getGuardUser()->id;
      $sessionUser->setAttribute('user_id', $userId, 'sfDoctrineGuardLoginHistoryPlugin');
      self::createHistoryEntry('login', $userId);
    }
    else
    {
      $userId = $sessionUser->getAttributeHolder()->remove('user_id', null, 'sfDoctrineGuardLoginHistoryPlugin');
      self::createHistoryEntry('logout', $userId);
    }
  }
  protected static function createHistoryEntry($state, $userId)
  {
    $history = new UserLoginHistory();
    $history->state = $state;
    $history->user_id = $userId;
    $history->ip = getenv('HTTP_X_FORWARDED_FOR') ? getenv('HTTP_X_FORWARDED_FOR') : getenv('REMOTE_ADDR');
    $history->save();
  }


As a result, we don't have to dig deep into the sfDoctrineGuardPlugin and modify the code to call the method above inside login/logout mechanisms. In fact, we don't have to know anything about . The only thing we need to know is that the user.change_authentication event is notified during user login/logout and we connect our method with this event - and everything works fine. Easy, fast and effective!


Predefined symfony events are some kind of an interface we can use very easily, as we can see in the plugin above (symfony provides us with many doors we can open with no need to destroy the walls to get in). It's a good idea to create custom events in bigger projects and connect them to the mechanisms. Then, enabling/disabling a chosen functionality is just a matter of adding/removing on line of code.

Command line SVN editor

For those of you who still use Subversion as the software versioning system, setting SVN_EDITOR option will be important sooner or later.

SVN properties such as svn:externals or svn:ignore need to be set manually by the developer. To do this, run svn propedit or svn propset command (IDEs such as NetBeans have built-in SVN support which does the same stuff). If you're working in the command line, run one of the following commands:

export SVN_EDITOR=vim
or
export SVN_EDITOR=nano
or
export SVN_EDITOR=mcedit (or whatever)

This sets the SVN editor for the current user session. If you want to store this option permanently (to be set whenever you log in to your machine), create/edit the ~/.bash_profile file and put the same content there.