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

Vanio Domain Bundle Laravel Package

vanio/vanio-domain-bundle

A Symfony bundle from Vanio for domain-driven design, providing base classes and infrastructure to organize domain models, repositories, and services cleanly within Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer and register the bundle the usual way for the Symfony version you are on:

composer require vanio/vanio-domain-bundle
// config/bundles.php
return [
    // ...
    Vanio\DomainBundle\VanioDomainBundle::class => ['all' => true],
];

There is no dedicated configuration block you have to fight with for a first run — the bundle is mostly a library of base classes, Doctrine mappings and a few services. Read the tests/ directory before the src/ directory. With a package this small (single-digit stars, no downstream dependents published), the test cases are the most reliable specification of intended behaviour; the README is thin and there is no hosted documentation.

A good first use case is replacing a primitive in an existing entity with a value object. Start with something trivial and self-contained, such as an Email or Money value object:

use Vanio\DomainBundle\Model\AbstractValueObject;

final class Email extends AbstractValueObject
{
    /** @var string */
    protected $email;

    public function __construct(string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Invalid email '$email'.");
        }
        $this->email = mb_strtolower($email);
    }

    public function __toString(): string
    {
        return $this->email;
    }
}

Map it as a Doctrine embeddable and wire it into one entity. If that round-trips through persistence, forms and json_encode correctly in your application, you have validated the whole stack the package depends on.

Implementation Patterns

Value objects as embeddables. The dominant pattern in this bundle is "small immutable object owned by an entity", mapped with Doctrine @Embeddable/#[ORM\Embedded]. Extending AbstractValueObject gives you consistent equality semantics (comparison by state, not identity), which is exactly what you want when the same email address appears in a query result and in a loaded entity. Use a private/protected constructor plus named static constructors (Amount::fromCents(), Amount::fromDecimal()) to keep creation explicit and guard invariants at the boundary.

Typed collections instead of arrays. The bundle's collection abstraction is the other half of the value-object story. Use it where you would otherwise write array<OrderLine> docblocks and then hand-roll add/remove/contains logic in the entity. Give the collection a method that expresses the domain operation ($lines->addForProduct($product, $qty)) rather than exposing add() directly, so that invariants stay in one place.

Domain events dispatched after the transaction commits. The interesting part of a DDD bundle is always event ordering. The pattern to aim for is: an entity records events internally ($this->recordThat(new OrderPlaced($this->id))), an aggregate-root base class collects them, and a Doctrine post-flush listener dispatches them once the unit of work has actually succeeded. Verify in a test that a listener thrown during dispatch does not leave you with a committed entity and an undelivered event — that is the failure mode worth writing a regression test for, because it will silently duplicate work the first time flush succeeds and dispatch fails.

Aggregate boundaries. Keep the aggregate root as the only entry point. Repositories return aggregates, aggregates return value objects and collections, and services coordinate. When wiring controllers, inject the repository service and translate HTTP input into value objects before the domain call — do not let form data reach entities directly.

Integration with the wider Vanio stack. This package is designed to sit alongside vanio/vanio-data-bundle and its form/data-mapper helpers. If you find yourself writing a lot of form data transformers for value objects, check whether the sibling bundle already provides the transformer; reimplementing it locally is a common source of drift. Doctrine types registered by the bundle are only useful if the mapping in your entity matches exactly what the type expects — mismatches manifest as opaque ConversionExceptions deep in UnitOfWork.

Testing. Prefer unit tests for value objects (pure, no kernel) and integration tests for embeddable mapping and event dispatch. Value objects are cheap to test exhaustively; the collection and event plumbing is where you want a booted kernel against a real database.

Gotchas and Tips

  • Doctrine embeddables have hard limits. No nullable embeddables at the root level (a classic workaround is a nullable column set inside an AlwaysEmbeddable-style wrapper), no lazy loading, and no association cycles. Decide on null semantics before you write the mapping — retrofitting an "optional" value object into an embeddable is painful.
  • Naming collisions on embeddable columns. Two value objects on the same entity with the same internal property name produce colliding column names. Explicitly set columnPrefix / #[ORM\Embedded(columnPrefix: 'billing_')] from day one, even if you only have one embeddable today.
  • Serialization round-trips. json_encode on an entity will serialise the value object's private state, not what __toString returns. If value objects leak into API responses, add normalizers explicitly rather than relying on JsonSerializable guessing.
  • Low adoption means thin documentation. With effectively no downstream users, treat the source as authoritative. When you hit something odd, read the bundle's event subscriber and service definitions; they are short, and reading them will take less time than searching for an answer that does not exist.
  • Debugging event dispatch. Because domain events are usually dispatched from Doctrine listeners, dump() calls feel like they land in the wrong place. Symfony\Bridge\Doctrine\Middleware\Debug\... or simply tagging your dispatcher with LoggerInterface logging and running with -vvv will show ordering far more clearly than breakpoints in a flush.
  • Do not adopt the whole bundle blindly. Pick the pieces that pay for themselves: value objects and typed collections almost always do. If you only need one value object in the codebase, a plain final class with an equals() method is less surface area. The bundle earns its keep once you have a family of value objects and events that need consistent semantics.
  • Extension points. The event dispatch path and the collection base class are the two places worth subclassing or decorating rather than forking. Register your own listener ahead of the bundle's if you need to enrich events before dispatch, and keep the bundle itself unmodified so future upgrades stay possible.
  • Pin the version. For a package with a single-digit star count, treat minor versions as capable of breaking you. Commit your composer.lock and read the diff on every update.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr