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 Events Bundle Laravel Package

andy-thorne/domain-events-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the package:

    composer require andy-thorne/domain-events-bundle
    
  2. Enable the bundle in config/bundles.php:

    return [
        // ...
        AndyThorne\Components\DomainEventsBundle\DomainEventsBundle::class => ['all' => true],
    ];
    
  3. Configure config/packages/domain_events.yaml (minimal async setup):

    domain_events:
        orm: true
        transport: async_domain_events
    framework:
        messenger:
            transports:
                async_domain_events: '%env(ASYNC_MESSENGER_TRANSPORT_DSN)%'
            buses:
                domain_event.bus:
                    default_middleware: allow_no_handlers
    
  4. Define a domain event (e.g., src/Event/UserRegistered.php):

    namespace App\Event;
    
    use AndyThorne\Components\DomainEventsBundle\Events\DomainEventInterface;
    
    class UserRegistered implements DomainEventInterface
    {
        public function __construct(public string $userId) {}
    }
    
  5. Dispatch an event in a domain service (e.g., src/Service/UserService.php):

    use AndyThorne\Components\DomainEventsBundle\DomainEvents;
    
    class UserService {
        public function registerUser(string $email): void {
            $user = new User($email);
            $this->entityManager->persist($user);
            $this->entityManager->flush();
    
            DomainEvents::dispatch(new UserRegistered($user->getId()));
        }
    }
    
  6. Create a handler (e.g., src/MessageHandler/UserRegisteredHandler.php):

    use AndyThorne\Components\DomainEventsBundle\Events\DomainEventInterface;
    use Symfony\Component\Messenger\Attribute\AsMessageHandler;
    
    #[AsMessageHandler]
    class UserRegisteredHandler {
        public function __invoke(UserRegistered $event): void {
            // Side effects (e.g., send email, log, etc.)
        }
    }
    
  7. Run the worker (if using async transport):

    php bin/console messenger:consume async_domain_events -vv
    

First Use Case: Post-Save Notifications

Use this bundle to trigger actions (e.g., sending emails, updating caches) after an entity is persisted, without tightly coupling the domain logic to infrastructure concerns.


Implementation Patterns

