Workflow Events - Part 2 - Custom Audit Trail Listener


In an earlier video in this series we looked at the AuditTrailListener. This is a class provided by the Workflow component that logs out a few of the events dispatched during any particular transition.

In the previous video we covered on the variety of events that are dispatched during a transition. If we hadn't already taken a sneaky peek inside the code in that video, with our new knowledge we could now deduce that the AuditTrailListener likely works by listening for these very generic events - leave, enter, transition - for the most generic / top level events, workflow.leave, for example.

This AuditTrailListener is really useful. It shows us a possibility, and is generic enough to be useful in any Symfony framework-based application that makes use of the Workflow component. If you are using the workflow component in your own project, you must make sure to pass in your own event dispatcher, or this section just won't work.

Anyway, whilst having this generic listener is useful, very much more likely is your requirements for logging specific events in your application. Actually, whether you choose to hook up a logger, or react to events with other code is not super important. All we are going to cover initially is the process.

First, we need to create a new class that is compatible with how Symfony's event dispatcher expects to work. For this, we will create a new CustomAuditTrailListener:

<?php

// /src/AppBundle/Event/Subscriber/CustomAuditTrailListener.php

namespace AppBundle\Event\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CustomAuditTrailListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [

        ];
    }
}

That's the shell. But it's not very interesting, and it's not hooked up so it doesn't even do anything.

All that's important here is that we implement EventSubscriberInterface, which dictates that we must implement the static method of getSubscribedEvents.

This expects us to return an array, where the key is the name of the event we want to subscribe too (hear about), and the value is either:

  • a string that is the name of a method that will be called in this class
  • or, an array of strings and their priorities - higher priorities being called earlier

We don't need to bother about passing an array, but if that interests you, check out the documentation for more details and an example of doing so.

Let's create a service definition:

# /app/config/services.yml

services:
    audit_trail_listener:
        class: AppBundle\Event\Subscriber\CustomAuditTrailListener
        tags:
            - { name: kernel.event_subscriber }

Cool, so our CustomAuditTrailListener is now registered with the event dispatcher. This means when interesting events are dispatched, our subscriber is going to hear about them.

Now, let's add in our first event subscription:

<?php

// /src/AppBundle/Event/Subscriber/CustomAuditTrailListener.php

namespace AppBundle\Event\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CustomAuditTrailListener implements EventSubscriberInterface
{
    public function onLeave(Event $event)
    {
        // do something interesting
    }

    public static function getSubscribedEvents()
    {
        return [
            'workflow.customer_signup.leave' => 'onLeave',
        ];
    }
}

As covered in the previous video, whenever we leave a place, at the very least, three events will be dispatched:

  • workflow.leave
  • workflow.[workflow_name].leave
  • workflow.[workflow_name].leave.[place_name]

Our onLeave method will be called whenever objects (subject) traveling through our workflow leave any place.

Let's hook up Monolog to record a new log entry each time this occurs.

First, we will need to pass in Monolog via the constructor. Monolog implements LoggerInterface, so we will make our code that tiny bit more modular by telling our code that we will be injecting something that implements LoggerInterface, rather than tying ourselves directly to Monolog. It's a small improvement sir, but it checks out.

# /app/config/services.yml

services:
    audit_trail_listener:
        class: AppBundle\Event\Subscriber\CustomAuditTrailListener
        arguments:
            - "@logger"
        tags:
            - { name: kernel.event_subscriber }

So that's the service entry updated. We can now update the constructor, and then log something out:

<?php

// /src/AppBundle/Event/Subscriber/CustomAuditTrailListener.php

namespace AppBundle\Event\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Psr\Log\LoggerInterface;

class CustomAuditTrailListener implements EventSubscriberInterface
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onLeave(Event $event)
    {
        $this->logger->debug('on leave', [
            'marking - places' => $event->getMarking()->getPlaces(),
            'transition' => $event->getTransition()->getName(),
        ]);
    }

    public static function getSubscribedEvents()
    {
        return [
            'workflow.customer_signup.leave' => 'onLeave',

            // with only a slight variation on this concept 
            // we could start to do more interesting things...
            // 'workflow.customer_signup.announce.approve_vip' => 'onAnnounceApproveVIP',
        ];
    }
}

At this point we have effectively re-implemented the basic functionality of the AuditTrailListener.

If that was all we needed to do at this point then we could - and probably should - have stuck with the AuditTrailListener.

However, now we control the implementation we can modify it to do whatever we need.

We could inject the entity manager and make further changes to the subject as dictated by our business rules. My advice would be stay clear of changing the marking information outside of the workflow though.

We could inject a different workflow and kick start an entirely different definition depending on where we are in our current workflow. And pretty much anything else besides.

Code For This Course

Get the code for this course.

Episodes