bentools/doctrine-watcher-bundle
Install the Bundle
composer require bentools/doctrine-watcher-bundle:1.0.x-dev
Ensure compatibility with your Symfony version (e.g., ^5.4 or ^6.0).
Enable the Bundle
Add to config/bundles.php:
return [
// ...
Bentools\DoctrineWatcherBundle\DoctrineWatcherBundle::class => ['all' => true],
];
Basic Configuration
Define a watcher service and configure it in config/packages/doctrine_watcher.yaml:
doctrine_watcher:
watch:
App\Entity\Book:
properties:
title:
callback: 'App\Services\BookWatcher::onTitleChange'
First Use Case: Trigger Logic on Entity Changes Create a watcher service:
// src/Services/BookWatcher.php
namespace App\Services;
class BookWatcher
{
public function onTitleChange(string $oldValue, string $newValue, object $entity): void
{
// Logic for title changes (e.g., log, notify, or update related data)
\Log::info("Book title changed from {$oldValue} to {$newValue}");
}
}
Hook into Doctrine Events
Register the watcher in config/packages/doctrine.yaml:
doctrine:
orm:
event_subscribers:
- Bentools\DoctrineWatcherBundle\EventSubscriber\DoctrineWatcherSubscriber
Define Watchable Entities
Configure properties to watch in doctrine_watcher.yaml:
doctrine_watcher:
watch:
App\Entity\User:
properties:
email:
callback: 'App\Services\UserWatcher::validateEmail'
posts:
callback: 'App\Services\UserWatcher::onPostsChange'
iterable: true # For collections/arrays
Implement Callbacks Create services with methods matching the configured callbacks:
// src/Services/UserWatcher.php
class UserWatcher
{
public function validateEmail(string $oldEmail, string $newEmail, User $user): void
{
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email format");
}
}
public function onPostsChange(array $oldPosts, array $newPosts, User $user): void
{
// Handle post collection changes (e.g., sync metadata)
}
}
Tag-Based Configuration (Alternative) Use service tags for dynamic registration:
services:
App\Services\PostWatcher:
tags:
- { name: bentools.doctrine_watcher, entity_class: App\Entity\Post, property: 'content', method: 'onContentUpdate' }
Integration with Doctrine Events Leverage Symfony’s event system for pre/post-save logic:
// src/EventSubscriber/CustomSubscriber.php
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
class CustomSubscriber implements EventSubscriber
{
public function getSubscribedEvents(): array
{
return ['onFlush'];
}
public function onFlush(OnFlushEventArgs $args): void
{
$entityManager = $args->getEntityManager();
$uow = $entityManager->getUnitOfWork();
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof \App\Entity\Book) {
// Trigger watcher logic manually if needed
}
}
}
}
Bulk Operations For mass updates (e.g., via Doctrine QueryBuilder), disable watchers temporarily:
$entityManager->getConnection()->getConfiguration()->setSQLLogger(null);
// Perform bulk operations
$entityManager->flush();
Circular Dependencies
Avoid circular references in callbacks (e.g., UserWatcher calling BookWatcher which triggers UserWatcher again). Use dependency injection carefully.
Performance Overhead
OneToMany) can slow down flushes. Use iterable: false for performance-critical paths or limit watched properties.serializable: true in config to serialize instead:
doctrine_watcher:
watch:
App\Entity\ComplexEntity:
properties:
data:
callback: 'App\Services\ComplexWatcher::handleData'
serializable: true
Event Ordering
Callbacks run after Doctrine’s preUpdate/prePersist but before postUpdate/postPersist. Avoid side effects that interfere with Doctrine’s lifecycle.
Configuration Merging
If using Symfony Flex, ensure doctrine_watcher.yaml is in the correct location (config/packages/) to avoid merge conflicts with default configs.
Doctrine Version Compatibility The bundle assumes Doctrine ORM. For Doctrine DBAL or other ORMs, extend the subscriber manually.
Enable Logging
Add to config/services.yaml:
Bentools\DoctrineWatcherBundle\:
resource: '../vendor/bentools/doctrine-watcher-bundle/Resources/config/services.yaml'
public: true
Then log callback invocations:
public function onTitleChange(string $old, string $new, object $entity): void
{
\Log::debug("Watcher triggered for {$entity::class}.title: {$old} -> {$new}");
}
Disable Watchers Temporarily Override the subscriber in a test environment:
# config/packages/test/doctrine_watcher.yaml
doctrine_watcher:
enabled: false
Check for Missing Services
If callbacks fail with ServiceNotFoundException, ensure the service is:
autowire: true in services.yaml).public: true if needed).Custom Comparison Logic
Extend the Bentools\DoctrineWatcher\PropertyWatcher class to add custom comparison (e.g., ignore whitespace):
class CustomPropertyWatcher extends PropertyWatcher
{
protected function compareValues($oldValue, $newValue): bool
{
return trim($oldValue) === trim($newValue);
}
}
Register it in services.yaml:
services:
App\Services\CustomPropertyWatcher:
tags:
- { name: bentools.doctrine_watcher.property_watcher, alias: 'trim_comparison' }
Async Callbacks Use Symfony Messenger to offload watcher logic:
public function onTitleChange(string $old, string $new, Book $book): void
{
$message = new ProcessBookTitleChange($book->getId(), $old, $new);
$this->messageBus->dispatch($message);
}
Dynamic Property Watching Implement a runtime-based watcher (e.g., via Doctrine lifecycle callbacks):
use Doctrine\ORM\Event\LifecycleEventArgs;
class DynamicWatcher
{
public function postLoad(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof Book && $entity->isDynamicWatchEnabled()) {
// Register watcher dynamically
}
}
}
Bulk Property Updates For batch operations, use a single callback with a list of changes:
doctrine_watcher:
watch:
App\Entity\Product:
properties:
prices:
callback: 'App\Services\ProductWatcher::handleBulkPriceUpdate'
iterable: true
bulk: true # Custom tag to indicate bulk handling
How can I help you explore Laravel packages today?