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 Orm Value Object Laravel Package

matthiasnoback/doctrine-orm-value-object

A Doctrine ORM integration for mapping PHP value objects as embeddables, with support for custom types and conversion between value objects and database columns. It helps keep domain models free of ORM concerns while persisting rich value objects transparently.

Deep Wiki
Context7

Getting Started

Install with Composer:

composer require matthiasnoback/doctrine-orm-value-object

The README is the only real documentation — read it before anything else. Then read these three classes, in order, to understand the whole machine:

  1. ValueObjectMappings — the registry that says "value object class X is stored in column Y".
  2. ReflectionValueObjectSerializer — the default serializer; it reads private properties via reflection and reconstructs the object without calling its constructor.
  3. AbstractValueObjectType — the DBAL type that turns the object into a database string and back.

The library solves one specific problem: Doctrine has no way to map an arbitrary value object to a column without writing a custom DBAL type per class. This package lets you declare mappings once, and a loadClassMetadata subscriber rewires the entity's field to a type backed by the serializer.

Minimal bootstrap shape:

$mappings = new ValueObjectMappings();
$mappings->add(EmailAddress::class, 'email');     // VO class => entity field
$mappings->add(Money::class, 'price');

// register the DBAL type(s) and the loadClassMetadata subscriber with
// your EntityManager configuration (see README for the exact wiring)

First use case: give an entity a private EmailAddress $email property with a getter/setter, register the mapping, run schema:update, and verify that persisting a new EmailAddress('[email protected]') writes a string column and re-hydrates into the value object on find().

Implementation Patterns

Register mappings at bootstrap, before metadata loads. Put the ValueObjectMappings construction in your EM factory / service provider. Doctrine caches metadata; if a mapping is missing when metadata is first built, the field type will be wrong until you clear the cache.

Prefer a custom Serializer over reflection for domain objects. The reflection serializer is fine for quick prototypes, but it skips constructors and any validation. Implement the Serializer interface and round-trip through a named constructor so invariants hold:

final class EmailAddressSerializer implements Serializer
{
    public function serialize($object): string
    {
        return (string) $object;                 // EmailAddress::__toString()
    }

    public function deserialize(string $value): object
    {
        return EmailAddress::fromString($value); // validates on the way in
    }
}

For multi-property value objects, use json_encode of a canonical toArray() and a matching fromArray() factory.

Keep value objects flat. The reflection serializer handles scalar and array properties well; nested value objects and collections are where it gets fragile. If you have nesting, write the serializer explicitly.

Design around the single-column storage. The day-to-day cost is queryability: the column is an opaque string, so you cannot do WHERE email = :x in a portable way. Options:

  • Use LIKE '%…%' for coarse search.
  • Denormalize the searchable parts into their own scalar columns and keep the value object as the source of truth.
  • Filter in PHP after loading a bounded result set.

Test at two levels. Unit-test every serializer with a round-trip (deserialize(serialize($vo)) == $vo). Then a functional test that persists, flushes, refreshes, and asserts the value object came back intact — that catches metadata wiring problems the unit test can't.

Gotchas and Tips

  • Reflection bypasses constructors. The default serializer instantiates objects without calling __construct, so validation and invariants are silently skipped on hydration. For anything security- or money-related, supply a custom serializer.
  • Handle null explicitly. Both convertToDatabaseValue and convertToPHPValue receive null for nullable columns; make sure your serializer (and the base type) short-circuit it rather than passing null into serialize().
  • The stored format is an undocumented schema. Renaming a property, changing a type, or switching to JSON from reflection will invalidate existing rows. This library has no migration tooling — plan a backfill or dual-read before you change value object internals.
  • Consider embeddables first. Doctrine 2.5+ ships @Embeddable / @Embedded, which handle value objects natively with per-field columns and full queryability. Reach for this package only when you deliberately want one serialized column, or you have many value objects and want to avoid a DBAL type class each. Also weigh that this library is small, low-adoption, and predates modern Doctrine — vet it against your ORM/DBAL versions before adopting.
  • Version compatibility is the main risk. The code targets old Doctrine 2.x / DBAL 2.x. Type::getName() is deprecated/removed in DBAL 3, and ORM 3's metadata APIs differ; smoke-test a real persist/find before committing.
  • Ordering matters. Mappings and the metadata subscriber must be registered before the EntityManager loads class metadata. Clear the metadata cache whenever you add a mapping.
  • Debugging workflow: dump the raw column value to confirm serialization, verify the type is registered under the name Doctrine expects (Type::hasType(...)), and confirm the loadClassMetadata subscriber actually runs. Most "it's not hydrating" bugs are a missing mapping or a cached metadata entry.
  • Extension points: the Serializer interface (validation/formatting), AbstractValueObjectType (change serialization/column semantics), and the mappings registry (per-class type names). There is no plugin system beyond these — keep customizations inside your own serializer.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect