Implementing Cron jobs with Yii and CConsoleCommand

A site I’m currently building requires a bunch of Cron jobs running daily to bring in and update various bits of data. It’s the first time I’ve needed to do this within the Yii framework, and it was a useful experience figuring out how it works. The Yii documentation located at http://www.yiiframework.com/wiki/91/implementing-cron-jobs-with-yii/ isn’t completely clear, so hopefully this post will help.

Firstly, forget about browser emulation, the best way to implement Cron jobs in Yii is by using Yii’s Console Application (CConsoleCommand) functions.

Essentially this is a separate instance of your application that can be run from the command line rather than as a publicly executable php script.

The first thing to do is to create a new entry or index script – which looks a lot like the index.php file in your public_html or root directory. However, for extra security your new index script can be (and probably should be) placed outside of the public root so you can only access it through the command line.

I called my script cron.php and dropped it on to the server below public_html. The file will look something like this:

// change the following paths if necessary $yii=dirname(__FILE__).'/framework/yii.php'; $config=dirname(__FILE__).'/public_html/protected/config/cron.php'; // remove the following lines when in production mode defined('YII_DEBUG') or define('YII_DEBUG',true); // specify how many levels of call stack should be shown in each log message defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3); require_once($yii); $app = Yii::createConsoleApplication($config)->run();

Two things to notice here. Firstly the $config information comes from a new config file I’ve called cron.php. This is very much like your main.php config file, except it contains a lot less stuff:

return array(         'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',         'name'=>'Cron',         'preload'=>array('log'),          'import'=>array(                 'application.components.*',                 'application.models.*',         ),         // application components         'components'=>array(                 'db'=>array(                         'connectionString' => 'mysql:host=localhost;dbname=db_name',                         'emulatePrepare' => true,                         'username' => 'root',                         'password' => '',                         'charset' => 'utf8',                         'enableProfiling' => true,                 ),                 'log'=>array(                         'class'=>'CLogRouter',                         'routes'=>array(                                 array(                                         'class'=>'CFileLogRoute',                                         'logFile'=>'cron.log',                                         'levels'=>'error, warning',                                 ),                                 array(                                         'class'=>'CFileLogRoute',                                         'logFile'=>'cron_trace.log',                                         'levels'=>'trace',                                 ),                         ),                 ),                 'functions'=>array(                         'class'=>'application.extensions.functions.Functions',                 ),         ), );

Secondly, you’re running Yii’s createConsoleApplication method, rather than firing up the web application.

Once you’ve done that, the next thing is to understand what this createConsoleApplication()->run(); whatsit is doing.

When this function is fired, it looks in /protected/commands for commands to run. A command takes the form of a Class file of the name YourFunctionCommand.php – yes, you must suffix it with Command.php.

Within this Class file you need something like

class VisitorsCommand extends CConsoleCommand {         public function run($args)         {            // Do stuff         } }

So this file is called VisitorsCommand.php and the class name is VisitorsCommand – get it?

Now, if you were to fire up your command line interface and run something along these lines:

/usr/bin/php /Users/james/Dropbox/Sites/mySiteRoot/cron.php

You’ll get a message saying

The following commands are available:  - visitors

So now you can do

/usr/bin/php /Users/james/Dropbox/Sites/mySiteRoot/cron.php visitors

And Yii will instantiate your Visitors Class and fire the run() method.

Also… you will notice the run() method accepts a parameter – I’ve called it $args, for the sake of argument.

You can pass parameters into the run method like this

/usr/bin/php /Users/james/Dropbox/Sites/mySiteRoot/cron.php visitors param1 param2 param3

And they’ll end up in your run method as an array. You could for example write one Command class that can perform a variety of functions, or pass a key to validate the user of the Cron.

Within your new Command Class, you can access all your Models as normal to perform whatever functions you require.

Have fun!

Yii extensions – Where to put site-wide functions?

One aspect of Yii that’s confused me in the past is where to put generic site-wide functions that you want to access throughout the application. Stuff like an encryption method or a hash generator, that don’t belong to a specific model.

The answer, of course, is to use Yii’s system of Extensions. Some reading through the documentation (http://www.yiiframework.com/doc/guide/1.1/en/extension.create) revealed that what I was looking for was an ‘Application component’ which extends Yii’s existing CApplicationComponent and is then available using

Yii::app()->yourExtension->yourMethod();

Firstly, create a new directory within protected/extensions called ‘functions’.

Then, in there, create a new php file called ‘Functions.php’

This file is your extension class, but it must extend CApplicationComponent:

<?php class Functions extends CApplicationComponent {     public function returnSomething()     {         return 'Something';     } } ?>

Now go to protected/config/main.php and add the reference to your class in the ‘components’ array:

'components'=>array(     'functions'=>array(                         'class'=>'application.extensions.functions.Functions',                 ),     ),

Now, all being well you can access your functions using:

<?php print Yii::app()->functions->returnSomething(); ?>

Adding conditions to Yii model relations

Something I realised today is the power of adding conditions to model relations. This allows you to specify which records are returned as part of the relationship, which therefore allows you to set up several relationships for different situations.

for example:

'users' => array(self::MANY_MANY, 'User', 'user2project(project_id, user_id)', 'condition' => 'accepted = 1'),

This only brings back users who have accepted an invitation.

I can then also set up a new relation, called ‘invitedUsers’ where the condition is the opposite.

This allows me to access the users easily:

$project = Project::model()->findByPk(1); $users = $project->users; $invitedUsers = $project->invitedUsers;

Joining tables – using ‘with’ and ‘together’

Today we had a problem where a findAll() with criteria wasn’t bringing back expected results.

Yii appeared to be bringing back some results, but then as we looped through the CActiveRecord objects and attempted to access their relations, it became clear that because we didn’t have all the information we needed, Yii was almost ‘filling in the blanks’ and adding more records into our results – even records that weren’t possible given the original $CDbCriteria object passed into the findAll().

After a bit of head scratching and research, we worked out our query was missing one crucial element – the ‘together’ method. This guy makes sure that all the related tables requested in the ‘with’ method, or in the CDbCriteria are included and used properly:

$criteria = new CDbCriteria; $criteria->condition = //.... etc // Be sure to use ->with('joined_tables') ... // and ->together() to make it all work! $categories = Category::model()->with('products','products.images')->together()->findAll($criteria);

This gave us our correct array of CActiveRecord objects, with all the correct relations that we were then able to loop through, problem free.

Sorting an array of CActiveRecord Objects

So today I had a need to retrieve and order an array of CActiveRecord objects before looping and printing.

However, I knew I already had the objects in my $model object and the thought occurred that it must be possible to order the relations within this model, rather than perform a new CActiveRecord call with an order clause.

After a bit of reading from the Yii docs (http://www.yiiframework.com/doc/guide/1.1/en/database.arr#dynamic-relational-query-options), I realised that this was indeed possible.

You can perform dynamic relational queries on an existing CActiveRecord object, so in my case – ordering:

// Get a category, which comes with all its relations: $category = Category::model()->findByPk($id); // the 'default' order of products would be: $products = $category->products; // But we can pass an array of query options, such as order: $products = $category->products(array('order' => 'order_column ASC'));

We could also have other query options:

$products = $category->products(array('condition' => 'active=1')); $products = $category->products(array('condition' => 'product_id IN (1,2,3)')); // etc, etc...

Very handy!