I've been building a bunch of Sync code for a CRM and custom website recently and needed a quick and efficient way to track what entities/fields actually got changed. This lead me to google to see if anyone else had a clever solution to this and it seems everyone is using Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity) or building a custom event listener. Both of these solutions aren't great so I instead found another method of accomplishing this.

Symfony has a built in serializer component that we can use to convert entities into arrays. We can then use these arrays to detect if any changes occurred on the entity. Here is an example:

// clone the entity before we apply changes to fields
$originalSubscriptionEntity = clone $entity;

// apply the changes we want to do
$entity->setName("Test Name");

// convert both old and new version of entity to arrays and compare the arrays
$originalEntityData = $this->serializer->normalize($originalSubscriptionEntity, null);
$newEntityData = $this->serializer->normalize($entity, null);

foreach($newEntityData as $key => $value) {
    if(array_key_exists($key, $originalEntityData) || $value === $originalEntityData[$key]) {
        continue;
    }

    // detected changes

    break;
}
Detecting entity changes

This way you can easily access what fields got changed or if the entity got changed at all. This is especially handy for debugging and has helped me find issues with my CRM sync code.

I hope others find this useful as I didn't find anyone else doing something similar online. I saw some really bad answers that are converting the entities to JSON and then converting the JSON into an array. That is very inefficient and should definitely be avoided. This is why we instead use the normalize() function.

\DateTime objects always show as changed

I ran into this issue using the above fix. The problem is that currently Symfony's Serializer will change timezones on \DateTime objects before converting it to a string. You can fix this by specifying what timezone to use when calling the normalize function:

$defaultTimezone = (new \DateTime())->getTimezone();
$originalEntityData = $this->serializer->normalize($originalSubscriptionEntity, null, [
    DateTimeNormalizer::TIMEZONE_KEY => $defaultTimezone
]);
Setting timezone for Symfony's Serializer

After applying this fix my \DateTime objects now convert correctly.

Ignoring Attributes

You can easily set what fields to skip normalizing. You can set fields to ignore by either using annotations on the entity or by using the $context normalize() argument. See the Serializer docs for more information.

Feedback

Did this help you out or did I make a mistake somewhere? Have a better way of doing things? Feel free to leave a comment below. I enjoy hearing back from my readers :)