Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Domain Event Dispatcher Bundle Laravel Package

ashleydawson/domain-event-dispatcher-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require ashleydawson/domain-event-dispatcher-bundle
    

    Register the bundle in config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 2/3):

    AshleyDawson\DomainEventDispatcherBundle\AshleyDawsonDomainEventDispatcherBundle::class => ['all' => true],
    
  2. First Event: Define a simple event class (e.g., src/DomainEvent/UserRegistered.php):

    namespace App\DomainEvent;
    
    class UserRegistered {
        public function __construct(private int $userId) {}
        public function getUserId(): int { return $this->userId; }
    }
    
  3. First Listener: Create a listener (e.g., src/Listener/UserRegisteredListener.php):

    namespace App\Listener;
    
    use App\DomainEvent\UserRegistered;
    
    class UserRegisteredListener {
        public function __invoke(UserRegistered $event) {
            // Handle event (e.g., log, send email)
        }
    }
    
  4. Register Listener: Tag the listener in config/services.yaml:

    services:
        App\Listener\UserRegisteredListener:
            tags:
                - { name: ashley_dawson.domain_event_listener }
    
  5. Dispatch an Event: Trigger the event from a service/entity (e.g., src/Service/UserService.php):

    use AshleyDawson\DomainEventDispatcher\DomainEventDispatcher;
    
    class UserService {
        public function registerUser(): void {
            $userId = 123;
            DomainEventDispatcher::getInstance()->dispatch(new UserRegistered($userId));
        }
    }
    

First Use Case: Domain-Driven Design (DDD) Workflow

Use this bundle to decouple side effects (e.g., notifications, analytics) from core business logic. For example:

  • Dispatch OrderCreated when saving an Order entity.
  • Let listeners handle inventory updates, email notifications, or audit logs without coupling them to the Order class.

Implementation Patterns

1. Event-Driven Architecture Workflow

  • Separation of Concerns:

    • Entities/Aggregates: Dispatch events (e.g., UserActivated, PaymentProcessed).
    • Listeners: Handle cross-cutting concerns (e.g., logging, caching, external APIs).
    • Example:
      // Entity dispatches event
      $user->activate(); // Internally dispatches UserActivated event
      
      // Listener handles side effects
      public function __invoke(UserActivated $event) {
          $this->sendWelcomeEmail($event->getUserId());
      }
      
  • Deferred Events:

    • Configure deferred dispatch in config/packages/ashley_dawson_domain_event_dispatcher.yaml:
      ashley_dawson_domain_event_dispatcher:
          dispatch_deferred_events_from_kernel_event: kernel.terminate
      
    • Useful for async operations (e.g., sending emails after request completes).

2. Integration with Symfony Ecosystem

  • Dependency Injection:

    • Inject DomainEventDispatcher via constructor (Symfony 3.3+):
      use AshleyDawson\DomainEventDispatcher\DomainEventDispatcher;
      
      class OrderService {
          public function __construct(private DomainEventDispatcher $dispatcher) {}
      }
      
    • Avoid Singleton Anti-Pattern: Prefer DI over DomainEventDispatcher::getInstance().
  • Command Bus Integration:

    • Dispatch events after handling commands (e.g., in a command handler):
      public function handle(CreateUserCommand $command) {
          $user = $this->userRepository->save($command->toUser());
          $this->dispatcher->dispatch(new UserCreated($user->getId()));
      }
      
  • Messenger Component:

    • Bridge domain events to Symfony Messenger for async processing:
      public function __invoke(UserRegistered $event) {
          $this->messageBus->dispatch(new SendWelcomeEmailMessage($event->getUserId()));
      }
      

