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

Hydrator Laravel Package

patchlevel/hydrator

Hydrate objects to arrays and back without calling constructors. Supports final and readonly classes, nested structures, enums, dates, lazy hydration, field renaming, and metadata caching with a developer-friendly, fully typed API.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require patchlevel/hydrator

The package is framework-agnostic, so the first thing to do is build one hydrator instance for the whole application (usually in a DI container) and reuse it. Wiring is via a builder/factory that combines four collaborators:

  1. A metadata factory (reads attributes on your classes/properties).
  2. A normalizer resolver / guesser chain (turns enums, dates, nested objects, collections into scalars and back).
  3. Optional middleware — a stack around hydrate/extract; the 2.1 line adds skippable middleware, which can mark a field as "leave it alone" mid-stack.
  4. Optional extensions (cross-cutting hooks around hydrate/extract), including the new groups extension from 2.1.0.
use Patchlevel\Hydrator\Hydrator;

final class UserDto
{
    public function __construct(
        public readonly string $id,
        public readonly string $email,
        public readonly ?DateTimeImmutable $createdAt = null,
    ) {}
}

// Build once (see the "Hydrator" docs page for the exact builder signature in 2.x)
$hydrator = /* HydratorBuilder / Hydrator + metadata factory + guessers + middleware */;

$user = $hydrator->hydrate(UserDto::class, [
    'id' => '01J...',
    'email' => '[email protected]',
    'createdAt' => '2025-01-01T12:00:00+00:00',
]);

$payload = $hydrator->extract($user);
// ['id' => '01J...', 'email' => '[email protected]', 'createdAt' => '2025-01-01T12:00:00+00:00']

Where to look first

  • docs/…/hydrator — the two core methods and constructor bypass behavior.
  • docs/…/normalizer — which types are auto-resolved and how to plug your own.
  • docs/…/guesser and docs/…/extensions — only needed once defaults don't fit.
  • docs/…/middleware — new in 2.x, extended in 2.1 (skippable middleware + generated middleware). Read this before writing your third extension; a lot of "I need to intercept this" cases are middleware.
  • docs/…/groups — new in 2.1.0; the first thing to reach for when one DTO needs two different array shapes.
  • docs/…/lazy (PHP 8.4) and docs/…/cryptography — read before using; they change runtime semantics.

First real use case: map a decoded JSON request body into a typed, readonly DTO instead of hand-writing new UserDto($data['id'], $data['email'], ...) with isset() guards. The same DTO can then be extracted straight into a json column. That round-trip — array → object → array — is the sweet spot of this library. The second real use case is 2.1's groups: the same DTO extracted with ['public'] for an API response and ['internal'] for the DB.

Implementation Patterns

Request → DTO → validate → handler

Hydration is not validation. Treat it as a typed cast, then run your validator (Symfony Validator, Respect, etc.) on the hydrated object:

$dto = $hydrator->hydrate(CreateUser::class, $request->getPayload());
$violations = $validator->validate($dto);

Keep the class name a literal or an allow-listed constant — never take it from user input.

Persistence: JSON columns and payload blobs

// writing
$entity->payload = $hydrator->extract($command);

// reading
$command = $hydrator->hydrate(CreateUser::class, $entity->payload);

This is the pattern that pays off most: event-sourcing payloads, audit logs, outbox tables, and Doctrine json columns all become "store the array, hydrate on read", and schema changes are handled by renaming/excluding fields rather than hand-mapping.

Groups: one DTO, several shapes (2.1.0)

Instead of maintaining UserDto, UserApiDto, UserAdminDto and hand-copying fields, declare groups on the properties/promoted params and select the group per call:

final class UserDto
{
    public function __construct(
        #[Groups(['public', 'internal'])]
        public readonly string $id,

        #[Groups(['internal'])]
        public readonly string $email,

        #[Groups(['internal'])]
        public readonly string $internalNote = '',
    ) {}
}

$hydrator->extract($user, ['groups' => ['public']]);
// ['id' => '01J...']

The typical wins: public API vs. internal payload, v1 vs. v2 consumers, and "what goes in the DB column" vs. "what goes over the wire". Register the groups extension once, pick groups at call time — don't bake group selection into separate hydrator instances.

Middleware: cross-cutting behavior with an escape hatch

Middleware wraps hydrate/extract and is the right layer for anything that inspects or rewrites the stream rather than one field: key translation, audit stamping, per-request context. 2.1's skippable middleware adds the ability to say "this field is out of scope" and stop the remaining stack from touching it — which is exactly how the groups extension does its filtering.

Rules that keep it sane:

  • Keep middleware stateless and cheap; it runs per field, not per request.
  • Order matters. Put "decide whether this field is in play" middleware (groups, authorization) before value-transforming ones (rename, encrypt, normalize).
  • Skipping should be a decision, not a swallow — log or assert in dev when a field is unexpectedly skipped.
  • If middleware-generated code is part of your setup (the 2.1 "middleware generation" docs), treat the generated output like cached metadata: regenerate on DTO/attribute changes and commit or warm it as part of your build, not at runtime.

Nested structures work out of the box

