net-code / laravel-domain
DDD tactical building blocks (aggregate root, value objects, domain events, typed UUID identifiers, business-rule specification) with optional Laravel integration.
Package info
github.com/Net-Tech-Marek-Rode-Sp-z-o-o/laravel-domain
pkg:composer/net-code/laravel-domain
Requires
- php: ^8.5
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- ramsey/uuid: ^4.9
Requires (Dev)
- illuminate/events: ^13.0
- laravel/pint: ^1.27
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.2
README
DDD tactical building blocks — the reusable kernel that concrete bounded contexts build on.
The core (NetCode\Domain\*) is pure PHP 8.5+ (only ramsey/uuid); a thin, optional
Laravel integration layer (NetCode\Domain\Laravel\*) wires the kernel into a Laravel app
through an auto-discovered service provider.
Install
composer require net-code/laravel-domain
What's inside
| Class / interface | Purpose |
|---|---|
NetCode\Domain\AggregateRoot |
Base aggregate that records and releases domain events. |
NetCode\Domain\ValueObject |
Base value object with an equals() contract. |
NetCode\Domain\DomainEvent |
Marker interface for events (occurredOn()). |
NetCode\Domain\DomainEventPublisher |
Publisher seam implemented by the host application. |
NetCode\Domain\Identifier\Uuid |
Base for typed UUID identifiers (UUIDv7). |
NetCode\Domain\Rule\BusinessRule |
A single invariant that can be broken. |
NetCode\Domain\Rule\BusinessRuleCode |
Backed-enum contract for machine-readable rule codes. |
NetCode\Domain\Rule\TranslatableRuleParams |
Optional translation parameters for a rule. |
NetCode\Domain\Rule\Specification |
Checks rules and throws BusinessRuleException on the first broken one. |
NetCode\Domain\Rule\BusinessRuleException |
Carries the broken rule's code and translation params. |
NetCode\Domain\Exception\DomainException |
Base for domain-layer exceptions. |
NetCode\Domain\Exception\InvalidArgumentException |
Invalid value-object input. |
Laravel integration (NetCode\Domain\Laravel)
| Class | Purpose |
|---|---|
DomainServiceProvider |
Auto-discovered; binds DomainEventPublisher → LaravelDomainEventPublisher. |
LaravelDomainEventPublisher |
Publishes domain events through Laravel's event dispatcher. |
IdentifierCast |
Eloquent cast between a string column and a typed Identifier\Uuid. |
Usage
Typed identifier
use NetCode\Domain\Identifier\Uuid; final class OrderId extends Uuid {} $id = OrderId::random(); // UUIDv7 $id->equals(OrderId::fromString((string) $id)); // true
Aggregate + domain events
use NetCode\Domain\AggregateRoot; final class Order extends AggregateRoot { public function place(): void { // ... mutate state ... $this->recordThat(new OrderPlaced(/* ... */)); } } $events = $order->releaseEvents(); // hand to your DomainEventPublisher
Business rules via a specification
use NetCode\Domain\Rule\Specification; Specification::check( new OrderMustHaveLines($order), new OrderMustBeUnpaid($order), ); // throws BusinessRuleException on the first broken rule
Specification::check() is a static, stateless guard — call it directly from your aggregates.
There is no instance to construct, inject, or subclass.
Laravel integration
DomainServiceProvider is auto-discovered, so DomainEventPublisher resolves to
LaravelDomainEventPublisher (domain events go through Laravel's dispatcher) with zero wiring.
Cast a model's identifier column to its typed value object:
use Illuminate\Database\Eloquent\Model; use NetCode\Domain\Laravel\IdentifierCast; final class OrderModel extends Model { /** @return array<string, string> */ protected function casts(): array { return ['id' => IdentifierCast::class.':'.OrderId::class]; } }
Development
composer check # pint --test + phpstan (level 8) + phpunit
License
MIT