Core Workflow: Event-Driven Domain Logic

  1. Domain Layer:

    • Define events in the domain layer (e.g., UserRegistered, OrderShipped).
    • Dispatch events after persisting entities (e.g., in a UserService or repository method).
    • Example:
      $user = $this->userRepository->save($user);
      DomainEvents::dispatch(new UserRegistered($user->getId()));
      
  2. Infrastructure Layer:

    • Configure the bundle to use async transport for decoupled processing.
    • Implement handlers as stateless services (e.g., UserRegisteredHandler).
    • Use Symfony Messenger’s attributes (#[AsMessageHandler]) for clarity.
  3. Testing:

    • Mock the DomainEvents dispatcher in unit tests.
    • Use MessengerTestTrait (from Symfony Messenger) to test handlers in isolation.
    • Example test:
      use Symfony\Component\Messenger\Test\MessageBusTestTrait;
      
      class UserRegisteredHandlerTest {
          use MessageBusTestTrait;
      
          public function testHandler() {
              $bus = $this->createBus([new UserRegisteredHandler()]);
              $bus->dispatch(new UserRegistered('user-123'));
              // Assert side effects (e.g., logged email)
          }
      }
      

Integration Tips

  1. Doctrine Lifecycle Events:

    • Avoid dispatching events in prePersist/preUpdate. Use postPersist/postUpdate or dispatch manually after flush().
    • Example:
      $entityManager->persist($entity);
      $entityManager->flush();
      DomainEvents::dispatch(new EntityUpdated($entity->getId()));
      
  2. Event Sourcing:

    • Combine with Symfony UID for immutable event IDs.
    • Store events in a separate table (e.g., domain_events) for replayability.
  3. Bulk Operations:

    • Dispatch events in batches to avoid overwhelming the message bus.
    • Example:
      $entityManager->flush();
      foreach ($entities as $entity) {
          DomainEvents::dispatch(new EntityProcessed($entity->getId()));
      }
      
  4. Retry Logic:

    • Use Symfony Messenger’s retry middleware for transient failures.
    • Configure in config/packages/messenger.yaml:
      framework:
          messenger:
              transports:
                  async_domain_events:
                      dsn: '%env(ASYNC_MESSENGER_TRANSPORT_DSN)%'
                      retry_strategy:
                          max_retries: 3
                          delay: 1000
                          multiplier: 2
      
  5. Event Versioning:

    • Use Symfony UID’s Ulid for events to ensure chronological ordering.
    • Example:
      use Symfony\Component\Uid\Ulid;
      
      class UserRegistered implements DomainEventInterface {
          public function __construct(public string $eventId, public string $userId) {
              $this->eventId = (new Ulid())->toRfc4122();
          }
      }
      

Gotchas and Tips

Pitfalls

  1. Event Dispatch Timing:

    • Issue: Events dispatched in prePersist may not be processed if the entity fails validation.
    • Fix: Dispatch events after flush() or in a separate transaction.
    • Example:
      try {
          $entityManager->persist($entity);
          $entityManager->flush();
          DomainEvents::dispatch(new EntityCreated($entity->getId()));
      } catch (\Exception $e) {
          $entityManager->rollback();
          throw $e;
      }
      
  2. Async Transport Configuration:

    • Issue: Forgetting to set default_middleware: allow_no_handlers for the domain event bus will cause synchronous failures.
    • Fix: Ensure your domain_event.bus is configured to allow no handlers:
      framework:
          messenger:
              buses:
                  domain_event.bus:
                      default_middleware: allow_no_handlers
      
  3. Circular Dependencies:

    • Issue: Dispatching events in a handler for the same entity can cause infinite loops.
    • Fix: Use a EventBus decorator to track in-flight events or implement idempotency checks.
  4. Doctrine Proxy Issues:

    • Issue: Events dispatched on proxied entities may not have initialized properties.
    • Fix: Use getId() or initialize lazy-loaded properties before dispatching:
      $entity->getId(); // Force initialization
      DomainEvents::dispatch(new EntityUpdated($entity->getId()));
      
  5. Testing Async Handlers:

    • Issue: Async handlers won’t run in unit tests without a test bus.
    • Fix: Use MessengerTestTrait or mock the DomainEvents dispatcher:
      $dispatcher = $this->createMock(DomainEvents::class);
      $dispatcher->expects($this->once())->method('dispatch');
      

Debugging

  1. Check Event Dispatch:

    • Enable Symfony Messenger debug mode to log dispatched messages:
      framework:
          messenger:
              transports:
                  async_domain_events:
                      dsn: '%env(ASYNC_MESSENGER_TRANSPORT_DSN)%'
                      options:
                          logger: true
      
  2. Worker Logs:

    • Run the worker with verbose logging to diagnose failures:
      php bin/console messenger:consume async_domain_events -vv
      
  3. Event Routing:

    • Verify routing is configured correctly. Run:
      php bin/console debug:container --parameter=framework.messenger.routing
      
    • Ensure your event class is routed to the async transport.

Extension Points

  1. Custom Event Bus:

    • Override the default bus to add middleware (e.g., logging, deduplication):
      framework:
          messenger:
              buses:
                  custom_domain_bus:
                      middleware:
                          - AndyThorne\Components\DomainEventsBundle\Middleware\LogDomainEvents
                          - allow_no_handlers
      
      Then update domain_events.bus in config to custom_domain_bus.
  2. Dynamic Event Routing:

    • Use a custom router to route events based on conditions (e.g., tenant ID):
      use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
      use Symfony\Component\Messenger\Transport\TransportInterface;
      
      class TenantAwareRouter implements RouterInterface {
          public function getRoutes(): iterable {
              yield new Route(
                  new DomainEventInterface(),
                  new TransportInterface(...)
              );
          }
      }
      
      Register it as
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.
daikazu/eloquent-salesforce-objects
unseen-codes/chat
romalytar/yammi-jobs-monitoring-laravel
kisame76/filament-db-table-state
nqxcode/laravel-lucene-search
dpfx/laravel-livewire-wizards
workos/workos-php-laravel
sofa/laravel-global-scope
nawasara/auth-primitives
adhocrat-io/arkhe-main
make-dev/orca-harpoon
itsemon245/lamet
baks-dev/dashboard
amoifr/pickle-panther-bundle
make-dev/orca
dmstr/symfony-system-resources-bundle
dmstr/symfony-job-queue-bundle
dmstr/openapi-json-schema-bundle
dmstr/keycloak-security-bundle
dmstr/doctrine-audit-log-bundle