dmytrof/doctrine-modification-events-bundle
Install the Bundle:
composer require dmytrof/doctrine-modification-events-bundle
Enable it in config/bundles.php:
return [
// ...
Dmytrof\DoctrineModificationEventsBundle\DmytrofDoctrineModificationEventsBundle::class => ['all' => true],
];
First Use Case: Audit Logging Create a listener service to log entity changes:
php bin/console make:event-listener AuditLogger
Update the generated listener (src/EventListener/AuditLogger.php):
namespace App\EventListener;
use Dmytrof\DoctrineModificationEventsBundle\Event\ModificationEvent;
use Psr\Log\LoggerInterface;
class AuditLogger
{
public function __construct(private LoggerInterface $logger)
{
}
public function onEntityUpdate(ModificationEvent $event): void
{
$entity = $event->getEntity();
$changes = $event->getChanges();
$this->logger->info(
sprintf('Entity %s updated. Changes: %s', get_class($entity), json_encode($changes))
);
}
}
Configure the Listener
Add the listener to config/packages/dmytrof_doctrine_modification_events.yaml:
dmytrof_doctrine_modification_events:
listeners:
App\Entity\User: # Target entity
- on: [postUpdate] # Event type
method: onEntityUpdate # Listener method
service: App\EventListener\AuditLogger
Test It
Update a User entity and check logs:
$user = $entityManager->getRepository(User::class)->find(1);
$user->setEmail('[email protected]');
$entityManager->flush();
Verify the log entry appears.
Entity-Specific Listeners Target specific entities with granular control:
dmytrof_doctrine_modification_events:
listeners:
App\Entity\Product:
- on: [preUpdate, postUpdate]
method: validatePrice
service: app.product_validator
App\Entity\Order:
- on: [postUpdate]
method: notifyCustomer
service: app.order_notifier
Global Listeners Apply to all entities (use cautiously):
dmytrof_doctrine_modification_events:
global_listeners:
- on: [postUpdate]
method: logChange
service: app.global_audit_logger
Conditional Logic Filter events in listeners:
public function onProductUpdate(ModificationEvent $event): void
{
$product = $event->getEntity();
if ($product->isDiscounted()) {
$this->dispatchDiscountAlert($product);
}
}
Symfony Messenger Offload async tasks (e.g., notifications) to a queue:
use Symfony\Component\Messenger\MessageBusInterface;
public function __construct(private MessageBusInterface $bus) {}
public function onUserUpdate(ModificationEvent $event): void
{
$this->bus->dispatch(new UserUpdatedEvent($event->getEntity()));
}
Doctrine Lifecycle Callbacks
Combine with @HasLifecycleCallbacks for pre/post-persist/update:
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Product {
#[ORM\PreUpdate]
public function preUpdate(): void {
// Custom logic before bundle events fire
}
}
Event Payload
Leverage ModificationEvent methods:
$entity = $event->getEntity();
$changes = $event->getChanges(); // Array of [field => [old, new]]
$unitOfWork = $event->getUnitOfWork();
$isFlushAndModified = $event->isFlushAndModified();
Prioritize Listeners
Use priority in YAML to control execution order:
listeners:
App\Entity\User:
- on: [postUpdate]
method: notifyAdmin
service: app.user_listener
priority: 10 # Higher priority = earlier execution
Idempotent Design Ensure listeners handle duplicate events gracefully (e.g., during retries):
public function onOrderUpdate(ModificationEvent $event): void
{
if ($event->isFlushAndModified()) {
$this->updateExternalSystem($event->getEntity());
}
}
Testing
Use ModificationEvent in unit tests:
$event = new ModificationEvent(
$entityManager->getUnitOfWork(),
$entity,
['price' => [100, 150]]
);
$listener->onProductUpdate($event);
Duplicate Events
postUpdate may fire twice if ForceFlushPreviousModificationsInterface is triggered (e.g., in nested transactions).$event->isFlushAndModified() or use postFlush instead of postUpdate for critical logic.Circular Dependencies
preUpdate listeners. Use postUpdate for side effects.Performance Spikes
Missing Changes
getChanges() may return empty for null values or unchanged fields.$uow = $event->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($entity);
Annotation Conflicts
@ORM\PreUpdate/@ORM\PostUpdate annotations disabled.Enable Event Dispatcher Debugging
php bin/console debug:event-dispatcher
Look for dmytrof.doctrine.modification.* events.
Log Event Payloads
public function onDebugEvent(ModificationEvent $event): void
{
file_put_contents(
'debug_event.log',
print_r($event->getChanges(), true),
FILE_APPEND
);
}
Check UnitOfWork State
Use getScheduledEntityInsertions()/getScheduledEntityUpdates() to inspect pending changes:
$uow = $event->getUnitOfWork();
$updates = $uow->getScheduledEntityUpdates();
Custom Event Classes
Extend ModificationEvent for additional data:
namespace App\Event;
use Dmytrof\DoctrineModificationEventsBundle\Event\ModificationEvent;
class CustomModificationEvent extends ModificationEvent
{
public function getCustomData(): array
{
return $this->customData;
}
}
Dynamic Listeners Register listeners programmatically in a compiler pass:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class DynamicListenerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$definition = $container->findDefinition('dmytrof.doctrine_modification_events.listener_registry');
$definition->addMethodCall('addDynamicListener', [
'App\Entity\Product',
['on' => 'postUpdate', 'method' => 'syncToExternalApi']
]);
}
}
Event Filtering Create a decorator to filter events:
use Dmytrof\DoctrineModificationEventsBundle\Event\ModificationEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FilteringSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ModificationEvent::POST_UPDATE => 'filterEvent',
];
}
public function filterEvent(ModificationEvent $event): void
{
if (!$event->getEntity()->isActive()) {
$event->stopPropagation();
}
}
}
use Dmytrof\DoctrineModification
How can I help you explore Laravel packages today?