Полное логирование изменений в системе

В некоторых проектах критически важно знать изменения, которые производят пользователи системы. Важно знать не просто о том, что произошло изменение, а конкретно кто, когда и что именно изменил.

В этом примере рассмотрим реализацию подобного функционала. Сразу обратим ваше внимание, что изменения будут сохраняться только при работе с Db_Object. Изменения, которые внесены вручную, обрабатываться не будут.

Создадим словарь операций с объектом, назовем action:

  • create;
  • update;
  • delete.

Создадим объект ORM, который будет хранить историю всех изменений, object_state:

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

Добавим индексы object_name, datetime, object_id:

Создадим модель, которая будет добавлять записи в лог:

/**
* Модель для лога состояний объектов
*/
class Model_Object_State extends Model
{
/**
* Словарь типов операций
* @var Dictionary
*/
private $_stateDictionary = false;

/**
* Получить словарь типов операций
* @return Dictionary
*/
private function getStateDictionary()
{
   if(!$this--->_stateDictionary)
      $this->_stateDictionary = Dictionary::getInstance('action');

   return $this->_stateDictionary;
}
/**
* Сохранить состояние объекта
* @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();
   // проверяем, существует ли такая операция
   if(!$d->isValidKey($operation)){
       $this->logError('Invalid operation name "'.$operation.'"');
       return false;
   }
   // проверяем, существует ли такой тип объектов
  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;
  }
 }
}

Осталось определиться в какой момент сохранять изменения. Самое удобное для этого место - триггеры Db_Object_Storage. Там есть события onAfterAdd, onAfterDelete и новое событие (dvelum 0.9.3) onAfterUpdateBeforeCommit, которое возникает после того как данные объекта сохранены в базу, но еще не применены к объекту методом (commit). Это удобный момент, чтобы узнать, что было и что стало с объектом.

Поскольку мы будем следить за изменениями всех объектов, то обработчики событий внесем прямо в system/app/Trigger.php. Доопределим существующие обработчики, чтобы для объектов использующих логирование изменений, происходила запись состава данных:

...
 	 	 	
public function onAfterAdd(Db_Object $object)
{
    // Если объект хранит историю изменений (в настройках ORM  выставлено History Log)
   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())
	 );
   }

   // оставляем код, который был написан ранее
   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
	       );
	 }
           // оставляем код, который был написан ранее
	if(!$this->_cache)
	        return;
	$this->_cache->remove($this->_getItemCacheKey($object));
}

...

Таким образом, при создании нового объекта в истории будет храниться:

  • action – create;
  • before – null;
  • after - массив со всеми данными полей (ключ имя поля).

При удалении объекта:

  • action – delete;
  • before - массив со всеми данными полей (ключ имя поля);
  • after – null.

При обновлении объекта:

  • action – update;
  • before - массив со всеми данными полей (ключ имя поля);
  • after – массив, содержащий только измененные поля (ключ имя поля).

Инструментарий логирования готов. Далее при помощи дизайнера можно разработать интерфейс упрощающий работу с этими данными.