Полное логирование изменений в системе
В некоторых проектах критически важно знать изменения, которые производят пользователи системы. Важно знать не просто о том, что произошло изменение, а конкретно кто, когда и что именно изменил.
В этом примере рассмотрим реализацию подобного функционала. Сразу обратим ваше внимание, что изменения будут сохраняться только при работе с 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 – массив, содержащий только измененные поля (ключ имя поля).
Инструментарий логирования готов. Далее при помощи дизайнера можно разработать интерфейс упрощающий работу с этими данными.