Installation:
composer require astina/domain-events-bundle:dev-master
Ensure your doctrine/doctrine-bundle is on dev-master (as per the package requirement).
Enable the Bundle:
Add to config/bundles.php (or AppKernel.php for older Laravel versions):
Astina\Bundle\DomainEventsBundle\AstinaDomainEventsBundle::class => ['all' => true],
First Use Case:
Create a subscriber service (e.g., app/Services/EventListeners/ProductUpdatedListener.php):
namespace App\Services\EventListeners;
class ProductUpdatedListener
{
public function postUpdate(\App\Models\Product $product, $eventArgs)
{
// Handle post-update logic (e.g., log, notify, or trigger workflows)
logger()->info("Product updated: {$product->name}");
}
}
Register the Subscriber:
Define the service in config/services.yaml:
services:
App\Services\EventListeners\ProductUpdatedListener:
arguments: ['@logger'] # Inject dependencies if needed
Configure the Listener:
Add to config/packages/astina_domain_events.yaml:
astina_domain_events:
subscribers:
App\Services\EventListeners\ProductUpdatedListener:
entity: App\Models\Product
events: [postUpdate]
Event-Driven Logic: Use subscribers to decouple domain logic from entities. Example:
// app/Services/EventListeners/OrderFulfillmentListener.php
class OrderFulfillmentListener
{
public function postPersist(\App\Models\Order $order, $eventArgs)
{
if ($order->isFulfillable()) {
$this->fulfillmentService->process($order);
}
}
}
Dependency Injection: Inject services into listeners via constructor or arguments:
services:
App\Services\EventListeners\InventoryListener:
arguments:
- '@inventoryService'
- '@notificationService'
Conditional Logic:
Use $eventArgs to inspect entity state or event metadata:
public function preUpdate(\App\Models\User $user, PreUpdateEventArgs $eventArgs)
{
$uow = $eventArgs->getObjectManager()->getUnitOfWork();
$changes = $uow->getEntityChangeSet($user);
if (isset($changes['role'])) {
$this->roleChangeHandler->handle($user, $changes['role'][1]);
}
}
Bulk Operations:
Leverage preFlush/postFlush for batch processing:
public function preFlush($eventArgs)
{
$entityManager = $eventArgs->getObjectManager();
$entities = $entityManager->getUnitOfWork()->getScheduledEntityInsertions();
foreach ($entities as $entity) {
if ($entity instanceof \App\Models\Loggable) {
$this->logger->logEntityAction($entity, 'create');
}
}
}
Cross-Entity Workflows: Chain listeners to propagate events across aggregates:
astina_domain_events:
subscribers:
App\Services\EventListeners\OrderStatusListener:
entity: App\Models\Order
events: [postUpdate]
App\Services\EventListeners\InventorySyncListener:
entity: App\Models\Product
events: [postUpdate] # Triggered by OrderStatusListener
Doctrine Version Lock:
doctrine/doctrine-bundle:dev-master. Avoid using stable releases to prevent runtime errors.dev-master commit if stability is critical.Method Naming Strictness:
postUpdate, not handlePostUpdate). Case-sensitive.Service ID Mismatch:
subscribers YAML key must use the exact service ID defined in services.yaml. Typos here will silently fail.Event Argument Order:
$eventArgs. Reversing this breaks binding:
// ❌ Wrong
public function postUpdate($eventArgs, \App\Models\User $user) { ... }
Circular Dependencies:
EntityManager directly into listeners. Use $eventArgs->getObjectManager() instead to prevent circular references.Performance with preFlush/postFlush:
Verify Subscriber Registration:
Check if the subscriber is loaded by inspecting the EventManager:
$eventManager = $entityManager->getEventManager();
$listeners = $eventManager->getListeners('preUpdate');
dump($listeners); // Should include your subscriber
Enable Doctrine Debugging:
Add to config/packages/doctrine.yaml:
doctrine:
orm:
eventmanager:
debug: true
Logs event listener invocations to the Symfony profiler.
Test Event Firing: Use a dummy subscriber to confirm events trigger:
class DebugSubscriber
{
public function postPersist($entity, $eventArgs)
{
logger()->debug("Event fired for: " . get_class($entity));
}
}
Dynamic Subscribers:
Override the bundle’s SubscriberLoader to load subscribers dynamically (e.g., from a database):
// src/EventListener/SubscriberLoader.php
class CustomSubscriberLoader extends \Astina\Bundle\DomainEventsBundle\SubscriberLoader
{
public function loadSubscribers()
{
$subscribers = $this->fetchFromDatabase(); // Custom logic
return $subscribers;
}
}
Register it in services.yaml:
services:
Astina\Bundle\DomainEventsBundle\SubscriberLoader:
class: App\EventListener\CustomSubscriberLoader
Custom Events: Extend the bundle to support non-Doctrine events (e.g., Symfony kernel events) by creating a wrapper service.
Field-Level Subscriptions: Implement a proxy pattern to intercept field-specific changes (as hinted in the TODO):
class FieldChangeSubscriber
{
public function preUpdate($entity, PreUpdateEventArgs $eventArgs)
{
$uow = $eventArgs->getObjectManager()->getUnitOfWork();
$changes = $uow->getEntityChangeSet($entity);
foreach ($changes as $field => $change) {
if ($this->shouldHandleField($field)) {
$this->handleFieldChange($entity, $field, $change);
}
}
}
}
Priority Handling:
Use Symfony’s priority tag to control listener execution order:
services:
App\Services\EventListeners\HighPriorityListener:
tags:
- { name: 'astina_domain_events.subscriber', priority: 100 }
How can I help you explore Laravel packages today?