[EXTENSION] AuditTrail

Thanks for the tips. I have the typos in the migrations updated, sorry for that! I’m on the fence about the controller issue, though. I was trying to make the module just sort of “work” and look like the rest of your app. If I make it use its own controller, then you have to figure out a way to make it easy to modify without making someone edit the module code. I just made AuditTrailController check the config file for its variables and then set defaults if there is nothing in the config for menu, breadcrumbs, and layout. Does that sound like a good compromise? If so I can bundle and release a minor version

Yes, it may be the way.

Also, you can use getter methods in your behavior instead of relying on magic, ie:


$this->getOwner()

in place of


$this->Owner

as using the method directly is a bit faster

I’m using this extension now and extended it to suit my needs. Feel free to use my enhanced version and merge it with main extension if you wish.

Here’s what I did:

  • Refactored code

  • Added possibility to ignore attributes from trailing

  • Added possibility to log model creation only

  • Added property to set time format

  • Added possibility to save timestamp instead of formatted time

  • Added possibility to use model’s field instead of user id (this is useful when you have no specific users in your application, but there are more important models, to which other ones are bound)

The only modified file for now is LoggableBehavior.php:

[spoiler]




/**

 * Allow models to leave audit trail behind

 */

class LoggableBehavior extends CActiveRecordBehavior

{

	/**

	 * @var array Don't create trails for these model attributes

	 */

	public $ignore = array('created_at', 'updated_at');


	/**

	 * @var bool If to log model updates

	 */

	public $logUpdates = true;


	/**

	 * @var string Time format for audit trail records

	 */

	public $timeFormat = 'Y-m-d H:i:s';


	/**

	 * @var bool If to save timestamp instead of formatted date

	 */

	public $saveTimestamp = false;


	/**

	 * @var string Use specific model attribute to identify change author.

	 * This property is useful when you have no specific users in your application,

	 * but there are more important models, to which other ones are bound.

	 * Current user's id is used if this is set to null.

	 */

	public $userAttribute = null;




	/**

	 * @var array Record attributes to compare to on save

	 */

	private $_oldAttributes = array();




	/**

	 * Audit model creation or modification

	 * @param $event

	 * @return mixed

	 */

	public function afterSave($event)

	{

		$attributes = $this->getOwner()->getAttributes();

		if ($this->getOwner()->getIsNewRecord())

		{

			$this->leaveTrail('CREATE');

			$this->auditAttributes($attributes);

		}

		elseif ($this->logUpdates)

		{

			$this->auditAttributes($attributes, $this->_oldAttributes);

		}


		$this->setOldAttributes($attributes);

		return parent::afterSave($event);

	}




	/**

	 * Audit model removal

	 * @param $event

	 * @return mixed

	 */

	public function afterDelete($event)

	{

		$this->leaveTrail('DELETE');

		return parent::afterDelete($event);

	}




	/**

	 * Saves model attributes so we can compare them on save

	 * @param $event

	 * @return mixed

	 */

	public function afterFind($event)

	{

		$this->setOldAttributes($this->getOwner()->getAttributes());

		return parent::afterFind($event);

	}




	/**

	 * @return array Old model attributes

	 */

	public function getOldAttributes()

	{

		return $this->_oldAttributes;

	}




	/**

	 * Save old model attributes

	 * @param array $value

	 */

	public function setOldAttributes(array $value)

	{

		$this->_oldAttributes = $value;

	}




	/**

	 * Create audit trail for model's attributes

	 * @param array $attributes

	 * @param array $oldAttributes

	 */

	protected function auditAttributes($attributes, $oldAttributes = array())

	{

		if (count(array_diff($attributes, $oldAttributes)) == 0)

			return;


		foreach ($attributes as $name => $value)

		{

			if (!in_array($name, $this->ignore))

			{

				if (!empty($oldAttributes))

				{

					$oldValue = $oldAttributes[$name];

					$action = 'CHANGE';

				}

				else

				{

					$oldValue = '';

					$action = 'SET';

				}


				if ($value != $oldValue)

				{

					$this->leaveTrail($action, $value, $oldValue, $name);

				}

			}

		}

	}




	/**

	 * Create new audit trail with provided params

	 * @param string $action

	 * @param mixed $value

	 * @param mixed $oldValue

	 * @param string $fieldName

	 * @return boolean

	 */

	protected function leaveTrail($action, $value = '', $oldValue = '', $fieldName = '')

	{

		$log = new AuditTrail();

		$log->old_value = $oldValue;

		$log->new_value = $value;

		$log->action = $action;

		$log->model = get_class($this->getOwner());

		$log->model_id = $this->getNormalizedPk();

		$log->field = $fieldName;

		$log->stamp = $this->saveTimestamp ? time() : date($this->timeFormat);

		$log->user_id = $this->getUserId();

		return $log->save();

	}




	/**

	 * @return string Table's primary key as plain string.

	 * Encodes complex keys using JSON.

	 */

	protected function getNormalizedPk()

	{

		$pk = $this->getOwner()->getPrimaryKey();

		return is_array($pk) ? json_encode($pk) : $pk;

	}




	/**

	 * @return string Identifier to use in audit trails

	 */

	protected function getUserId()

	{

		if (isset($this->userAttribute))

		{

			$data = $this->getOwner()->getAttributes();

			return isset($data[$this->userAttribute]) ? $data[$this->userAttribute] : '';

		}

		else

		{

			try

			{

				$userId = Yii::app()->user->id;

				return empty($userId) ? '' : $userId;

			}

			catch (Exception $e)

			{

				return '';

			}

		}

	}

}



[/spoiler]

P.S.: Can you publish the extension on some code hosting site like GitHub so we can help you enhance the extension?

it 's true that there are some errors in the original version , i agree with put the extension to github or google code , thus we can make it better :lol:

I have issue , Event afterDelete is not work. How I fix?

GitHub is up!

Beware - This is not a stable version! I’m used to svn, so I got the core code up fine. I ran into some errors I didn’t understand when trying to create a branch for the nonstable copy, so I just committed everything to master. In any event, this is what is there:

Van Damm’s code is integrated into LoggableBehavior, but with a few changes. I do not like the configuration values inside of the behavior, so I moved them to the module. The behavior gets them from the module as needed, but this way they can be set up in the main config file. This will make updating to later versions of the code easier since you don’t have to go back and reset defaults inside of any core objects that would be overwritten by the new files.

I still can’t get PHPUnit running on my freaking laptop. Every time I try I wind up wasting most of the day. I should have just hand tested instead, but I don’t have time now. Please check it out and let me know what you think!

I think that you still need to be able to configure the behavior individually for different models even if default values are retrieved from module configuration. This is needed because different models can require different logging options.

Maybe I don’t understand enough about what you were thinking. Were you thinking that each time you instantiate a model you would set the criteria then? Maybe a code or pseudocode sample would help me understand how this would happen? I can change the code accordingly once I see ow you were thinking to use it

You need to attach the behavior to each model you want it to log:




public function behaviors()

{

  return array(

    'LoggableBehavior' => 'application.modules.auditTrail.behaviors.LoggableBehavior',

  );

}

So you may need to apply different logging settings to different model classes.

For example, you want changes to User models to be logged using timestamps instead of textual date representation and changes to Comment models to ignore ‘modification_date’ field:




class User extends ActiveRecord 

{

  // ...

  public function behaviors()

  {

    return array(

      'LoggableBehavior'=> array(

        'class' => 'application.modules.auditTrail.behaviors.LoggableBehavior',

        'saveTimestamp' => true

      );

    );

  }

  // ...

}


class Comment extends ActiveRecord 

{

  // ...

  public function behaviors()

  {

    return array(

      'LoggableBehavior'=> array(

        'class' => 'application.modules.auditTrail.behaviors.LoggableBehavior',

        'ignore' => array('modification_date')

      );

    );

  }

  // ...

}

What I mean here is that the developer needs to be able to customize behavior settings for each model class it is being attached to.

I use extension and have issue. When I delete but audit does not work but create and update do work. I was checked bug I know afterDelete doesn’t work. Why is it and How do fix?

Hi,

I love this extension. Have you considered normalising the database schema into something more on the lines of this?

AuditLog

All the best.

hey guyz…i can’t seam to make the modul work. I read all the info in this thread and no luck. The error i get is this:


CException     Alias "auditTrail.AuditTrailModule" is invalid. Make sure it points to an existing PHP file.

i copied the ziped content in modules dir and modified the main.php fiel with the import and modules but stil no working.

Can anyone help me?!

This is cool. I haven’t seen this before. Thanks for showing it to me! Van Damm, any words of wisdom on this? I think you probably have dealt with the limitations of the current module more than I have, and I am curious to know your take on this link.

Can you post the full section of your config files where you import audit trail, as well as let us know what file is giving you the error? Seems like we can probably solve this easily enough.

@ MadSkillsTisdale it workes now

My only problem now is that i don’t know how to modify it so it showes the names instead of the id’s:(

For example i have a computer table that has multiple colums of with half are id’s that are in relation to other tables. how do i enhance the behavior file so that it saves in the log table the names asociated to the id’s. Anyone has a clue?

You wouldn’t save the names associated, you would modify the display in the widget configuration to look up the foreign rows and display the fields you want at display time. The widget accepts configuration like CDataGrid does, so you would change the display the same way. Does that make sense?

yes i thought @ that but how does he know what table i make reference to?! Do youu have a small example?!

A good job~

The migration doesn’t work for a mysql database. I edited the migration as this:




$this->createTable( 'tbl_audit_trail',

	array(

		'id' => 'pk',

		'old_value' => 'text',

		'new_value' => 'text',

		'action' => 'string NOT NULL',

		'model' => 'string NOT NULL', // added the string keyword

		'field' => 'string NOT NULL', // added the string keyword

		'stamp' => 'datetime NOT NULL',

		'user_id' => 'string',

		'model_id' => 'string NOT NULL',

			)

);



And I commented out the lines where the migration adds the indexes on old_value and new_value. I think they’re not needed (sorting on those won’t happen a lot I think), and mysql doesn’t support searching in TEXT data without a maximum size.

I also think user_id should be an integer (in my case it is)

Thanks for the extension, simple, handy, and it works (once I patched the mySQL schema).

I wanted to restrict the Audit to just the important fields.

In case anyone else wants to do the same, I hacked as follows:

modules/auditTrail/behaviours/LoggableBehavior.php

37-: if ($value != $old ) {

37+: if ($value != $old & (!empty($this->Owner->auditFields) ? in_array($name,$this->Owner->auditFields) : true) ) {

65+: if ( (!empty($this->Owner->auditFields) ? in_array($name,$this->Owner->auditFields) : true) ) {

76+: }

And in my models where I don’t want to audit every field, after including the behaviours, I added:

public $auditFields = array(‘id’,‘code’,‘name’,‘status’);

I’m a bit rusty on PHP and new to Yii - there are probably neater and more standard ways of doing this but it works as is. :slight_smile: