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

Domain Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. src/Entities/ — AbstractEntity, AbstractIdentity, AbstractValueObject, AbstractEnum, AbstractUuid, AbstractCollection, plus Behaviours/ traits (timestampable, identifiability, soft-delete-style flags). These are the day-to-day workhorses.
  2. 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.

Implementation Patterns

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.

Gotchas and Tips

  • Events vanish silently. If you never call 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.
  • The publisher is global state. PHPUnit tests leak events and dispatchers between cases. Reset in 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.
  • Release before you publish, and only after commit. 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.
  • Value objects throw, they don't validate. Bad input raises 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.
  • Method names drift across majors. Traits have been renamed and moved (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.
  • Queue serialization. Domain events carrying value objects must survive 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.
  • Equality is not === 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.
  • Extension points worth knowing: subclass 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.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
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