Menu

Bob van de Vijver

Daily business

Creating automatic changesets using only Doctrine

The Doctrine-project houses several PHP libraries which are primarily focused on database storage and object mapping, and it is being used as default ORM/DBAL (Object Relation Mapper/Database Abstraction Layer) in for example Symfony 2 and above. Doctrine eases your Symfony project by abstracting the database connection for you by simply given you access to objects that represent the data in your database. Next to that, it is build in such a way that you can easily extend the functionality. One way of doing that is hooking into the emitted events that can happen during the object lifecycle. But how would you use said events to automatically create changeset events?

Actually, it is quite simple! All you'll need to do is hook into a single event: onFlush. This event is called every time your will flush the Entity Manager state, and will contain information about all database actions that will be executed (in basis those are insert, update and delete). When you are using Symfony, you simply create a private service that subscribes to the Doctrine onFlush event:

wesp_admin_bundle.event.remove_event:
  class: FooBundle\Events\EventSubscriber\EventSubscriber
  public: false
  tags:
    - { name: doctrine.event_subscriber, connection: default }

The event subscriber class will have the following basic content, which allows you to handle new, updated and deleted content:

<?php

namespace FooBundle\Events\EventSubscriber;

use Doctrine\Common\EventSubscriber as BaseEventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;

class EventSubscriber implements BaseEventSubscriber
{

  /**
   * Returns an array of events this subscriber wants to listen to.
   *
   * @return array
   */
  public function getSubscribedEvents()
  {
    return array(
        'onFlush',
    );
  }

  /**
   * Handler for the onFlush event
   *
   * @param OnFlushEventArgs $args
   */
  public function onFlush(OnFlushEventArgs $args)
  {
    $em  = $args->getEntityManager();
    $uow = $em->getUnitOfWork();

    // Loop new entities
    foreach ($uow->getScheduledEntityInsertions() as $insertedEntity) {
    }

    // Loop edited entities
    foreach ($uow->getScheduledEntityUpdates() as $updatedEntity) {
    }

    // Loop deleted entities
    foreach ($uow->getScheduledObjectDeletions() as $deletedEntity) {
    }
  }
}

This method can be used to implement for example blameable behavior, but there is already a bundle available that can do that for you (which uses this method internally): Gedmo Doctrine Extension Bundle. However, we're going to save the changes made in the entities as events in our database in order to let an external tool use them for further processing outside of the Symfony project.

To order to do this, we need to have create, update and delete events: the exact implementation can be as complicated as you would like (several event classes for every separate event), but here I will use a simple example. I will be saving the changes in array form, which will be serialized by Doctrine before saving it into the database. This can be done as the data is a simple array without to complex information: if you need more configuration possibilities, I suggest to use JMS Serializer to serialize the data into for example JSON. Our simple event class looks like this (I've excluded the getters/setters from the example):

<? php

namespace FooBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Event
 *
 * @ORM\Table(name="event")
 * @ORM\Entity(repositoryClass="FooBundle\Repository\EventRepository")
 */
class Event
{
  /**
   * @var int
   *
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @var string
   *
   * @ORM\Column(name="event_type", type="string", length="10")
   */
  private $eventType;

  /**
   * @var array
   *
   * @ORM\Column(name="event_data", type="array")
   */
  private $eventData;
}

In the Event entity we save the event type (insert/update/delete) and the changeset in the data field in case of an update event. This means that the insert and delete event are pretty straight forward as we do not generate the changeset data. For the updated entity we can actually ask the Unit Of Work for its changes! In this example I only want to store the changed fields, which is accomplished by selecting only the keys of the array returned by Doctrine.

// Loop new entities
foreach ($uow->getScheduledEntityInsertions() as $insertedEntity) {
  $event = new Event()
  $event->setEventType('insert');
}

// Loop edited entities
foreach ($uow->getScheduledEntityUpdates() as $updatedEntity) {
  $event = new Event()
  $event->setEventType('update');
  $event->setEventData(array_keys($uow->getEntityChangeSet($updatedEntity)));
}

// Loop deleted entities
foreach ($uow->getScheduledObjectDeletions() as $deletedEntity) {
  $event = new Event()
  $event->setEventType('delete');
}

The tricky part is to save the newly generated entity while we're in the onFlush event listener which you called to flush the entity manager. We will need to persist the new entity and tell the Unit Of Work to recompute it state in order to really insert the new event Entity. However, this is relatively easy if you use the method below (make sure to only call it after you've completely filled the Event object):

/**
 * Persist the event
 *
 * @param EntityManager $em
 * @param Event         $event
 *
 * ComputeChangeSet is internal, but may be used here according to documentation
 * https://github.com/doctrine/doctrine2/blob/master/docs/en/reference/events.rst#onflush
 */
private function persistEvent(EntityManager $em, Event $event)
{
  $meta = $em->getClassMetadata(Event::class);
  $em->persist($event);
  $em->getUnitOfWork()->computeChangeSet($meta, $event);
}

And that should be it! With the code examples above you should already be capable to create events for every Entity insert/update/deletion in your Symfony application (or any other application that uses Doctrine, although the service definition will probably differ). 

Written by Bob van de Vijver on Sunday November 19, 2017

« Symfony 2: Extending the Security Component - Apache Ignite: Open-source next generation in-memory computing »