Backed enums, DateTimeImmutable, arrays of DTOs, and nested readonly objects are resolved by the default guessers/normalizers. Reach for attributes only when conventions fail:

final class UserDto
{
    #[Rename('user_id')]
    public readonly string $id;

    #[Exclude]
    public readonly string $internalToken = '';

    #[Normalizer(new MyMoneyNormalizer())]
    public readonly Money $balance;
}

Use #[Rename] for snake_case APIs without touching your PHP property names, and #[Exclude] for derived or sensitive fields that must never be extracted.

Extensions for cross-cutting behavior

Extensions are the right place for versioned payload handling (v1 → v2 migration of arrays), defaulting missing keys, or stamping metadata on extract. Keep them stateless and side-effect free; ordering matters when multiple extensions touch the same field. Groups now ship as an extension — read its source before writing your own field-filtering extension, you'll usually find it already covers you.

Custom normalizers for value objects

If you have Money, Uuid, Email, or PhoneNumber VOs, write one normalizer each and register it once. That single investment removes dozens of manual casts and keeps the array representation consistent across the app.

Lazy hydration (PHP 8.4+)

For large aggregates or deep graphs, #[Lazy]-marked properties hydrate as proxies and only materialize on first access. Use it on optional/heavy relations, not on required scalar fields.

Crypto-shredding for PII

Mark sensitive properties with the crypto attribute so extraction stores ciphertext and hydration needs the key. This is the strongest reason to pick this package over a hand-rolled mapper: deleting the key effectively erases the PII while leaving the rest of the payload intact. Combine with groups: keep ciphertext out of the public extraction entirely.

DI wiring

Register the hydrator as a singleton. If you're on Symfony/Laravel, define one service with the metadata factory, guesser chain and middleware stack; don't instantiate a hydrator inside repositories or controllers.

Gotchas and Tips

  • The constructor is not called. That's the feature and the trap. Readonly/final classes hydrate fine, but invariants (if ($id === '') throw ...) are bypassed. Validate after hydration, or put invariant checks in a factory that calls the hydrator.
  • Properties must be settable by reflection. Static properties are ignored; promoted constructor properties work normally. If a property has no type or a union type the guesser can't decide, you'll need an explicit #[Normalizer].
  • Missing keys are not the same as null. Decide per DTO whether absent means "skip" or "null" — default parameter values help, and an extension is the clean fix if you need consistent behavior. Test this explicitly; it's the most common source of subtle bugs.
  • Skipped ≠ null ≠ missing. With 2.1's skippable middleware (and therefore with groups), a field can be skipped, which is a third state. A skipped field must not be silently defaulted by another extension — check the interaction order of groups + any defaulting extension you add.
  • Group coverage is easy to get wrong. A property with no group never appears in a group-filtered extract; a typo'd group name silently yields an empty payload. Assert on the extracted shape in tests, and prefer constants for group names rather than scattered string literals.
  • Middleware ordering is semantics. Groups, rename, and crypto all read as "small transformations", but stacking them wrong gives you renamed-then-skipped vs. skipped-then-renamed differences. Write one test per combination you rely on.
  • Generated middleware is code you own. If you use the 2.1 middleware generation, regenerate in CI and diff — stale generated code after an attribute change is a silent, confusing misbehavior.
  • Pure enums vs backed enums. Backed enums resolve automatically; pure (unit) enums generally need a normalizer. Same story for DateTimeInterface subclasses with non-default formats — configure the format in the normalizer rather than relying on ATOM.
  • Naming conventions. If your API is snake_case and your PHP is camelCase, either add #[Rename] per field or install a naming-strategy guesser. Don't mix both approaches in one DTO.
  • Cache/keep the metadata factory. Metadata is computed per class; re-instantiating the hydrator or the factory per request throws away the main performance benefit. One hydrator per process is the intended usage.
  • Crypto-shredding is irreversible. Key rotation and key storage are on you. If the key is lost, the data is gone — which is the point, but make sure backups and retention policies agree with that.
  • Lazy proxies leak through reflection. var_dump, serialize(), and strict instanceof checks can force initialization or surprise you. Don't combine lazy hydration with code that reflects over the object graph.
  • Class-name injection. hydrate($class, $data) will happily instantiate whatever class you pass. Always pass a compile-time constant or a value you've allow-listed.
  • Debugging metadata problems. When a field silently doesn't map, dump the resolved class metadata (via the metadata factory) instead of guessing — it's the fastest way to see whether the attribute, the type, the normalizer, or a skipping middleware is the problem.
  • Check your PHP constraint. Version 2.x requires a modern PHP; lazy hydration requires 8.4. Pin the major version in composer.json (^2.1 if you want groups/skippable middleware) and read the upgrade notes before composer update. 2.1.0 is a minor release — additive, no breaking changes, but the new attributes/middleware interfaces are worth adopting deliberately rather than incidentally.
  • Extend, don't fork. The guesser, middleware and extension interfaces are the intended customization points. If you find yourself writing a second hydrator to cover edge cases, you probably want one more normalizer, middleware, or a group instead.
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.
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
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
cgv/localized-resource