actiane/entity-change-watch-bundle
Symfony bundle to watch Doctrine entity lifecycle changes. Configure per-entity callbacks on create, update (all or specific properties), and delete. Invoke tagged services after flush with entity, changed properties, and EntityManager; supports pre-flush via flush:false.
Installation:
composer require actiane/entity-change-watch-bundle
Enable the bundle in config/bundles.php:
return [
// ...
Actiane\EntityChangeWatchBundle\EntityChangeWatchBundle::class => ['all' => true],
];
Configuration:
Create config/packages/actiane_entity_change_watch.yaml (or merge into config/packages/framework.yaml):
entity_change_watch:
classes:
App\Entity\MyEntity:
update:
properties:
name:
- { name: 'App\Service\MyListener', method: 'onNameChange' }
First Use Case:
Trigger a service method when MyEntity's name property is updated:
// src/Service/MyListener.php
class MyListener {
public function onNameChange(MyEntity $entity, string $oldValue, string $newValue) {
// Log or process the change
Logger::info("Name changed from {$oldValue} to {$newValue}");
}
}
Lifecycle Hooks:
Use create, update, and delete to bind services to Doctrine events:
entity_change_watch:
classes:
App\Entity\User:
create:
- { name: 'App\Service\AuditService', method: 'logCreation' }
update:
all:
- { name: 'App\Service\NotificationService', method: 'sendUpdateNotification' }
Property-Level Granularity: Target specific fields for fine-grained control:
update:
properties:
email:
- { name: 'App\Service\EmailValidator', method: 'validateEmailChange' }
status:
- { name: 'App\Service\WorkflowService', method: 'handleStatusTransition' }
Service Integration: Pass entity data to services via method signatures:
public function handleStatusTransition(MyEntity $entity, string $oldStatus, string $newStatus) {
// Business logic for status changes
}
Conditional Logic:
Use flush: false to prevent auto-flushing during pre-save hooks:
create:
- { name: 'App\Service\PreSaveService', method: 'validateBeforeSave', flush: false }
Dynamic Configuration:
Load YAML config dynamically via EntityChangeWatchBundle's Extension class:
$this->container->get('actiane_entity_change_watch.extension')->addListener(
'App\Entity\Product',
'update',
['App\Service\InventoryService', 'updateStock']
);
Event Aggregation: Combine multiple services for a single event:
update:
all:
- { name: 'App\Service\Logger', method: 'logUpdate' }
- { name: 'App\Service\Analytics', method: 'trackChange' }
Entity Inheritance: Extend parent entity configurations:
classes:
App\Entity\BaseEntity: # Parent
update:
all:
- { name: 'App\Service\AuditService', method: 'logUpdate' }
App\Entity\User: # Child (inherits + overrides)
update:
properties:
username:
- { name: 'App\Service\UsernameValidator', method: 'validate' }
Circular Dependencies:
Avoid injecting the entity manager into listeners that trigger other Doctrine operations (risk of infinite loops).
Fix: Use flush: false or refactor to async processing.
Missing Method Signatures: The bundle expects methods to match:
public function methodName(Entity $entity, mixed $oldValue = null, mixed $newValue = null)
Tip: Use PHPDoc to enforce this:
/** @param mixed|null $oldValue */
/** @param mixed|null $newValue */
Configuration Overrides: Later YAML entries overwrite earlier ones. Use explicit keys to avoid surprises:
# Overrides all 'update' listeners for App\Entity\User
App\Entity\User:
update: ~ # Clears previous listeners
Performance:
Heavy listeners on update.all can slow bulk operations.
Tip: Limit to critical properties or use flush: false for async processing.
Doctrine Events Order:
prePersist/preUpdate run before preFlush. Place validation logic in prePersist if you need to reject saves early.
Enable Logging:
Add to config/packages/monolog.yaml:
handlers:
actiane:
type: stream
path: "%kernel.logs_dir%/actiane_entity_change.log"
level: debug
channels: ["actiane_entity_change"]
Then configure the bundle to log events:
entity_change_watch:
debug: true
Check Event Firing: Verify listeners are registered via:
$this->container->get('actiane_entity_change_watch.listener_registry')->getListeners('App\Entity\User');
Handle Exceptions: Wrap listener calls in try-catch to prevent Doctrine from failing silently:
try {
$service->method($entity, $oldValue, $newValue);
} catch (\Exception $e) {
// Log and continue
}
Custom Event Types:
Extend the bundle to support additional lifecycle events (e.g., postLoad):
// src/EventListener/CustomListener.php
class CustomListener implements ContainerAwareInterface {
public function postLoad(EntityManager $em, Entity $entity) {
// Custom logic
}
}
Dynamic Property Mapping: Use a service to dynamically resolve properties:
update:
properties:
dynamic_property:
- { name: 'App\Service\DynamicPropertyResolver', method: 'resolveAndNotify' }
Conditional Listeners: Implement logic to skip listeners based on runtime conditions:
public function onUpdate(MyEntity $entity) {
if (!$entity->isActive()) {
return; // Skip inactive entities
}
// ...
}
Bulk Operation Optimization: For bulk updates, disable listeners temporarily:
$em->getEventManager()->removeEventListeners('preUpdate');
// Perform bulk operations
$em->getEventManager()->addEventListeners($listeners);
How can I help you explore Laravel packages today?