Monday, April 29, 2013

Yii Active Record Events

I am skipping ahead a bit, but I just worked out an important detail that I want to capture. CActiveRecord pre-defines some events that you can use to save yourself some time and effort. The documentation tells that they are there, but doesn't really tell how to use them. These events are:

onBeforeValidation - in CModel - executes just before the validation rules are processed - can be used to stop save
onAfterValidation - in CModel - executes just after the validation rules are processed - can be used to stop save, but it is harder.
onBeforeSave - executes after validation but before the record is saved - can be used to stop save
onAfterSave - executes after the record is saved
onBeforeDelete - executes before the record is deleted, can be used to stop deletion
onAfterDelete - executes after the record is deleted
onBeforeFind - executes before the find command is executed
onAfterFind - executes after each found record is instantiated
onAfterConstruct - executes after a new model is constructed

I'm not going to go through all  these to tell you what they are useful for. I will leave that to you. But I will show you how to enable one of them (the rest will work the same way), and give you a bit of code that I am using it for.

I was asked to put some audit fields on each of my files to track create date and time, create user, last changed date and time, and last change user. Ordinarily it would take the same bit of code in each of my models to populate these audit fields. That code isn't all that complex. In fact here it is:

   if ($this->isNewRecord) {
      $this->create_user = Yii::app()->user->name;
      $this->create_time = new CDbExpression('current_timestamp');
   } else {
      $this->change_user = Yii::app()->user->name;
      $this->change_time = new CDbExpression('current_timestamp');
   }

This is not a large piece of code, but if things change, and they decide that they want the name of the function that called it or something else, I have to go back and change a lot of code. Instead, I can wrap this in an event handler and call it when the BeforeSave event is raised. So, I created a class in Components named Audit which has a single static method:

class Audit
{
   public static function beforeSaveHandler($event)
   {
      $model = $event->sender;
      if ($model->isNewRecord) {
         $model->create_user = Yii::app()->user->name;
         $model->create_time = new CDbExpression('current_timestamp');
      } else {
         $model->change_user = Yii::app()->user->name;
         $model->change_time = new CDbExpression('current_timestamp');
      }
   }
}

I still have to attach the handler to each model, but that is easy. It is attached in the init() method of the model. If you don't have one, just create it. It will override the init() method from CActiveRecord.

   public function init()
   { 
      parent::init();
      $this->onBeforeSave = array(new Audit, 'beforeSaveHandler');
   }

You do not really have to call the CActiveRecord::init() function because it does nothing, but I do it anyway just in case the developers of Yii put something there in the future.

So there you have it, a simple beforeSave event handler that populates audit fields in a table. You can add as many event handlers as you want to that init() function. I'm not sure what order they will be executed in, I suspect in the order that they are added. But, I wouldn't count on that as it could change. I also wouldn't write dependencies into the handlers as this will likely add unnecessary complexity to your application.

Let me know what you think.

No comments:

Post a Comment