How to log changes of ActiveRecords?

17 followers

A simple and effective way to keep track what your users are doing within your application is to log their activities related to database modifications. You can log whenever a record was inserted, changed or deleted, and also when and by which user this was done. For a CActiveRecord Model you could use a behavior for this purpose. This way you will be able to add log functionality to ActiveRecords very easily.

First of all you have to create a table for the log-lines in the database. Here is an example (MySQL):

CREATE TABLE ActiveRecordLog (
  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  description VARCHAR(255) NULL,
  action VARCHAR(20) NULL,
  model VARCHAR(45) NULL,
  idModel INTEGER UNSIGNED NULL,
  field VARCHAR(45) NULL,
  creationdate TIMESTAMP NOT NULL,
  userid VARCHAR(45) NULL,
  PRIMARY KEY(id)
)
TYPE=InnoDB;

The next step would be to create the corresponding Model using the YII Shell Tool:

model ActiveRecordLog

If you would like to have basic crud functionality created, you should also type:

crud ActiveRecordLog

To be able to log the changes we will use a behavior class. So you have to create one (i.e. ActiveRecordLogableBehavior) and store it somewhere in your application directory (i.e. \protected\behaviors). The behavior class should extend CActiveRecordBehavior as we want to work with ActiveRecords here.

class ActiveRecordLogableBehavior extends CActiveRecordBehavior
{
    private $_oldattributes = array();
 
    public function afterSave($event)
    {
        if (!$this->Owner->isNewRecord) {
 
            // new attributes
            $newattributes = $this->Owner->getAttributes();
            $oldattributes = $this->getOldAttributes();
 
            // compare old and new
            foreach ($newattributes as $name => $value) {
                if (!empty($oldattributes)) {
                    $old = $oldattributes[$name];
                } else {
                    $old = '';
                }
 
                if ($value != $old) {
                    //$changes = $name . ' ('.$old.') => ('.$value.'), ';
 
                    $log=new ActiveRecordLog;
                    $log->description=  'User ' . Yii::app()->user->Name 
                                            . ' changed ' . $name . ' for ' 
                                            . get_class($this->Owner) 
                                            . '[' . $this->Owner->getPrimaryKey() .'].';
                    $log->action=       'CHANGE';
                    $log->model=        get_class($this->Owner);
                    $log->idModel=      $this->Owner->getPrimaryKey();
                    $log->field=        $name;
                    $log->creationdate= new CDbExpression('NOW()');
                    $log->userid=       Yii::app()->user->id;
                    $log->save();
                }
            }
        } else {
            $log=new ActiveRecordLog;
            $log->description=  'User ' . Yii::app()->user->Name 
                                    . ' created ' . get_class($this->Owner) 
                                    . '[' . $this->Owner->getPrimaryKey() .'].';
            $log->action=       'CREATE';
            $log->model=        get_class($this->Owner);
            $log->idModel=      $this->Owner->getPrimaryKey();
            $log->field=        '';
            $log->creationdate= new CDbExpression('NOW()');
            $log->userid=       Yii::app()->user->id;
            $log->save();
        }
    }
 
    public function afterDelete($event)
    {
        $log=new ActiveRecordLog;
        $log->description=  'User ' . Yii::app()->user->Name . ' deleted ' 
                                . get_class($this->Owner) 
                                . '[' . $this->Owner->getPrimaryKey() .'].';
        $log->action=       'DELETE';
        $log->model=        get_class($this->Owner);
        $log->idModel=      $this->Owner->getPrimaryKey();
        $log->field=        '';
        $log->creationdate= new CDbExpression('NOW()');
        $log->userid=       Yii::app()->user->id;
        $log->save();
    }
 
    public function afterFind($event)
    {
        // Save old values
        $this->setOldAttributes($this->Owner->getAttributes());
    }
 
    public function getOldAttributes()
    {
        return $this->_oldattributes;
    }
 
    public function setOldAttributes($value)
    {
        $this->_oldattributes=$value;
    }
}

The behavior class uses the ActiveRecordLog Model to store the log lines into the database. It will log a line each time a record is inserted or deleted. It will also log a line for each field which is changed.

In order to make an ActiveRecord Model use this behavior, you have to add the following code to the Model class:

public function behaviors()
{
    return array(
        // Classname => path to Class
        'ActiveRecordLogableBehavior'=>
            'application.behaviors.ActiveRecordLogableBehavior',
    );
}

Of course this simple example could be enhanced:

  • support for mult-column primary keys
  • savethe attributeLabels instead of the field names
  • make description customizable
  • and so on...

Links ΒΆ

Total 2 comments

#4653 report it
MadSkillsTisdale at 2011/07/31 09:58pm
I made this into an extension so it is accessible to everyone

I turned this tutorial into an extension. I included migrations for table creation, and I have modified it to include an admin widget and made it a little more extensible. If anyone is interested, please check it out here:

http://www.yiiframework.com/extension/audittrail

Also, thank you so much to pfth for writing this wiki in the first place.

#3830 report it
roysarango at 2011/05/12 01:41pm
Awesome!

Thanks for this great extension and for the detailed explanations also :) I just modified to accomplish a client's requirement. I used it for saving changes on one specific model (Historical Info). Fields in main model are identical to the historical model except by id and date, those 2 were added in the latter (not present in the former) for obvious reasons. Here's my code... I hope it's useful for someone.

<?php
class HistorialPaciente extends CActiveRecordBehavior
{
    private $_oldattributes = array();
    public function afterSave($event)
    {
        if (!$this->Owner->isNewRecord) {
 
            // new attributes
            $newattributes = $this->Owner->getAttributes();
            $oldattributes = $this->getOldAttributes();
 
            $log=new CambioPaciente;
            $cont = 0;
            // compare old and new
            foreach ($newattributes as $name => $value) {
                if (!empty($oldattributes)) {
                    $old = $oldattributes[$name];
                } else {
                    $old = '';
                }
 
                if ($value != $old) {
                    $cont = $cont + 1;
                    $log->$name = $old;
                }
            }
            if ($cont <> 0)
                $log->fecha = time();
                $log->save();
        } 
    }
 
 
    public function afterFind($event)
    {
        $this->setOldAttributes($this->Owner->getAttributes());
    }
 
    public function getOldAttributes()
    {
        return $this->_oldattributes;
    }
 
    public function setOldAttributes($value)
    {
        $this->_oldattributes=$value;
    }
}
?>

Leave a comment

Please to leave your comment.

Write new article
  • Written by: pfth
  • Updated by: camac
  • Category: Tutorials
  • Votes: +12
  • Viewed: 6,586 times
  • Created on: Feb 13, 2009
  • Last updated: Oct 19, 2011
  • Tags: Logging