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

Ddd Doctrine Bridge Laravel Package

becklyn/ddd-doctrine-bridge

Doctrine ORM bridge for becklyn/ddd-core: provides event store and transaction manager implementations plus ORM mappings and a migration. Includes a DBAL type override to persist microsecond-precision event timestamps (MySQL/Oracle).

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Install Dependencies

    composer require becklyn/ddd-core becklyn/ddd-doctrine-bridge doctrine/orm doctrine/migrations
    
  2. Configure Doctrine Add Doctrine’s EntityManager to Laravel’s service container (e.g., in config/app.php or a custom service provider):

    $this->app->bind(\Doctrine\ORM\EntityManagerInterface::class, function ($app) {
        return EntityManager::create($app['db.connection'], $config);
    });
    
  3. Run Migrations Publish and execute the provided Doctrine migration:

    php artisan vendor:publish --provider="Doctrine\Migrations\Bundle\DoctrineMigrationsBundle" --tag="migrations"
    php artisan doctrine:migrations:migrate
    
  4. Register Event Store Bind the Doctrine event store to becklyn/ddd-core in a service provider:

    $this->app->bind(\Becklyn\Ddd\Core\EventStoreInterface::class, function ($app) {
        return new \Becklyn\Ddd\EventStore\Infrastructure\Doctrine\DoctrineEventStore(
            $app->make(\Doctrine\ORM\EntityManagerInterface::class)
        );
    });
    
  5. Override datetime_immutable Type (Optional) For microsecond precision in MySQL/Oracle, add this during bootstrapping (e.g., in AppServiceProvider):

    use Doctrine\DBAL\Types\Type;
    Type::overrideType('datetime_immutable', \Becklyn\Ddd\DateTime\Infrastructure\Doctrine\DateTimeImmutableMicrosecondsType::class);
    
  6. First Use Case: Persist an Aggregate Event

    $eventStore = app(\Becklyn\Ddd\Core\EventStoreInterface::class);
    $eventStore->record(
        new \App\Domain\Events\UserRegistered(
            uuid: Uuid::fromString('...'),
            timestamp: new \DateTimeImmutable(),
            payload: ['email' => 'user@example.com']
        )
    );
    

Implementation Patterns

Core Workflows

1. Event Sourcing with Aggregates

  • Pattern: Use becklyn/ddd-core's AggregateRoot alongside the Doctrine-backed event store.
  • Example:
    class UserAggregate extends AggregateRoot
    {
        public function register(string $email): void
        {
            $this->recordThat(new UserRegistered(
                uuid: $this->id(),
                timestamp: new \DateTimeImmutable(),
                payload: ['email' => $email]
            ));
        }
    }
    
    // Persist aggregate state via event store
    $aggregate = new UserAggregate();
    $aggregate->register('user@example.com');
    $eventStore->record($aggregate->pullDomainEvents());
    

2. Transaction Management

  • Pattern: Wrap aggregate operations in Doctrine transactions for atomicity.
  • Example:
    $entityManager = app(\Doctrine\ORM\EntityManagerInterface::class);
    $entityManager->getConnection()->beginTransaction();
    
    try {
        $eventStore->record($events);
        $entityManager->flush();
        $entityManager->getConnection()->commit();
    } catch (\Exception $e) {
        $entityManager->getConnection()->rollBack();
        throw $e;
    }
    

3. Event Projections (CQRS)

  • Pattern: Use Doctrine listeners or async jobs to project events into read models.
  • Example:
    // In a Doctrine event listener
    $eventStore->listen(UserRegistered::class, function (UserRegistered $event) {
        $user = new UserReadModel($event->payload['email']);
        $entityManager->persist($user);
        $entityManager->flush();
    });
    

4. Laravel Integration

  • Pattern: Bridge Laravel’s event system with becklyn/ddd-core events.
  • Example:
    // Dispatch Laravel events from DDD events
    $eventStore->listen(UserRegistered::class, function (UserRegistered $event) {
        event(new \App\Events\UserRegisteredLaravelEvent($event->payload));
    });
    

Integration Tips

  1. Service Provider Setup Centralize Doctrine and DDD bindings in a dedicated service provider:

    class DddServiceProvider extends ServiceProvider
    {
        public function register()
        {
            $this->app->bind(\Becklyn\Ddd\Core\EventStoreInterface::class, function ($app) {
                return new \Becklyn\Ddd\EventStore\Infrastructure\Doctrine\DoctrineEventStore(
                    $app->make(\Doctrine\ORM\EntityManagerInterface::class)
                );
            });
        }
    }
    
  2. Database Schema

    • Use the provided Doctrine migrations for the event_entry table.
    • Extend the schema if needed (e.g., add indexes for query performance):
      CREATE INDEX idx_event_entry_aggregate_id ON event_entry(aggregate_id);
      
  3. Event Serialization

    • Ensure events implement JsonSerializable or use a serializer like jms/serializer for complex payloads.
    • Example:
      class UserRegistered implements JsonSerializable
      {
          public function jsonSerialize(): array
          {
              return [
                  'uuid' => $this->uuid->toString(),
                  'timestamp' => $this->timestamp->format(\DateTimeInterface::ATOM),
                  'payload' => $this->payload,
              ];
          }
      }
      
  4. Testing

    • Use Doctrine’s in-memory database for unit tests:
      use Doctrine\ORM\Tools\SchemaTool;
      use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
      
      public function setUp(): void
      {
          $this->entityManager = EntityManager::create(
              ['url' => 'sqlite:///:memory:'],
              $this->getDoctrineConfig()
          );
          $this->eventStore = new DoctrineEventStore($this->entityManager);
      }
      

Gotchas and Tips

Pitfalls

  1. Microsecond Timestamp Quirks

    • Issue: The DateTimeImmutableMicrosecondsType may fail silently on unsupported databases (e.g., PostgreSQL, SQLite).
    • Fix: Wrap overrides in a database-specific check:
      if (in_array($connection->getDatabasePlatform()->getName(), ['mysql', 'oci'])) {
          Type::overrideType('datetime_immutable', DateTimeImmutableMicrosecondsType::class);
      }
      
  2. Transaction Isolation

    • Issue: Long-running transactions (e.g., sagas) may cause locks in Doctrine.
    • Fix: Use SET TRANSACTION ISOLATION LEVEL READ COMMITTED in MySQL or optimize aggregate loading.
  3. Event Deduplication

    • Issue: Duplicate events may slip through if not handled at the application level.
    • Fix: Add a unique constraint on (aggregate_id, event_type, event_id) in the event_entry table.
  4. Laravel Queue Conflicts

    • Issue: Doctrine transactions and Laravel queues may interfere (e.g., PendingDispatch).
    • Fix: Disable queue workers during critical transactions or use queue:work --once.
  5. Aggregate Loading

    • Issue: Loading aggregates by event history can be slow for large event stores.
    • Fix: Implement snapshotting or lazy-loading for aggregates.

Debugging Tips

  1. Event Store Inspection

    • Query the event_entry table directly to verify events:
      SELECT * FROM event_entry ORDER BY aggregate_id, version DESC;
      
  2. Doctrine Logging

    • Enable SQL logging for debugging:
      $entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
      
  3. Event Deserialization Errors

    • Symptom: UnexpectedValueException when replaying events.
    • Fix: Ensure all events implement JsonSerializable or use a consistent serializer.
  4. Microsecond Precision Debugging

    • Symptom: Events lose microseconds on read.
    • Fix: Verify the DateTimeImmutableMicrosecondsType is registered and the database supports microseconds.

Extension Points

  1. Custom Event Types

    • Extend the event_entry table for metadata (e.g., event_metadata JSON column):
      /**
       * @ORM\Column(type="json", nullable=true)
       */
      private ?array $metadata = null;
      
  2. Event Projections

    • Create a base listener for common projections:
      class EventProjectionListener
      {
          public function __invoke(object $event): void
          {
              // Logic to update read models
          }
      }
      
  3. Transaction Retry Logic

    • Implement a decorator for the EventStoreInterface to handle transient failures:
      class RetryableEventStore implements EventStoreInterface
      {
          public function record(iterable $events):
      
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.
besmartand-pro/php-quality-config
sentix/ai-chatbot
codifyo/ts-generator-bundle
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor