Symfony events and persistence

Despite the fact that ORMs are quite evil, often we cannot escape this burden because the decision about using or not using an ORM was taken by someone else on the team, maybe way before we could affect this decision somehow. Well, ORMs are, of course, not always a good thing to have, but misusing them is even worse.

I’m using Symfony, but other advanced ORMs such as Hibernate in Java or Entity framework in C# support ORM events as well, so the ideas I’m sharing here should be generally applicable to these platforms as well.

After some digging on StackOverflow, I’ve noticed a common pattern with ORMs and persistence events these ORMs provide: people tend to attach their listeners to entity lifecycle events and implement business rules there. Here’s an example from Symfony’s DoctrineBundle documentation:

namespace AppBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Product;

class SearchIndexer
{
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $entityManager = $args->getEntityManager();

        // perhaps you only want to act on some "Product" entity
        if ($entity instanceof Product) {
            // ... do something with the Product
        }
    }
}

I came to a conclusion this is quite a bad practice. Lifecycle events, such as prePersist, postPersist, etc. should not be used for business logic. It’s tempting to push some dependencies into an event listener and modify some entity fields or even create some other entities and persist those as well, but that’s not how ORM event listeners should be used.

So, why exactly using ORM events for business logic is wrong? #

Well, for starters, do you treat your ORM as a simple implementation detail? Do you want your code to be coupled with the ORM, or would you rather abstract the ORM away with some kind of bridge so it does not dictate how you should structure your own code? I actually think that not a single good coder actually wants to get their code coupled to vendor libraries, at least to some extent.

Imagine for example if you’ll need to switch from Doctrine (or Hibernate, or Entity Framework) to some other ORM: if you had your domain logic kept in a Doctrine listener, you’d have to migrate it as well. And this is definitely going to be a rewrite, and not an easy one: who says all the ORM implementations provide compatible event handler implementations?

Next, at least with Doctrine or Entity Framework, an event listener is not quite aware of the objects it’s dealing with. The prePersist method usually receives a general-purpose event that should contain a list of entities that were affected by the event. This means that if you’re aiming for an entity class, you need to check all of the ORM events to check if they’re relevant to the case. So this definitely should affect the overall performance. Imagine you have a lot of listeners, and on each persist call you have to run all of them to figure out if any of those need to run some logic.

By adding these event listeners, you also introduce coupling. Notice the entity matching implementation:

use AppBundle\Entity\Product;
// ...
// in postPersist():
if ($entity instanceof Product) { 

We’re checking that entity is an instance of Product here. OK, what if we want to rename the class at some point? Right, I know you have all those nice IDEs for that. And what if we want to introduce a new entity related to Product, which now is the one that needs to be processed here by this event listener?

You can think of any scenario you may want to implement here, but nevertheless, you cannot remove coupling the listener to your entity classes.

The reason of this all is simple. ORM is not something you should couple your code to. When you’re implementing ORM event listeners for business logic, you’re getting into specifics instead of going abstract. Instead of ORM events, go with domain-wide events instead, and handle those at the application level, not the ORM level.

 
8
Kudos
 
8
Kudos

Now read this

Why Propel is bad

There’s always the need to abstract out our data persistence mechanisms so that we don’t have to mess with mapping user data to tables by hand and handling the relations between user data entities. I’ve been always using Doctrine ORM in... Continue →