ahmedsaed/laravel-ddd

Lightweight DDD structure and architecture tooling for Laravel

Maintainers

Package info

github.com/ahmedsaed27/laravel-ddd

Homepage

pkg:composer/ahmedsaed/laravel-ddd

Transparency log

Fund package maintenance!

Ahmed Saed

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-07-30 13:04 UTC

This package is auto-updated.

Last update: 2026-07-30 14:26:55 UTC


README

Laravel DDD adds bounded-Context generators, architecture checks, and Context-aware database commands to a normal Laravel 11–13 application. It keeps Laravel’s container, events, queues, mail, routes, views, Eloquent, Pest, and PHPUnit as the integration APIs; it does not add wrapper frameworks around them.

Install

composer require ahmedsaed/laravel-ddd

New applications use project-root contexts/ and the Contexts namespace. Add the namespace to the application’s composer.json:

{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Contexts\\": "contexts/"
        }
    }
}

Then rebuild the autoloader:

composer dump-autoload

The path and namespace remain configurable in config/laravel-ddd.php for applications with an existing convention.

The generated Context

php artisan ddd:make-context Order

creates this case-sensitive structure:

contexts/Order/
├── Config/
│   └── config.php
├── Providers/
│   └── OrderServiceProvider.php
├── Domain/
│   ├── Aggregates/
│   ├── Entities/
│   ├── Events/
│   ├── Repositories/
│   └── ValueObjects/
├── Application/
│   ├── DTOs/
│   ├── Queries/
│   └── UseCases/
├── Infrastructure/
│   ├── Listeners/
│   ├── Jobs/
│   ├── Mail/
│   ├── Integrations/
│   ├── Persistence/
│   │   └── Eloquent/
│   │       ├── Models/
│   │       ├── Migrations/
│   │       ├── Seeders/
│   │       └── Factories/
│   └── Search/
├── Presentation/
│   ├── Console/
│   │   └── Commands/
│   └── Http/
│       ├── Controllers/
│       ├── Middleware/
│       ├── Requests/
│       ├── Resources/
│       └── Routes/
│           ├── api.php
│           └── web.php
├── resources/
│   ├── assets/
│   └── views/
└── tests/
    ├── Feature/
    └── Unit/

Config and Providers are uppercase. resources and tests are lowercase. This matters on Linux.

The command creates directories and framework bootstrap files, not fictional business classes or frontend entry files. It also creates the conventional empty OrderDatabaseSeeder.

Optional Aggregate and Vite setup

php artisan ddd:make-context Order --with-aggregate
php artisan ddd:make-context Order --with-vite
php artisan ddd:make-context Order --with-aggregate --with-vite

--with-aggregate adds only:

Domain/Aggregates/Order.php
Domain/Repositories/OrderRepository.php

Both classes are intentionally empty. --with-vite adds only contexts/Order/vite.config.js. It uses the root application’s Node dependencies, a Context-specific hot file, and a Context-specific build directory. It does not create a Context package.json or pretend that nonexistent JavaScript/CSS entry files exist. Add real entries to the generated empty input array when the Context gains frontend assets.

--force repairs framework scaffolding and missing directories. It preserves the entry seeder and optional Aggregate/Repository business files.

A Laravel-oriented DDD vocabulary

  • An Aggregate protects a transactional business invariant.
  • An Entity has identity and lifecycle inside a business model.
  • A Value Object represents an immutable value such as money.
  • A Domain Event records a business fact in Domain/Events.
  • A Repository interface describes Aggregate persistence in Domain.
  • A Use Case coordinates one application action in Application/UseCases.
  • A DTO carries application input or output in Application/DTOs.
  • A Laravel Listener, Job, or Mailable is an Infrastructure adapter.
  • A Form Request, controller, middleware, or Resource is a Presentation adapter.
  • The root Context Service Provider is the composition root.

Dependencies point inward:

Presentation → Application → Domain
Infrastructure → Application and Domain

Domain contains business rules and no Laravel/Symfony dependency, with one deliberate exception: a class in Domain/Events may use Illuminate\Foundation\Events\Dispatchable. This narrow compromise makes a Domain Event feel native in Laravel without allowing facades, helpers, Eloquent, queues, mail, HTTP, or the container into the rest of Domain.

Application owns DTOs, Queries, and Use Cases. It may directly dispatch an Aggregate’s recorded events after a successful save, but it does not use the database, Eloquent, cache, HTTP, mail, or queue adapters.

Infrastructure and Presentation are normal Laravel code. They may use the container, facades, and helpers appropriate to their boundaries.

Native Laravel events

Generate a Domain Event and a typed Listener:

php artisan ddd:make-event Order:OrderPlaced
php artisan ddd:make-listener Order:SendOrderReceipt \
    --event='Contexts\Order\Domain\Events\OrderPlaced'

The event stub contains Laravel’s event Dispatchable trait, so all standard styles work:

event(new OrderPlaced($orderId));
Event::dispatch(new OrderPlaced($orderId));
OrderPlaced::dispatch($orderId);

Listeners live directly in Infrastructure/Listeners. The package adds contexts/*/Infrastructure/Listeners to Laravel’s supported event-discovery paths. Laravel infers the event from the first argument:

final readonly class SendOrderReceipt
{
    public function __construct(
        private PrepareOrderReceipt $prepareReceipt,
    ) {}

    public function handle(OrderPlaced $event): void
    {
        SendOrderReceiptJob::dispatch(
            $this->prepareReceipt->execute($event),
        );
    }
}

There is no provider event map. Do not bind the Listener or the concrete Use Case: Laravel resolves both through constructor injection. Repository interfaces and other replaceable ports still need explicit bindings.

Laravel’s operational commands work with Context listeners:

php artisan event:list
php artisan event:cache
php artisan event:clear

When dispatch happens after persistence, record events in the Aggregate, save it, then release and dispatch them:

$this->orders->save($order);

foreach ($order->releaseEvents() as $event) {
    event($event);
}

Production systems that need atomic database/event delivery should add an outbox rather than dispatch before the transaction commits.

Native Laravel helpers

Context classes do not need package wrappers. In Infrastructure and Presentation, normal Laravel calls work:

event($event);
Event::dispatch($event);
OrderPlaced::dispatch($orderId);

dispatch(new SendOrderReceiptJob($orderId));
SendOrderReceiptJob::dispatch($orderId);
dispatch_sync(new RebuildOrderReadModel($orderId));

config('order.notification_recipient');
view('order::orders.show', ['order' => $order]);
route('order.orders.show', $order);
app(PrepareOrderReceipt::class);
cache()->put('order:last', $order->id);
logger()->info('Order placed');
response()->json($payload);
now();

Jobs may use the sync queue connection in local and test environments. The same dispatch APIs remain valid.

Context Service Provider

Providers/OrderServiceProvider.php:

  • merges Config/config.php under the snake-case order key;
  • loads Presentation/Http/Routes/api.php and web.php;
  • loads Context migrations;
  • registers resources/views as the order view namespace.

For example:

view('order::orders.show');

The provider’s register() method is where repository interfaces and other replaceable ports are bound:

$this->app->bind(
    OrderRepository::class,
    EloquentOrderRepository::class,
);

Providers are discovered from each direct child of contexts/. Explicit registration in bootstrap/providers.php is also supported. The former Infrastructure/Providers location remains a discovery-only compatibility fallback; if both files exist, only the root provider is registered.

Component commands

All class commands use Context:ClassName, print their target, protect existing files unless --force is supplied, and reuse the same configured path/namespace system.

Command Generated location
ddd:make-aggregate Domain/Aggregates
ddd:make-entity Domain/Entities
ddd:make-value-object Domain/ValueObjects
ddd:make-event Domain/Events
ddd:make-repository Domain/Repositories
ddd:make-dto Application/DTOs
ddd:make-use-case Application/UseCases
ddd:make-model Infrastructure/Persistence/Eloquent/Models
ddd:make-migration Infrastructure/Persistence/Eloquent/Migrations
ddd:make-seeder Infrastructure/Persistence/Eloquent/Seeders
ddd:make-factory Infrastructure/Persistence/Eloquent/Factories
ddd:make-listener Infrastructure/Listeners
ddd:make-job Infrastructure/Jobs
ddd:make-mail Infrastructure/Mail
ddd:make-controller Presentation/Http/Controllers
ddd:make-request Presentation/Http/Requests
ddd:make-resource Presentation/Http/Resources
ddd:make-middleware Presentation/Http/Middleware
ddd:make-provider Providers
ddd:make-config Config/config.php

Repository generation can add an Eloquent adapter with --eloquent and a mapper with --mapper. Jobs are queued by default; --sync creates a synchronous dispatchable Job. Mailables accept either --view or --markdown. Listener --event values must be fully qualified, and --queued applies Laravel’s queued-listener convention.

Migration inference is conservative:

  • create_orders_table uses Schema::create and drops the table in down;
  • safely inferred add/change names use Schema::table;
  • ambiguous names produce a valid neutral migration with no database work and no unused schema imports.

No generator invents fields, relationships, event properties, routes, or business methods.

Context databases

The package provides Context-aware migration and seeding commands:

php artisan ddd:migrate [Order]
php artisan ddd:migrate-status [Order]
php artisan ddd:migrate-rollback [Order]
php artisan ddd:migrate-reset [Order]
php artisan ddd:migrate-refresh [Order]
php artisan ddd:migrate-fresh [Order]
php artisan ddd:seed [Order]

Connection precedence is CLI option, Context config, then Laravel default. Rollback/reset select only migration rows owned by the chosen Context. ddd:migrate-fresh refuses a shared/default physical database and requires database.dedicated=true. Standard php artisan migrate and php artisan db:seed remain available.

See Context databases for the complete option and safety rules.

Context tests

The generator creates lowercase tests/Feature and tests/Unit directories. PHPUnit only discovers directories declared in a test suite, so add the Context root explicitly instead of rewriting phpunit.xml from a generator:

<testsuite name="Context tests">
    <directory suffix="Test.php">contexts</directory>
</testsuite>

For Pest, apply the application Test Case only to Context Feature tests:

$contextFeatureTests = glob(
    dirname(__DIR__).'/contexts/*/tests/Feature',
) ?: [];

uses(Tests\TestCase::class)->in(...$contextFeatureTests);

The example includes an additive phpunit.contexts.xml, Pest setup, and Unit and Feature tests. This keeps application test configuration an explicit developer choice and avoids fragile XML string editing.

Architecture checks

php artisan ddd:check

The checker reports namespace/layer violations, outward Domain dependencies, database/cache/HTTP/mail/queue dependencies in Application, direct cross-Context Aggregate/Entity imports, and obvious misplaced Laravel adapters. Strict mode fails; non-strict mode warns.

It tokenizes PHP namespaces and imports. It cannot prove every dynamic container lookup, helper call, string class name, or runtime dependency, so the documented policy still matters.

Example

examples/laravel-app contains a readable Order/Catalog/Search application slice:

PlaceOrder Use Case
→ Order Aggregate records OrderPlaced
→ OrderRepository saves
→ event(...) dispatches
→ automatically discovered Laravel Listener
→ concrete notification preparation Use Case
→ Laravel Job
→ Mailable and Context view

Catalog owns SQL product writes. Search owns the eventually consistent product read model. ProductCreated is handled by Search’s automatically discovered Listener, which dispatches an indexing Job and delegates to the Search Application layer.

More documentation