Symfony3.4 Dcotrine 如何自动保存修改记录

技术目标

一些重要的数据需要记录关键信息的修改记录,在后期的相关审计工作中提供数据支持。

实现这个目标要从底层上进行管理,保证在任何逻辑中修改数据都能够被调用,Doctrine 中为 新增数据(postPersist)、更新数据前(preUpdate) 两个事件。

要点:

  1. 指定一个或多个表,每个表记录单独保存
  2. 只保存关键字段,防止日志表冗余数据太对,若所有关键字段值未修改时不保存记录
  3. 无感设计,非当前业务技术人员无需学习和调用组件

技术方案

数据库设计

原数据

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 }

发表评论