somnambulist/domain
Domain-driven design building blocks for Laravel and PHP: UUIDs, value objects, entities, aggregates, domain events, commands and repositories. A clean, framework-friendly foundation for modelling business logic with clear boundaries.
This is a library of base classes, not a Laravel package: no service provider, no facade, no config/domain.php, no auto-discovery. You pull in the pieces you want and wire them yourself. That is the whole mental model.
composer require somnambulist/domain
Look in two places first, in this order:
src/Entities/ — AbstractEntity, AbstractIdentity, AbstractValueObject, AbstractEnum, AbstractUuid, AbstractCollection, plus Behaviours/ traits (timestampable, identifiability, soft-delete-style flags). These are the day-to-day workhorses.src/Events/ — AbstractDomainEvent, DomainEventPublisher, and the recording trait(s) you apply to aggregates. This is the part that needs integration work with Laravel.Version caveat: from v4 onward many ready-made value objects (EmailAddress, Uuid, DateTime, Money, Country, Coordinate) were split into the sibling somnambulist/type package. Check what you actually get before assuming Somnambulist\Domain\Entities\Types\... exists.
First use case — one vertical slice. Model a single aggregate with an identity that raises one event:
final class OrderId extends AbstractIdentity {}
final class Order extends AbstractEntity
{
use DomainEvents; // records events locally
public static function place(OrderId $id, EmailAddress $email): self
{
$order = new self($id);
$order->recordDomainEvent(new OrderWasPlaced($id->toString(), $email->toString()));
return $order;
}
}
Persist it, release the recorded events, publish them through Laravel's dispatcher in an AppServiceProvider. If that loop works end to end, you understand the package.
Value objects and identities. Extend AbstractValueObject / AbstractIdentity and you get equals(), toString(), __toString() for free. Identities are compared by class + scalar, which makes them safe as array keys after (string) casting and safe in in_array checks across collections. Use AbstractUuid (or the somnambulist/type equivalent) rather than sprinkling Str::uuid() through controllers — it gives you a single place to enforce format and a type you can hand to a repository.
Aggregate roots record; a service publishes. Keep the two halves separate:
$order = Order::place($id, $email);
$orders->store($order);
// at the edge, after persistence succeeds
DomainEventPublisher::instance()->publish(...$order->releaseEvents());
Never publish inside the entity constructor or a setter. Record, persist, then publish. This is the pattern that makes the package worth using — you get "events only if the write succeeded" for free, no transactional outbox required at small scale.
Wiring the publisher into Laravel. DomainEventPublisher::instance() is a process-wide singleton that expects a PSR-14/Symfony-style dispatcher. Laravel's Illuminate\Contracts\Events\Dispatcher is not that interface, so write a thin adapter once and register it in AppServiceProvider::register():
final class LaravelEventDispatcherAdapter implements EventDispatcherInterface
{
public function __construct(private Dispatcher $events) {}
public function dispatch(object $event, ?string $eventName = null): object
{
$this->events->dispatch($event);
return $event;
}
// addListener/removeListener etc.: no-op or delegate.
}
After that, normal Event::listen(OrderWasPlaced::class, ...) listeners, queued listeners, and Event::fake() all see your domain events. Nothing else in the app needs to know the package exists.
Eloquent lives at the boundary. AbstractEntity is a plain PHP object; it is not an Illuminate\Database\Eloquent\Model and never will be. Either (a) map it to/from an Eloquent model in a repository, or (b) use custom casts on the Eloquent model so the value objects survive the round trip:
protected $casts = [
'id' => OrderIdCast::class, // get: new OrderId($v); set: $v->toString()
'email' => EmailAddressCast::class,
];
Pattern (b) is the low-friction option for CRUD-ish apps; pattern (a) is what the package was designed for. Mixed codebases usually end up with (b) for value objects and (a) for the aggregate.
Enums before PHP 8.1. AbstractEnum gives you members(), create(), isValid(), and typed constants that throw on bad input. If you are on 8.1+ prefer native backed enums and use the package only for entities/events — do not maintain both.
setEventDispatcher() on the singleton, publish() is a no-op. No exception, no log line. This is the #1 "why isn't my listener firing" cause. Assert the dispatcher is set in a boot test.setUp()/tearDown(), or better, bind a fresh dispatcher per test. Event::fake() alone does not help if the adapter was constructed before the fake was installed.releaseEvents() typically clears the buffer. If store() throws afterwards, you have published events for a write that never happened. Do the release/publish after the transaction commits (DB::afterCommit()), not inside it.InvalidArgumentException (via the underlying assert library), not ValidationException. Catch at the FormRequest/controller boundary and convert to a 422, or validate the raw string first and construct the VO second.Behaviours/, Events/DomainEvents, DomainEventRecorder, PublishableDomainEvents). With 26 stars and effectively zero downstream packages, there is no ecosystem churn to warn you about breaking changes — pin the major version in composer.json and read the trait you actually installed.serialize() for queued listeners. AbstractDomainEvent payloads should be scalars; flatten to strings in the constructor rather than handing the payload a DateTimeImmutable you then have to reconstruct.=== and not ==. Use equals() everywhere, including in your own collections and array_unique-style de-duplication, otherwise you get duplicate rows for the "same" identity built twice.AbstractDomainEvent for a shared payload envelope/base name; implement your own collection by extending AbstractCollection so you get typed filter/map callbacks; and put all VO↔scalar casting in one place (a trait of cast classes) so the Eloquent boundary stays boring.Verdict for day-to-day use: adopt it if you are deliberately building a domain layer inside a Laravel app and want pre-built identity/value-object/domain-event primitives without installing Doctrine. Skip it if you want an out-of-the-box Laravel experience — you will spend more time on the dispatcher adapter and the Eloquent mapping than on the domain code itself, and with no dependents there is nobody else's integration to copy.
How can I help you explore Laravel packages today?