Zf2 Doctrine 2 Log Every Entity Change

September 18, 2015

In this article I will show a simple way to log every change to doctrine 2 entity.
This one will not be copy / paste. Just check out the code and help yourself.

1. Create event service

/**
 * @developer Ivan Gospodinow
 */

namespace Application\Service;

use Application\Entity\DbLogEntity;
use Application\Entity\AbstractEntity;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use \DateTime;

class DoctrineLogChangesService
{

    /**
     * Hold the changes
     * @var type
     */
    protected static $rows = [];
    protected $sm;
    protected $disabled = [
        //'Application\Entity\UserEntity',
    ];

    public function __construct($sm)
    {
        $this->sm = $sm;
        $sm->get(‘Application’)
                ->getEventManager()
                ->attach(‘finish ’, [$this, 'flush']);
    }

    public function flush()
    {
        $orm = $this->sm->get(‘doctrine.entitymanager.orm_default’);

        foreach (self::$rows as $row) {
            $orm->persist($row);
        }
        if (!empty(self::$rows)) {
            $orm->flush();
        }
    }

    public function getUnitOfWork()
    {
        return $this->sm->get(‘doctrine.entitymanager.orm_default’)->getUnitOfWork();
    }

    public function getEntityTableName(LifecycleEventArgs $args)
    {
        $meta = $args->getObjectManager()->getClassMetadata(get_class($args->getObject()));
        return $meta->table['name'];
    }

    public function getEntityFields(LifecycleEventArgs $args)
    {
        $meta = $args->getObjectManager()->getClassMetadata(get_class($args->getObject()));
        $fields = [];
        foreach ($meta->fieldMappings as $field) {
            $fields[$field['fieldName']] = $field['columnName'];
        }

        foreach ($meta->associationMappings as $field) {
            if (isset($field['sourceToTargetKeyColumns'])) {
                $fields[$field['fieldName']] = key($field['sourceToTargetKeyColumns']);
            }
        }

        return $fields;
    }

    /**
     * Adds all initial fields
     * @param LifecycleEventArgs $args
     * @return type
     */
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();
        if (in_array(get_class($entity), $this->disabled)) {
            return;
        }

        $tableName = $this->getEntityTableName($args);
        $fields = $this->getEntityFields($args);

        foreach ($fields as $field => $column) {
            $value = call_user_func_array([$entity, 'get' . ucfirst($field)], []);
            $row = [
                'user' => $this->sm->get('User')->getId(),
                'table' => $tableName,
                'pk' => $entity->getId(),
                'column' => $column,
                'oldValue' => null,
                'newValue' => $value instanceof AbstractEntity ? $value->getId() : $value
            ];
            $log = new DbLogEntity();
            $log->exchangeArray($row);
            self::$rows[] = $log;
        }
    }

    /**
     * Add only new changes
     * @param LifecycleEventArgs $args
     * @return type
     */
    public function preUpdate(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();
        if (in_array(get_class($entity), $this->disabled) || !$entity->canUseLifecycleEvents()) {
            return;
        }

        $tableName = $this->getEntityTableName($args);
        $fields = $this->getEntityFields($args);
        $set = $this->getUnitOfWork()->getEntityChangeSet($entity);
        foreach ($set as $property => $changes) {
            $oldValue = $changes[0];
            $newValue = $changes[1];
            if ($changes[0] instanceof AbstractEntity || $changes[1] instanceof AbstractEntity) {
                if ($changes[0] instanceof AbstractEntity) {
                    $oldValue = $changes[0]->getId();
                }
                if ($changes[1] instanceof AbstractEntity) {
                    $newValue = $changes[1]->getId();
                }
            }

            if ($oldValue == $newValue) {
                continue;
            }

            $row = [
                ‘user’ => $this->sm->get(‘User’)->getId(),
                ‘ table’ => $tableName,
                ‘ pk’ => $entity->getId(),
                ‘ column’ => $fields[$property],
                ‘ oldValue’ => $this->getStringValue($oldValue),
                ‘ newValue’ => $this->getStringValue($newValue)
            ];

            $log = new DbLogEntity();
            $log->exchangeArray($row);
            self::$rows[] = $log;
        }
    }

    public function getStringValue($value)
    {
        if (is_scalar($value)) {
            return $value;
        }

        if ($value instanceof DateTime) {
            return $value->format('Y-m-d H:i:s');
        } elseif (is_array($value)) {
            return serialize($value);
        }

        return $value;
    }

}

2. Add the event service to Doctrine 2

/**
 * @developer Ivan Gospodinow
 */

namespace Application;

use Zend\Mvc\MvcEvent;

class Module
{

    public function onBootstrap(MvcEvent $e)
    {
        $application = $e->getApplication();
        $sm = $application->getServiceManager();
        $doctrineEntityManager = $sm->get('doctrine.entitymanager.orm_default');
        $doctrineEventManager = $doctrineEntityManager->getEventManager();
        $doctrineEventManager->addEventListener(
            [
                \Doctrine\ORM\Events::postPersist,
                \Doctrine\ORM\Events::preUpdate,
            ],
            new Service\DoctrineLogChangesService($sm)
        );
    }
}

3. DbLogEntity

/**
 * @developer Ivan Gospodinow
 */

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;
use \Exception;

/**
 * @ORM\Table(name="__dblog")
 * @ORM\Entity()
 */
class DbLogEntity
{

    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", name="id")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", name="user_id")
     */
    protected $user;

    /**
     * @ORM\Column(type="string", name="table_name")
     */
    protected $table;

    /**
     * @ORM\Column(type="integer", name="pk")
     */
    protected $pk;

    /**
     * @ORM\Column(type="string", name="column_name")
     */
    protected $column;

    /**
     * @ORM\Column(type="string", name="old_value")
     */
    protected $oldValue;

    /**
     * @ORM\Column(type="string", name="new_value")
     */
    protected $newValue;

    /**
     * @ORM\Column(type="datetime", name="timestamp")
     */
    protected $timestamp;

}

Simple as that.
Suggestions or problems ? Write a comment.

tags: , , ,
posted in Doctrine 2, how to ?, Zend Framework 2 by Ivan Gospodinow

Follow comments via the RSS Feed | Leave a comment | Trackback URL

Leave Your Comment

 
 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org