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

Laravel Ddd Laravel Package

lunarstorm/laravel-ddd

A Laravel toolkit for domain-driven design: scaffold bounded contexts, aggregates, value objects, events, and commands with Artisan generators, plus conventions and service providers for keeping complex apps modular and maintainable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package with Composer and publish its config so you can control where bounded contexts live:

composer require teylabs/laravel-ddd --dev
php artisan vendor:publish --tag=ddd-config

The first thing to do is enumerate the commands rather than read the source — php artisan list ddd gives you the full generator surface (ddd:context, ddd:model, ddd:action, ddd:command, ddd:event, ddd:listener, ddd:repository, ddd:dto, and friends). Each command mirrors a make:* counterpart, so muscle memory transfers almost directly; the difference is that paths resolve into your configured domain root (typically app/Domain or src/Domain) instead of app/.

Set two things in config/ddd.php before generating anything real: the root namespace/path pair for your domains, and the base namespace used for event resolution (see the v3.3.0 listener change below). Getting these wrong means every generated file needs hand-editing, which defeats the point.

The canonical first use case is scaffolding one bounded context end-to-end: a context directory, one aggregate model with its repository interface, and a single command/action/event trio to prove the wiring. Do that before touching an existing app — it takes five minutes and shows you the package's opinions about layering (Domain vs. Application vs. Infrastructure) which are otherwise implicit.

Implementation Patterns

Scaffold a context, then fill in. ddd:context Billing creates the directory skeleton. From there, generate each artifact inside it. The generators produce class stubs with the right namespace, imports, and interface implementations — treat them as a fast correct-by-construction starting point, not as code you leave alone.

Layer-by-layer generation order. In practice: model/aggregate → value objects and DTOs → repository interface (Domain) → repository implementation (Infrastructure) → command + handler → action → event → listener. Generating top-down means each subsequent stub can reference classes that already exist, which keeps you from hand-fixing use statements.

Listeners as the seam between domain and app. The v3.3.0 change makes ddd:listener domain-first: --event=InvoicePaid now wires to Domain\Billing\Events\InvoicePaid. This is the right default for a DDD package — listeners reacting to your own domain events is the common case, and application/framework events are the exception. Reach for the fully-qualified form only when you genuinely need to bridge to Laravel's event system.

Mix with make:* deliberately. Laravel-native artifacts (middleware, notifications, mailables, console commands) are usually better generated by make:* and left in app/, with the domain depending on them via interfaces. Don't force every class through ddd:*; the package is opinionated about domain code, not about the whole application.

CI and scaffolding scripts. If you script generation (e.g. in a module bootstrap or a test-fixture builder), v3.3.0 changes the contract: generators now exit 0 on "already exists" and on PHP-reserved names. Any wrapper that relied on a non-zero exit to mean "skip, already scaffolded" needs rewriting — check for file existence before invoking, or parse the command output. Detecting existence up front is the more robust option and survives future exit-code drift.

Idempotent re-runs. Because existing files are left untouched and the command now exits successfully, you can safely re-run a generation script against a partly-scaffolded domain. That's a genuine ergonomic win for onboarding scripts and for regenerating after a merge.

Gotchas and Tips

The v3.3.0 exit-code change is the breaking change to watch. ddd:* commands now exit 0 when the target class already exists or the name collides with a PHP reserved word — matching make:*. Previously most exited 1. Shell chains like php artisan ddd:model Invoice || echo "exists" will silently stop firing, and stricter CI (set -e with an inverse check) can flip meaning without any error. The error text is still printed and the existing file is still untouched, so the failure mode is a silent success, not data loss. Audit any &&/||/$? logic around these commands.

Listener event resolution changed and is now namespace-sensitive. ddd:listener --event=InvoicePaid targets the listener's own domain event namespace. Two consequences:

  • Passing a bare name to listen for an application event no longer works — you'll silently register a listener for a domain event class that may not exist. Pass the FQCN: --event="App\Events\InvoicePaid".
  • A name containing a backslash is used verbatim, with no namespace prefixing. That makes the behaviour predictable, but it also means the interactive event picker's suggestions and a hand-typed FQCN take different code paths in your head — always quote the argument so the shell doesn't eat the backslashes.

Reserved names now pass silently. Generators exit cleanly when the class name is reserved by PHP (List, Match, Enum, Readonly, …). Combined with the exit-code change, a bulk generation loop over a user-supplied name list can skip entries with no signal other than stdout. Grep the output for the error line if the script must be exact.

Resolve the domain root once. Config drift between config/ddd.php and your PSR-4 autoload map is the most common source of "generated but not found" errors. If you rename a namespace, update both in the same commit; generated files carry hard-coded namespaces and won't be rewritten.

Extension point: stub overrides. Publish the stubs (vendor:publish --tag=ddd-stubs or equivalent) and edit them to match your team's conventions — constructor promotion, final classes, strict types, readonly properties. One-time cost, saves editing every generated file forever.

Debugging generation. When a command produces something unexpected, run it with -v to see the resolved target path. Nine times out of ten the class landed in a directory you didn't expect because the domain root resolved relative to a config value you forgot was there.

Version-pin scaffolding in shared tooling. Because exit codes and event resolution are both behavioural contracts that have now changed once, pin the package to a minor range (^3.3) in any repo where scripts call these commands, and read the changelog on bump.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource