Tracking attribute changes

Hi there,

I’m writing a kind of activity feed, like the Facebook feed. I have a model (User) and would like to track whenever some information changes like ($user->name = ‘new value’). The optimal solution provides both the former and the later value of $user->name.

I thought I could just implement "public function setName", but it seems that db fields have precedence, which I think, is a very bad decision as CActiveRecord should be the layer between DB and logic, not just a forwarder.

My second thought was a “beforeSetAttribute”, but such an event doesn’t seems to exist. So I created a class called “ActiveRecord” to extend from with these functions:




/**

	 * PHP setter magic method.

	 * This method is overridden so that AR attributes can be accessed like properties.

	 * @param string $name property name

	 * @param mixed $value property value

	 */

    public function __set($name,$value)	{

        if ($this->beforeSetAttribute($name,$value)) {

    		parent::__set($name,$value);

            $this->afterSetAttribute($name,$value);

        }

	}

    

    /*public function setAttribute($name,$value) {

        if ($this->beforeSetAttribute()) {

            if (parent::setAttribute($name,$value))) {

                $this->afterSetAttribute();

                return true;

            }

		}

		return false;

    }*/

    

    /**

	 * This event is raised before an attribute is set.

	 * By setting {@link CModelEvent::isValid} to be false, the normal {@link setAttribute()} process will be stopped.

	 * @param CModelEvent $event the event parameter

	 */

	public function onBeforeSetAttribute($event) {

		$this->raiseEvent('onBeforeSetAttribute',$event);

	}


	/**

	 * This event is raised after an attribute is set.

	 * @param CEvent $event the event parameter

	 */

	public function onAfterSetAttribute($event) {

		$this->raiseEvent('onAfterSetAttribute',$event);

	}

    

    /**

	 * This method is invoked before setting an attribute.

	 * The default implementation raises the {@link onBeforeSetAttribute} event.

	 * You may override this method to do any preparation work for setting an attribute.

	 * Make sure you call the parent implementation so that the event is raised properly.

	 * @return boolean whether the attribute should be updated. Defaults to true.

	 */

	protected function beforeSetAttribute($name,$value) {

		if ($this->hasEventHandler('onBeforeSetAttribute')) {

			$event = new CModelEvent($this);

            $event->params = array('name' => $name, 'value' => $value);

			$this->onBeforeSetAttribute($event);

			return $event->isValid;

		} else {

			return true;

        }

	}


	/**

	 * This method is invoked after setting an attribute successfully.

	 * The default implementation raises the {@link onAfterSetAttribute} event.

	 * You may override this method to do postprocessing after setting an attribute.

	 * Make sure you call the parent implementation so that the event is raised properly.

	 */

	protected function afterSetAttribute($name,$value) {

		if ($this->hasEventHandler('onAfterSetAttribute')) {

			$event = new CModelEvent($this);

            $event->params = array('name' => $name, 'value' => $value);

            $this->onAfterSetAttribute($event);

        }

	}



What do you think? How will this affect performance? Can it be done in a better way? Should something similar be implemented in Yii?

Check out the AuditTrail extension:

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

Very nice!

But I don’t like the idea of storing all old values, it seems like a potentially massive performance hit, especially for tables like user tables with a lot of columns.

There exist a more basic extension for tracking attribute changes - the name escapes me for the moment! that you can tweak for your own use.

I’ll let you know when I find it.

If you don’t find it before I do. :)

Here it is:

ActiveRecordLogableBehavior

I am using a modified version for my own project to good effect.

Since I only use it in one model, I just moved the behavior code into the model class itself.

It’s very efficient, especially if you filter out all the attributes you’re not interested in. :)

Check out this code - my Issue model:

Hooks:

/protected/models/Issue.php#cl-73

Helper function:

/protected/models/Issue.php#cl-381

Another Helper:

/protected/models/Issue.php#cl-413

And - finally - the hideous code which actually builds the actionLog:

/protected/models/Issue.php#cl-525

Not saying that it’s great, but it really does do the job.

Hopefully you’ll learn something - if not anything but how not to program in PHP. :lol:

Thanks for your effort.

My plan is to build a behavior like ActiveRecordLogableBehavior, but then attach it dynamically in the controllers were i want changes to be logged. My problem is now; why isn’t there a “onEnable” event were i can save the old values? Can I use __construct in the behavior class?

Override the attach() method in your behavior, and save old values there.

phtamas, just what I needed, thanks.