Case Study: Getting a Complete System Change Log

For some projects it is crucial to know what changes are made by system users. Not just the sheer fact that a change has been introduced is of importance, but specifically what was changed, when and by whom.

In this workshop we will look into implementing such a functionality. Please note that only DB_Object changes will be logged. Manual updates are not recorded.

Create a dictionary of object actions and name it ‘action’:

  • create;
  • update;
  • delete.

Create an ORM object logging all changes, object_state:

  • object_id bigint unsigned;
  • object_name varchar 255;
  • datetime datetime;
  • user_id link (user);
  • before – longtex;
  • after – longtext;
  • action - dictionary (action).

Add the following indices: object_name, datetime, object_id.

Create a model adding records to the log:


/**
* Model logging object states
*/
class Model_Object_State extends Model
{
/**
* Dictionary of action types
* @var Dictionary
*/
private $_stateDictionary = false;

/**
* Get dictionary of action types
* @return Dictionary
*/
private function getStateDictionary()
{
   if(!$this->_stateDictionary)
      $this->_stateDictionary = Dictionary::getInstance('action');

   return $this->_stateDictionary;
}
/**
* Save object state
* @param string $operation
* @param string $objectName
* @param integer $objectId
* @param integer $userId
* @param string $date
* @param string $before
* @param string $after
* @return integer | false
*/
public function saveState($operation , $objectName , $objectId , $userId , $date, $before = null , $after = null)
{
 $d = $this->getStateDictionary();

   // check if the action exists
   if(!$d->isValidKey($operation)){
	$this->logError('Invalid operation name "'.$operation.'"');
   return false;
   }
   // check if the object type exists
  if(!Db_Object_Config::configExists($objectName)){
      $this->logError('Invalid object name "'.$objectName.'"');
     return false;
  }

   try{
		$o = new Db_Object('Object_State');
		$o->setValues(array(
'action'=>$operation,
		 'object_name'=>$objectName,
		 'object_id'=>$objectId,
		 'user_id'=>$userId,
		 'datetime'=>$date,
		 'before'=>$before,
		 'after'=>$after
		));

		$id = $o->save(false , false);
		if(!$id)
			throw new Exception('Cannot save object state ' . $objectName . '::' . $objectId);

		return $id;
  }catch (Exception $e){
		$this->logError($e->getMessage());
		return false;
  }
 }
}

Now, it is time to define at what moment changes will be logged. Db_Object_storage triggers best suit this purpose. There are onAfterAdd, onAfterDelete events, as well as the new onAfterUpdateBeforeCommit event (added in DVelum 0.9.3), which comes into action when object data has been saved to the database, but the change has not yet been committed to the object. It is that very moment when we can learn both the previous and the new state of the object.

As we are inclined to monitor all object changes, insert event handlers directly to the system/app/Trigger.php. Extend existing handlers to record logged object data and add the following source:


...
 	 	 	
public function onAfterAdd(Db_Object $object)
{
    // If object history is logged (History Log enabled n ORM settings) 
   if($object->getConfig()->hasHistory())
   {
	 Model::factory('Object_State')->saveState(
	   'create' ,
	   $object->getName() ,
	   $object->getId() ,
	   User::getInstance()->id,
	   date('Y-m-d H:i:s'),
	   null ,
	   serialize($object->getData())
	 );
   }

// leave the existing source unchanged
   if(!$this->_cache)
       return;
   $this->_cache->remove($this->_getItemCacheKey($object));
}

...

public function onAfterUpdateBeforeCommit(Db_Object $object)
{
    if($object->getConfig()->hasHistory())
    {
	 	Model::factory('Object_State')->saveState(
	 	   'update' ,
	 	   $object->getName() ,
	 	   $object->getId() ,
	 	   User::getInstance()->id,
	 	   date('Y-m-d H:i:s'),
	 	   serialize($object->getData()),
	 	   serialize($object->getUpdates())
	 	);
     }
}

...

public function onAfterDelete(Db_Object $object)
{
	 if($object->getConfig()->hasHistory())
	 {
	          Model::factory('Object_State')->saveState(
	                'delete' ,
	                $object->getName() ,
	                $object->getId() ,
	                User::getInstance()->id,
	                date('Y-m-d H:i:s'),
	                serialize($object->getData()),
	                null
	       );
	 }
           // leave the existing source unchanged
	if(!$this->_cache)
	        return;
	$this->_cache->remove($this->_getItemCacheKey($object));
}

...

Thus, once a new object is created, its history log will include:

  • action - create;
  • before - null;
  • after - an array with all field data (field name is the key).

When deleting an object:

  • action – delete;
  • before - an array with all field data (field name is the key);
  • after – null.

When updating an object:

  • action – update;
  • before - an array with all field data (field name is the key);
  • after – an array with updated fields only (field name is the key).

The logging functionality is ready to use. Now, using the Layout Designer, you can create an interface that makes working with the data easier.