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

Doctrine Value Object Laravel Package

yokai/doctrine-value-object

yokai/doctrine-value-object brings reusable value objects to Doctrine in Laravel apps, offering embeddable types and integration helpers for cleaner, type-safe domain models with less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Doctrine ORM package, not an Eloquent one. In a Laravel app you'll need Doctrine wired up first (laravel-doctrine/orm or a standalone EntityManager bound in a service provider); on an Eloquent-only codebase, stop here and use Attribute casts instead.

composer require yokai/doctrine-value-object

Where to look first, in order:

  1. src/ValueObject.php — the toValue() / fromValue() contract every value object implements.
  2. src/StringValueObject.php (and the Integer/Float/Boolean siblings) — the base classes you'll actually extend.
  3. src/Doctrine/ — the #[ValueObject] attribute and the event subscriber that converts between the DB scalar and your object. Read these two before mapping anything.

First use case: collapse a primitive into a domain type.

use Yokai\DoctrineValueObject\StringValueObject;

final class Email extends StringValueObject
{
    public function __construct(string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Invalid email: {$value}");
        }
        parent::__construct($value);
    }
}
use Yokai\DoctrineValueObject\Doctrine\Attribute\ValueObject;

class User
{
    #[ORM\Column(type: 'string', length: 180)]
    #[ValueObject(Email::class)]
    private Email $email;
}

Then register the package's Doctrine event subscriber on the EntityManager's event manager (automatic under Symfony; manual in Laravel). Without that registration the attribute is inert and hydration will fail.

Implementation Patterns

Validation lives in the constructor. The base class stores the raw scalar; your subclass enforces invariant rules and passes the clean value up. Because the subscriber reconstructs via fromValue()/constructor during hydration, bad data already in the DB will throw at postLoad time — which is the point, but plan a migration/backfill if you're retrofitting existing tables.

Match the column type to toValue(). StringValueObject → string, IntegerValueObject → integer, etc. The Doctrine mapping stays a plain scalar column; the value object never gets its own DBAL type, which keeps the schema readable and query-able.

Nullable columns. If the column is nullable, guard in the value object or use a nullable-typed property. The subscriber will pass null through, so a non-nullable constructor param will blow up on rows with NULL.

Custom / composite objects. For something the base classes don't cover (a Money, a coordinate pair), implement the ValueObject interface directly with toValue() and fromValue(), store the composite as a JSON or delimited string column, and point #[ValueObject(YourClass::class)] at it. This is the main extension seam.

Collections. Store an array of scalars in a json/simple_array column and wrap it in a value object whose fromValue() rehydrates the members. Keep it small — Doctrine's change tracking only sees the property as a whole.

Testing. Boot an in-memory SQLite EntityManager in the test, register the subscriber exactly as you do in production, and assert find() returns real value objects and flush() persists the primitive. That catches the "forgot to register the subscriber" class of bug immediately.

Gotchas and Tips

  • Not Eloquent. This package has no Laravel integration, no service provider, no facade. Everything is Doctrine's EventManager. If your app is Eloquent-first, the equivalent is protected $casts = ['email' => EmailCast::class] with CastsAttributes — different trade-off, same idea, and worth writing yourself rather than pulling this in.
  • Silent no-op failure mode. If you forget to register the subscriber, the attribute is decoration. Doctrine loads a string into a property typed Email and PHP throws a TypeError deep in the hydrator. First debugging step is always "is the subscriber attached to this manager?" — log getEventManager()->hasListeners(Doctrine\ORM\Events::postLoad).
  • postLoad only. Objects built with new, returned from getReference(), or materialised through DQL partials bypass the listener. Same for merge() in some configurations. Test those paths if you rely on them.
  • Immutability vs. Doctrine proxies. Value objects should be readonly/immutable, but mutable-proxy entities and readonly properties interact badly. Keep the property non-readonly on the entity side and expose a changeEmail() method that reassigns.
  • Small ecosystem. 13 stars, last release July 2024, zero dependents. Pin the version in composer.json and read src/Doctrine/ yourself — it's a few hundred lines and you may end up copying the subscriber into your app if the package stalls.
  • Config quirk. If you run multiple entity managers, register the subscriber on every manager that has value-object-mapped entities; the attribute is per-property and the listener has no global default.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core