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 Laminas Hydrator Laravel Package

doctrine/doctrine-laminas-hydrator

Laminas hydrator integration for Doctrine: extract and hydrate entity data using Doctrine's metadata and object manager, letting you convert between entities and arrays in Laminas applications with strategies for associations, collections, and embedded objects.

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a Laravel package. It is the Laminas/Mezzio integration layer for laminas/laminas-hydrator that knows how to read Doctrine ClassMetadata. You reach for it only if you have already committed to Doctrine ORM inside Laravel (typically via laravel-doctrine/orm) and you want a metadata-aware mapper between request arrays and entities. If you are on Eloquent, stop here and use $model->fill($validated) plus API Resources.

Assuming Doctrine is in place:

composer require doctrine/doctrine-laminas-hydrator

That pulls laminas/laminas-hydrator and expects doctrine/orm (and doctrine/persistence) to already be installed. Read two things first: the class Doctrine\Laminas\Hydrator\DoctrineObject and the package's own DoctrineObjectTest, which documents more behavior than the README.

Minimal use:

use Doctrine\Laminas\Hydrator\DoctrineObject;

$hydrator = new DoctrineObject($entityManager);      // by-value mode
$user     = $hydrator->hydrate($payload, new User());
$array    = $hydrator->extract($user);

Bind it once in a service provider so you are not constructing it per request:

use Doctrine\Laminas\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManagerInterface;

$this->app->bind(DoctrineObject::class, fn ($app) => new DoctrineObject(
    $app->make(EntityManagerInterface::class)
));

Your first real use case: a controller or action that receives a validated payload and turns it into an entity, including a relation submitted as an ID:

$hydrator = new DoctrineObject($em, byValue: false); // resolve associations by identifier
$post     = $hydrator->hydrate($request->validated(), new Post());
$em->persist($post);
$em->flush();

Implementation Patterns

By-value vs by-reference is the central decision. The constructor's second argument is $byValue (default true). In by-value mode, association data is treated as nested data to hydrate into new/embedded objects. In by-reference mode, association data is treated as identifiers and resolved through the ObjectManager (find()), which is what you want when a form or JSON body posts author_id or tag_ids. Pick one mode per mapper and keep it consistent; mixing semantics in one hydrator is how people get TypeError: expected Author, int given.

Wire it into a dedicated mapper class, not directly into controllers. Laravel has no $form->setHydrator(...)->setObject($entity) equivalent, so the closest idiomatic shape is a small service:

final class PostMapper
{
    private DoctrineObject $hydrator;

    public function __construct(EntityManagerInterface $em)
    {
        $this->hydrator = new DoctrineObject($em, byValue: false);
        $this->hydrator->setNamingStrategy(new UnderscoreNamingStrategy());
    }

    public function map(array $data, Post $post = new Post()): Post
    {
        return $this->hydrator->hydrate($data, $post);
    }

    public function toArray(Post $post): array
    {
        return $this->hydrator->extract($post);
    }
}

Use naming strategies for snake_case APIs. Laminas\Hydrator\NamingStrategy\UnderscoreNamingStrategy maps createdAt (entity) to created_at (payload) in both directions; MapNamingStrategy handles one-off aliases. This keeps your entities camelCase and your JSON conventional.

Use field strategies for value objects and typed columns. AbstractHydrator::addStrategy($field, $strategy) is the extension point inherited by DoctrineObject. Ship Laminas\Hydrator\Strategy\DateTimeFormatterStrategy, BooleanStrategy, IntegerStrategy, or write a StrategyInterface for Money, Email, UserId, etc. The hydrator does not coerce scalars for you, so a "1" arriving for an int property will surface as a Doctrine type error at flush time unless you add a strategy or coerce in the FormRequest.

Collection semantics are controlled by strategies. AllowRemoveByValue, DisallowRemoveByValue, AllowRemoveByReference, DisallowRemoveByReference decide whether items absent from the payload are detached from a Collection. The removal strategies wrap the hydrator, e.g. $hydrator->addStrategy('tags', new AllowRemoveByValue($hydrator)); — check the constructor signature in your installed version before copying this.

Embedded objects and collections are handled automatically when they are described by ClassMetadata; you do not need custom code for an embeddable Address or an ArrayCollection of owned entities. For to-many by-reference hydration you pass an array of IDs and get back managed entities in an ArrayCollection.

Extraction for API responses. In by-reference mode extract() gives you association identifiers rather than nested object graphs, which is often exactly what you want for a "populate the edit form" endpoint. In by-value mode you get nested structures — convenient, but see the recursion warning below.

Gotchas and Tips

The namespace moved. Older tutorials reference DoctrineModule\Hydrator\DoctrineObject; the standalone package uses Doctrine\Laminas\Hydrator\DoctrineObject. If you are porting code from a Laminas app, update imports and the composer requirement together.

The default by-value mode is the #1 trap. Posting {"author_id": 5} into a hydrator constructed with defaults produces a type error or a broken association, not a looked-up Author. Conversely, by-reference mode requires the identifier field names to match what find() expects, so composite keys and non-standard identifiers need care.

No mass-assignment protection. Unlike Eloquent's $fillable, DoctrineObject will happily set every mapped field present in the payload — including id, created_at, and roles. There is no $guarded. Enforce the whitelist with a FormRequest or a DTO before hydration; unknown keys are silently ignored, which makes over-posting bugs quiet rather than loud.

It mutates the object you pass in. Hydrating into a managed entity dirties it immediately, before your validation or business rules have run. Validate first and hydrate second, or hydrate into a fresh instance and copy across.

By-reference collection hydration is N+1. Each identifier triggers a find(). There is no batching. For large tag lists, load the collection in one repository query and set it yourself.

extract() has no depth guard. Bidirectional associations (Post::comments, Comment::post) will recurse until PHP dies. Extract into a flat array deliberately (field-by-field or via a DTO), or use a serializer with circular-reference handling for graphs. Uninitialized lazy proxies and PersistentCollections will also trigger DB queries during extraction — initialize consciously.

The hydrator caches metadata per class internally. It is safe as a singleton for one EntityManager, but not across multiple managers (multi-tenant, test bootstrapping with separate connections). Strategies are mutable state on the instance; if you add per-request strategies, build a fresh DoctrineObject or clone rather than mutating the shared binding.

No scalar type conversion and no DateTime parsing. Strings land on typed properties as strings. A datetime column receiving "2026-01-01" becomes a TypeError unless you attach DateTimeFormatterStrategy or convert in the request layer.

ODM support is secondary. The constructor type-hints ObjectManager, so DocumentManager technically works, but the test suite focuses on ORM and MongoDB ODM compatibility tends to lag. Verify before betting on it.

Debugging shortcut: dump $em->getClassMetadata(Post::class)->getFieldNames() and getAssociationNames() to see exactly what the hydrator considers settable, and use hasStrategy($field) to confirm your strategy registered. If a field is silently not hydrating, it is almost always missing from metadata (wrong property name, missing mapping, or an embeddable path like address.street).

Weigh the ecosystem risk. Thirty-seven stars and essentially no dependents means few eyes on edge cases; the package is maintained by Doctrine but its release cadence tracks Laminas, not Laravel. Pin the version in composer.json, and if the mapping is central to your domain, compare against symfony/serializer with Doctrine's DoctrineNormalizer/DoctrineDenormalizer, spatie/laravel-data with custom casts, or a hand-written mapper. For a few entities, a hand-written mapper is often less surprising than any of them.

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.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager