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

geekcell/ddd-bundle

GeekCell DDD Bundle is a Laravel/PHP package for applying Domain-Driven Design with reusable building blocks and conventions that help organize domain, application, and infrastructure layers into cleaner, more maintainable code.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, low-adoption bundle (13 stars) from GeekCell that ships the usual DDD primitives for Symfony: base classes for entities/aggregate roots, value objects, domain events, and a publisher that hooks into the Symfony event dispatcher. The repository is archived with a last release in Feb 2025, so treat it as source code you are adopting, not a maintained dependency.

Minimal steps:

composer require geekcell/ddd-bundle

Register it in config/bundles.php (or config/bundles.php on Symfony 4+):

return [
    // ...
    GeekCell\DddBundle\DddBundle::class => ['all' => true],
];

Then read the package source before writing a line of domain code — it's small enough to audit in one sitting:

vendor/geekcell/ddd-bundle/src/
├── DddBundle.php
├── Domain/            # Entity, AggregateRoot, ValueObject, DomainEvent contracts
├── Event/             # publisher / dispatcher wiring
└── Resources/config/  # service definitions and tags

Also run php bin/console debug:container --tag=… and debug:event-dispatcher to see what the bundle actually registers in your app; auto-configuration is the main thing that will surprise you.

First use case: model one aggregate root with one value object and one domain event, and get the event to the Symfony dispatcher after a Doctrine flush. That single loop exercises ~90% of what the bundle gives you. Everything else (repositories, messaging) is conventional Symfony wiring you can add later.

Implementation Patterns

Aggregate root that records events, dispatcher releases them after commit. The typical shape is a base AggregateRoot that collects domain events internally, plus a subscriber or middleware that drains them at the right moment:

final class Order extends AggregateRoot
{
    /** @var OrderLine[] */
    private array $lines = [];

    public static function place(OrderId $id, CustomerId $customer): self
    {
        $order = new self($id, $customer);
        $order->recordThat(new OrderPlaced($id, $customer));

        return $order;
    }

    public function addLine(ProductId $product, int $qty): void
    {
        $this->lines[] = new OrderLine($product, $qty);
        $this->recordThat(new LineAdded($this->id, $product, $qty));
    }
}

Then release only after the transaction is durable. If the bundle's publisher is just an event-dispatcher bridge, prefer an explicit release in your command handler over relying on a Doctrine post-flush hook, otherwise a rolled-back transaction still emits events:

$this->em->wrapInTransaction(function () use ($order) {
    $this->orders->save($order);
    $this->publisher->publish(...$order->releaseEvents());
});

If you use Symfony Messenger for side effects, publish to a transport instead of dispatching in-process — that gives you retries and a natural transactional-outbox seam.

Value objects as the unit of validation. Use the base ValueObject for equality/immutability, but keep named constructors (OrderId::fromString(), Money::of()) as the only public entry points, and throw domain-specific exceptions rather than InvalidArgumentException. Private constructors + self:: factories also make Doctrine embedding and test fixtures much easier to reason about.

Repository interface in Domain, implementation in Infrastructure. The bundle doesn't generate repositories; write the interface yourself and bind it in config/services.yaml:

App\Order\Domain\OrderRepository:
    alias: App\Order\Infrastructure\DoctrineOrderRepository

This keeps the domain layer free of EntityManagerInterface and is the single highest-value DDD practice regardless of what the bundle provides.

Layer your tests. Aggregate and value-object tests should run without a kernel (new Order(...), assert recorded events). Only repository tests need KernelTestCase + Doctrine\Bundle\DoctrineBundle\.... Because the base classes are plain PHP, this split works well and is fast.

Integration tip — keep the domain events small and serializable. If events cross Messenger, they need to be plain-data or have Symfony Serializer normalizers. Value objects (IDs, Money) should get their own normalizers rather than leaking into the transport as opaque objects.

Gotchas and Tips

  • Archived and unmaintained. No security fixes, no Symfony 8 support, no PHP 8.5 compatibility guarantee. Pin the exact version in composer.json, and budget a half-day to vendor the base classes into src/Domain/Shared/ if you plan to keep the codebase past a year. The abstractions are ~a few hundred lines; forking is cheap and removes the supply-chain question.
  • Auto-configuration is aggressive. Bundles that scan for AggregateRoot subclasses or tag services can pick up test fixtures, DTOs, or classes in tests/. Check debug:container and scope the compiler pass with an explicit namespace in your config if it offers one.
  • Double dispatch on retry. Any postFlush/kernel.terminate publisher will re-emit events if the same aggregate is flushed again without clearing its event list. Always releaseEvents() (clear-then-return) rather than getEvents(), and clear in a finally.
  • Doctrine hydration + base-class constructors. Base entity classes often define a constructor or id property that fights with Doctrine's reflection-based hydration (private props on parents, __construct signatures). If you see "cannot access private property" or a null ID after find(), the base class is the culprit — either mark properties protected or use your own base.
  • Event ordering. In-process dispatch during flush() means listeners run before the DB transaction commits; a listener that queries the same aggregate may see stale state. Prefer after-commit dispatch or Messenger.
  • Serialization of IDs. UUID/ULID value objects need Normalizer/Denormalizer registration for Messenger and for API responses; don't assume the bundle's Doctrine type covers transport serialization.
  • Debugging: php bin/console debug:event-dispatcher <EventClass> to confirm your domain event actually reaches listeners; debug:container --tag=… to see tagged handlers; enable doctrine.dbal.logging to catch flush ordering issues.
  • Extension points to prefer: decorate the bundle's publisher service (decorates:) to add logging/outbox/after-commit semantics rather than editing vendor code; alias repository interfaces in services.yaml; if the bundle exposes compiler-pass options, set them explicitly so upgrades/forking are predictable.
  • Don't over-adopt. With 13 stars and 0 dependents, there's no ecosystem of examples or Stack Overflow answers. Use the package as a reference implementation and a starting scaffold, keep your domain classes free of bundle-specific types in public signatures, and you can swap it out in an afternoon.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons