Download the PHP package lava83/laravel-ddd without Composer
On this page you can find all versions of the php package lava83/laravel-ddd. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download lava83/laravel-ddd
More information about lava83/laravel-ddd
Files in lava83/laravel-ddd
Package laravel-ddd
Short Description A comprehensive toolkit providing foundational building blocks for implementing Domain-Driven Design (DDD) patterns in Laravel 12+ applications. This package offers battle-tested base classes, contracts, and infrastructure components to help you build scalable, maintainable domain-driven applications.
License MIT
Homepage https://github.com/lava83/laravel-ddd
Informations about the package laravel-ddd
Laravel DDD
Work in progress. The public API may still change between releases.
Foundational building blocks for Domain-Driven Design in Laravel. The package ships battle-tested base classes and contracts — Entities, Aggregates, Value Objects, Repositories, Entity ↔ Model Mappers and Domain Events — so your applications can focus on the domain instead of the plumbing.
It enforces a strict layer separation (Domain, Application, Infrastructure) and gives you optimistic locking, automatic domain-event dispatching on save, and a set of ready-made value objects out of the box.
Requirements
- PHP 8.4+
- Laravel 13 (
illuminate/contracts^13.0)
Installation
The service provider and the LaravelDdd facade are registered automatically through Laravel package discovery. There are no migrations to publish, and no configuration is required — you build your own domains on top of the provided base classes, as shown below. An optional config file tunes the make:aggregate scaffolder (see Scaffolding).
Quick start
The example models a single Article aggregate with a Title value object and persists it through a mapper and a repository. It is the smallest slice that still exercises every core building block: a value object, an aggregate, an Eloquent model, a mapper and a repository.
Suggested structure inside a consuming application:
1. Value objects
An identity and a small, self-validating value object. Both are immutable.
2. Aggregate
State changes go through updateAggregateRoot(), which tracks the change and bumps the version (and can record a domain event — see What else is in the box). Business rules live here, never in the application or infrastructure layer.
3. Eloquent model & migration
Extend the package Model — it provides a UUID primary key (via the HasUuids concern), version tracking, timestamp casts and a filtering layer. Point the model at its entity so toEntity() can resolve the mapper.
The id, version, created_at and updated_at columns are handled by the base model, so the migration only adds them plus your own fields:
4. Mapper
The mapper is the single translation point between the domain and the database. findOrCreateModelFillData() (from the base mapper) loads or creates the row and fills the shared columns (id, version, timestamps) for you.
5. Repository
Keep the contract in the domain layer and the Eloquent implementation in the infrastructure layer. The base Repository provides saveEntity() / deleteEntity(), the optimistic-locking check and automatic domain-event dispatching; you add the read methods your application needs.
6. Wire it up
Register the mapper with the resolver and bind the repository contract to its implementation. A dedicated service provider keeps this in one place.
Register the provider in bootstrap/providers.php:
7. Use it
Scaffolding
Rather than writing every class by hand (as the Quick start does), make:aggregate generates the building blocks for an aggregate in a bounded context: the identity value object, the aggregate root and the Eloquent model, and — optionally — a repository (contract plus Eloquent implementation) and an entity mapper.
Run it interactively and answer the prompts (aggregate name, bounded context, identity type, and whether to also create a repository and a mapper):
Or pass everything up front:
Options
name— the aggregate name (e.g.Order); prompted when omitted.bounded-context— the context it lives in (e.g.OrderProcessing); prompted when omitted.--with-repository— also generate the repository contract and its Eloquent implementation.--with-entity-mapper— also generate the entity mapper.--id-type=— identity type,uuid(default) orinteger.--force— overwrite existing files instead of skipping them.
In non-interactive contexts (e.g. CI) the prompts are skipped: arguments and options drive everything, the identity type defaults to uuid, and the repository and mapper are generated only when their flags are present.
What it generates
Always:
{Name}Id— identity value object extending the packageUuidorIntegerbase.{Name}— the aggregate root, typedAggregate<{Name}Model, {Name}Id>, withcreate()/fromState()factory methods and avalidate()hook.{Name}Model— an Eloquent model extending the packageModel, with a#[Table]attribute derived from the snake-cased plural name (and theHasUuidsconcern for UUID identities).
On request:
--with-repository→{Name}RepositoryContract(findAll,findOrFail,save,remove) andEloquent{Name}Repository.--with-entity-mapper→{Name}MapperwithtoEntity()/toModel().
The generated files are skeletons: the model and mapper carry a name placeholder column, and the aggregate's validate() and the mapper's toModel() are left for you to complete. Existing files are reported as SKIPPED (exists) and left untouched unless you pass --force. After writing, the command prints the service-provider bindings to register — the mapper via entity_mapper_resolver()->registerMapper(...) and the repository via $this->app->bind(...) — and warns if the target root namespace isn't autoloaded yet.
Namespaces
Two keys in config/laravel-ddd.php decide where the classes land:
bounded_contexts_root_namespace— root namespace for generated code (defaultApp\BoundedContexts).bounded_contexts_without_own_layers— whether bounded contexts share the layer namespaces (defaulttrue).
With the defaults, make:aggregate Order OrderProcessing --with-repository --with-entity-mapper writes:
Set bounded_contexts_without_own_layers to false and each context owns its layers instead — the context and layer segments swap, e.g. App\BoundedContexts\OrderProcessing\Domain\Aggregates\Order.
Target paths are resolved from your composer.json PSR-4 map; if the root namespace isn't mapped yet, the command prints the autoload entry to add and reminds you to run composer dump-autoload.
Publish the config to change these defaults:
What else is in the box
Beyond the slice above, the package provides an AggregateRoot contract with domain-event recording: events collected through updateAggregateRoot($changes, $eventClass) are dispatched automatically via Laravel's event system after a successful save(), then cleared from the aggregate. Every aggregate carries a version for optimistic locking and raises a ConcurrencyException on conflicting writes. You also get a growing catalogue of ready-made value objects — Uuid, Email, Phonenumber, Money, Link, Json, GeoAddress and more — plus a fluent Eloquent filtering layer on the base Model (see Filtering).
Filtering
The base Model ships with a filtering layer built on indexzer0/eloquent-filtering. Infrastructure\Models\Filter\Builder composes a set of filters fluently and serialises them — via toArray() — to the operator-array shape the model's filter() query scope consumes.
Building and applying filters
toArray() produces one MongoDB-style operator entry per filter ($eq, $gte, $in, $null, …):
Reconstructing filters from a request
Builder::fromArray() is the inverse of toArray() — it rebuilds a Builder from that array shape, for example from filters that arrive over HTTP. It validates strictly and throws Filter\Filters\Exceptions\FilterArrayNotValid on a missing key, an unknown operator, or a value whose type does not match the operator.
Because the check is strict, carry the filters as JSON rather than as bracket-notation query parameters. PHP parses every query-string value as a string, so ?filters[0][type]=$gte&filters[0][value]=18 yields the string "18" — which the numeric operators ($gt, $gte, $lt, $lte) and $null reject, while string-friendly operators like $eq and $in would still pass, making bracket notation deceptively half-working. A JSON payload preserves int and bool:
URL-encode the
filtersvalue in practice ([→%5B,"→%22,$→%24, …); it is shown decoded here for readability.
On the server, decode the JSON and hand the array to fromArray():
The request above reconstructs exactly:
Development
License
The MIT License (MIT). See LICENSE.md for details.
All versions of laravel-ddd with dependencies
giggsey/libphonenumber-for-php Version ^9.0
illuminate/contracts Version ^13.0
indexzer0/eloquent-filtering Version ^2.2
lava83/laravel-sqid Version ^0.2.0
spatie/laravel-data Version ^4.18
spatie/laravel-package-tools Version ^1.16