技术目标
一些重要的数据需要记录关键信息的修改记录,在后期的相关审计工作中提供数据支持。
实现这个目标要从底层上进行管理,保证在任何逻辑中修改数据都能够被调用,Doctrine 中为 新增数据(postPersist)、更新数据前(preUpdate) 两个事件。
要点:
- 指定一个或多个表,每个表记录单独保存
- 只保存关键字段,防止日志表冗余数据太对,若所有关键字段值未修改时不保存记录
- 无感设计,非当前业务技术人员无需学习和调用组件
技术方案
数据库设计
原数据
App\Bundle\Entity\Price:
type: entity
repositoryClass: App\Bundle\Entity\PriceRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
beginDate:
type: date
nullable: true
endDate:
type: date
nullable: true
price:
type: decimal
precision: 20
scale: 10
quantity:
type: decimal
precision: 20
scale: 2
nullable: false
options:
default: 0
日志表数据库,新增时记录一个日志,修改了单价、截止日期或数量字段后保存修改前值和修改后值到数据库,如果本次保存没有修改单价或数量字段时跳过。
App\Bundle\Entity\PriceAudit:
type: entity
repositoryClass: App\Bundle\Entity\PriceAuditRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
priceId:
type: integer
nullable: true
options:
comment: 'Price ID'
name:
type: string
nullable: true
options:
comment: '名称'
createdAt:
type: datetime
nullable: true
options:
comment: '时间'
userId:
type: integer
nullable: true
options:
comment: 'User ID'
event:
type: string
length: 30
nullable: true
options:
comment: '事件 新增或更新'
priceOld:
type: decimal
precision: 20
scale: 3
nullable: true
options:
comment: '修改前价格'
priceNew:
type: decimal
precision: 20
scale: 3
nullable: true
options:
comment: '修改后价格'
endDateOld:
type: date
nullable: true
options:
comment: '修改前报价截止日期'
endDateNew:
type: date
nullable: true
options:
comment: '修改后报价截止日期'
quantityOld:
type: float
nullable: true
options:
comment: '修改前 起订量'
quantityNew:
type: float
nullable: true
options:
comment: '修改后 起订量'
lifecycleCallbacks: { }
监听事件
namespace App\Bundle\Listeners;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Symfony\Component\VarDumper\VarDumper;
use App\Bundle\Entity\Price;
use App\Bundle\Entity\PriceAudit;
// BasePrice Entity 事件,存储价格变更记录到审计日志表
class PriceEntityAuditListener
{
private $fields = ['endDate'];
private $fieldsNumber = ['price','quantity'];
private $audit = [];
public function postPersist(LifecycleEventArgs $args){
$entity = $args->getEntity();
if($entity instanceof BasePrice){
$isChange = false;
$basePriceAudit = $this->newAuditEntity($entity);
$basePriceAudit->setEvent('postPersist');
foreach ($this->fields as $field) {
$newFunc = "set".ucfirst($field)."New";
$getFunc = "get".ucfirst($field);
if(method_exists($basePriceAudit,$newFunc)){
$newValue = $entity->$getFunc();
if( is_object($newValue) and empty($newValue) == false ){
if(method_exists($newValue,'getId')){
$newValue = $newValue->getId();
}elseif(method_exists($newValue,'__toString')){
$newValue = $newValue->__toString();
}
}
$basePriceAudit->$newFunc( $newValue );
$isChange = true;
}
}
foreach ($this->fieldsNumber as $field) {
$newFunc = "set$field"."New";
$getFunc = "get".ucfirst($field);
if(method_exists($basePriceAudit,$newFunc)){
$basePriceAudit->$newFunc($entity->$getFunc());
$isChange = true;
}
}
if($isChange == true){
$this->audit[] = $basePriceAudit;
}
}
}
public function preUpdate(PreUpdateEventArgs $args) // OR LifecycleEventArgs
{
$entity = $args->getEntity();
if ( $entity instanceof BasePrice ) {
$isChange = false;
$priceAudit = $this->newAuditEntity($entity);
$priceAudit->setEvent('preUpdate');
if ( $entity->getId() ) {
foreach ($this->fields as $field) {
$oldFunc = "set".ucfirst($field)."Old";
$newFunc = "set".ucfirst($field)."New";
if(method_exists($priceAudit,$oldFunc) and method_exists($priceAudit,$newFunc)){
if ( $args->hasChangedField($field) ) {
$oldValue = $args->getOldValue($field);
$newValue = $args->getNewValue($field);
if($oldValue != $newValue){
if( is_object($oldValue) and empty($oldValue) == false ){
if(method_exists($oldValue,'getId')){
$oldValue = $oldValue->getId();
}elseif( method_exists($oldValue,'__toString') ){
$oldValue = $oldValue->__toString();
}
}
if( is_object($newValue) and empty($newValue) == false ){
if(method_exists($newValue,'getId')){
$newValue = $newValue->getId();
}elseif(method_exists($newValue,'__toString')){
$newValue = $newValue->__toString();
}
}
$priceAudit->$oldFunc( $oldValue );
$priceAudit->$newFunc( $newValue );
$isChange = true;
}
}
}
}
foreach ($this->fieldsNumber as $field) {
$oldFunc = "set$field"."Old";
$newFunc = "set$field"."New";
if(method_exists($priceAudit,$oldFunc) and method_exists($priceAudit,$newFunc)){
if ( $args->hasChangedField($field) and floatval($args->getOldValue($field)) != floatval($args->getNewValue($field))) {
$priceAudit->$oldFunc($args->getOldValue($field));
$priceAudit->$newFunc($args->getNewValue($field));
$isChange = true;
}
}
}
if($isChange == true){
$this->audit[] = $basePriceAudit;
}
}
}
}
public function postFlush(PostFlushEventArgs $args)
{
if ( empty($this->audit) == false ) {
$em = $args->getEntityManager();
foreach ($this->audit as $k=>$audit) {
unset($this->audit[$k]);
$em->persist($audit);
}
$em->flush();
}
}
private function newAuditEntity(BasePrice $entity){
$priceAudit = new PriceAudit();
$priceAudit->setBasePrice($entity->getId());
$priceAudit->setUserId($entity->getUpdateUser() ? $entity->getUpdateUser()->getId() : 0);
$priceAudit->setCreatedAt($entity->getUpdatedAt());
$priceAudit->setName($entity->getName());
return $priceAudit;
}
}
Service
services:
# ....
app.listeners.price_entity_audit_listener:
class: App\Bundle\Listeners\PriceEntityAuditListener
tags:
- { name: doctrine.event_listener, entity: 'App\Bundle\Entity\BasePrice', event: postPersist }
- { name: doctrine.event_listener, entity: 'App\Bundle\Entity\BasePrice', event: preUpdate }
- { name: doctrine.event_listener, entity: 'App\Bundle\Entity\BasePrice', event: postFlush }