3. Testing Patterns

  • Mocking the Dispatcher:

    • Use DomainEventDispatcherInterface (if available) or mock the singleton:
      $dispatcher = $this->createMock(DomainEventDispatcher::class);
      $dispatcher->method('dispatch')->willReturnCallback(function($event) {
          $this->lastDispatchedEvent = $event;
      });
      
    • Test Listeners: Verify events are dispatched and listeners receive them:
      $listener = new UserRegisteredListener();
      $event = new UserRegistered(1);
      $listener($event);
      $this->assertTrue($this->emailSent);
      
  • Event Recording:

    • Use the Symfony Profiler to debug dispatched/deferred events during tests:
      $client = static::createClient();
      $client->request('GET', '/register');
      $this->assertCount(1, $client->getProfile()->getCollector('ashley_dawson_domain_event_dispatcher')->getEvents());
      

4. Performance Considerations

  • Batch Dispatching:
    • For high-frequency events (e.g., clicks), batch dispatch outside the request cycle:
      // In a cron job or command
      $dispatcher = DomainEventDispatcher::getInstance();
      $dispatcher->dispatch(new BatchUserActivityEvents($userActivityEvents));
      
  • Avoid Blocking:
    • Use deferred events for I/O-bound operations (e.g., API calls, database writes).

Gotchas and Tips

Pitfalls

  1. Singleton Overuse:

    • Issue: Directly calling DomainEventDispatcher::getInstance() tightly couples code to the singleton.
    • Fix: Prefer constructor injection or service locator pattern:
      $dispatcher = $this->container->get('ashley_dawson.domain_event_dispatcher');
      
  2. Circular Dependencies:

    • Issue: Listeners dispatching new events can create infinite loops.
    • Fix: Use a EventDispatcherInterface wrapper to mock/disable dispatching during tests:
      $dispatcher = $this->createMock(DomainEventDispatcher::class);
      $dispatcher->method('dispatch')->willReturnCallback(function($event) {
          if ($event instanceof UserRegistered) {
              throw new \LogicException("Infinite loop detected!");
          }
      });
      
  3. Deferred Events in CLI:

    • Issue: kernel.terminate events don’t fire in Symfony CLI commands.
    • Fix: Dispatch deferred events manually or use console.terminate:
      ashley_dawson_domain_event_dispatcher:
          dispatch_deferred_events_from_kernel_event: console.terminate
      
  4. Event Ordering:

    • Issue: Listeners for the same event may execute out of order.
    • Fix: Use priority tags (if supported) or design events to be idempotent:
      tags:
          - { name: ashley_dawson.domain_event_listener, priority: 100 }
      

Debugging Tips

  1. Profiler:

    • Check the Symfony Profiler under the "Domain Events" tab to see:
      • Dispatched events (with payloads).
      • Deferred events (pending dispatch).
    • Clear Events: Deferred events persist until dispatched; clear them manually if needed:
      DomainEventDispatcher::getInstance()->clearDeferredEvents();
      
  2. Logging:

    • Log dispatched events for debugging:
      public function __invoke(MyDomainEvent $event) {
          $this->logger->debug('Event dispatched', ['event' => get_class($event), 'data' => $event->getData()]);
      }
      
  3. Event Validation:

    • Validate event payloads in listeners to catch bugs early:
      public function __invoke(UserRegistered $event) {
          if ($event->getUserId() <= 0) {
              throw new \InvalidArgumentException("Invalid user ID");
          }
      }
      

Extension Points

  1. Custom Event Dispatcher:

    • Extend the dispatcher to add middleware or logging:
      class CustomDomainEventDispatcher extends DomainEventDispatcher {
          public function dispatch($event) {
              $this->logger->info(sprintf('Dispatching %s', get_class($event)));
              parent::dispatch($event);
          }
      }
      
    • Register it as a service and override the bundle’s default.
  2. Dynamic Listeners:

    • Load listeners dynamically (e.g., from a database) by implementing a custom ListenerLocator:
      $dispatcher = DomainEventDispatcher::getInstance();
      $dispatcher->addListener('UserRegistered', $dynamicListener);
      
  3. Event Metadata:

    • Attach metadata to events (e.g., timestamps, user context):
      class UserRegistered {
          public function __construct(private int $userId, private array
      
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui
babelqueue/php-sdk
facebook/capi-param-builder-php
babelqueue/symfony
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium