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

Laminas Hydrator Laravel Package

laminas/laminas-hydrator

Laminas Hydrator converts arrays into objects and extracts objects back into arrays. It supports naming strategies, filters, hydration/extraction strategies, nested objects, and reusable data mapping for PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Check you can take v5 first. It is a major release with real BC breaks (typed signatures, enum filter conditions, static→instance properties). Also check whether something else is pinning you back:

    composer why laminas/laminas-hydrator
    composer show laminas/laminas-hydrator
    

    Note the PHP constraint in the v5 composer.json — v5 aligns with Laminas' current support policy, so it will not run on older PHP.

  2. Installation

    composer require laminas/laminas-hydrator:^5.0
    

    There is no Laravel service provider in the package — you wire it up yourself (see Implementation Patterns).

  3. Basic hydration (array → object)

    use Laminas\Hydrator\ClassMethodsHydrator;
    
    $hydrator = new ClassMethodsHydrator();          // underscoreSeparatedKeys = true by default
    $user = new User();                              // must have setName()/setAge()
    $hydrator->hydrate(['name' => 'John', 'age' => 30], $user);
    
  4. Basic extraction (object → array)

    $hydrator->extract($user); // ['name' => 'John', 'age' => 30]
    

Where to Look First

  • Documentation — restructured for v5 in this release (PR #170). Old bookmarks may 404; re-navigate.
  • v4 → v5 Migration Guide — new in this release (PR #134). Read this before upgrading; the v2→v3 guide alone is no longer sufficient.
  • src/Filter/ — the enum that replaces the old integer filter-condition constants lives here. Confirm the exact case names in your installed version before copying examples from anywhere.
  • src/Strategy/ and src/NamingStrategy/ — the extension points you will actually use day to day.
  • composer.json of the package — the authorative source for the new PHP constraint and the bumped dependency floors (laminas-serializer ^4, laminas-servicemanager ^4, psr/container ^2).

First Use Case: Request Payload → DTO

This is where a hydrator earns its place in a Laravel app: mapping validated snake_case request data onto a typed DTO or value object with explicit setters, and back out again for the JSON response.

public function store(StoreUserRequest $request)
{
    $hydrator = app(ClassMethodsHydrator::class);

    $dto = new CreateUserData();                 // setName(), setEmail(), setBirthdate()...
    $hydrator->hydrate($request->validated(), $dto);

    $user = User::create($dto->toArray());

    return response()->json($hydrator->extract($user));
}

Reality check for Laravel teams: Eloquent's fill()/create() already covers flat model attributes, and packages like spatie/laravel-data cover typed DTOs with validation attached. Reach for laminas-hydrator when you want a framework-agnostic mapper you can reuse outside Laravel (queues, CLI importers, a shared package), or when you need fine-grained control over naming, filters, and per-property strategies.


Implementation Patterns

Core Workflows

  1. Hydration with ClassMethodsHydrator

    • Requires real setX() methods; keys are matched to methods, not properties.
    • underscoreSeparatedKeys is on by default, so first_name finds setFirstName() — handy when your payload is snake_case and your DTO is camelCase.
    • Turn the flag off when your payload is already camelCase and you want a strict match:
      $hydrator = new ClassMethodsHydrator(underscoreSeparatedKeys: false);
      
  2. Hydration with ReflectionHydrator

    • Writes properties directly, bypassing setters, mutators and casts. Good for plain data bags, dangerous for anything with invariants.
    $hydrator = new ReflectionHydrator();
    $hydrator->hydrate(['email' => '[email protected]'], $dataBag);
    
  3. Extraction and naming strategy

    • ClassMethodsHydrator converts getFirstName() → first_name when underscore separation is enabled.
    • For the reflection-based hydrators you need an explicit naming strategy:
    use Laminas\Hydrator\NamingStrategy\UnderscoreNamingStrategy;
    
    $hydrator->setNamingStrategy(new UnderscoreNamingStrategy());
    
    • MapNamingStrategy is often a better fit than guessing conventions — explicit maps for wire format ↔ PHP property names, and they are trivially unit-testable.
  4. Per-property strategies (dates, enums, money)

    use Laminas\Hydrator\Strategy\StrategyInterface;
    
    final class DateStrategy implements StrategyInterface
    {
        public function hydrate(mixed $value, ?array $data = null): mixed
        {
            return $value === null ? null : Carbon::parse($value);
        }
    
        public function extract(mixed $value, ?object $object = null): mixed
        {
            return $value?->format('Y-m-d');
        }
    }
    
    $hydrator->addStrategy('birthdate', new DateStrategy());
    

    The mixed parameter/return types above are mandatory in v5 (see Gotchas).

  5. Filters (selective mapping / mass-assignment defence)

    use Laminas\Hydrator\Filter\MethodMatchFilter;
    
    $hydrator->addFilter('sensitive', new MethodMatchFilter('isSensitive'));
    

    Use filters to keep fields like is_admin, password, or computed accessors out of hydration/extraction. This is a cheaper, more explicit alternative to guarding with $fillable.

Laravel-Specific Patterns

  1. Bind hydrators in the container — keeps configuration in one place and makes the hydrator injectable in jobs, commands and controllers:

    // AppServiceProvider::register()
    $this->app->singleton(ClassMethodsHydrator::class, function () {
        $hydrator = new ClassMethodsHydrator(underscoreSeparatedKeys: true);
    
        $hydrator->addStrategy('birthdate', new DateStrategy());
        $hydrator->addFilter('sensitive', new MethodMatchFilter('isSensitive'));
    
        return $hydrator;
    });
    

    Prefer bind() over singleton() if you ever mutate strategies/filters at runtime — a shared instance is a hidden global.

  2. Form requests as the validation boundary, hydrator as the mapping boundary

    public function update(UpdateUserRequest $request, User $user)
    {
        $dto = app(ClassMethodsHydrator::class)->hydrate($request->validated(), new UpdateUserData());
    
        $user->update($dto->toArray());
    
        return new UserResource($user);
    }
    

    Never let the hydrator be your validator — it does no validation and will happily accept junk that passes its filter.

  3. Collections — HydratingIterator exists in Laminas\Hydrator\Iterator, but for API responses Laravel's own ResourceCollection is usually a better fit. If you do use the iterator, wrap it in iterator_to_array() and paginate separately:

    $rows = DB::table('imports')->cursor();
    
    $iterator = new \Laminas\Hydrator\Iterator\HydratingIterator($rows, $hydrator);
    
  4. Nested structures — AggregateHydrator chains hydrators with priorities. Keep aggregates to a single level deep, and prefer mapping nested payloads to nested DTOs in your own application code rather than relying on the aggregate to resolve relationships. Aggregate ordering is priority-based, so a hydrator that matches loosely can steal keys from a more specific one.


Gotchas and Tips

v4 → v5 Upgrade Checklist (do these first)

  1. Typed signatures are now enforced. PR #135 applied Rector and added parameter, return and property types throughout. Any custom class of yours implementing StrategyInterface, FilterInterface, HydratorInterface or extending AbstractHydrator with untyped signatures will fatal with an incompatible-declaration error. Add mixed/bool/string types to hydrate(), extract(), filter() and friends.
  2. Integer filter-condition constants are gone. PR #162 replaced FilterComposite::CONDITION_AND / CONDITION_OR with an enum. Update any FilterComposite construction:
    use Laminas\Hydrator\Filter\FilterComposite;
    
    // v5: enum case instead of an int constant — verify the case name in src/Filter/
    $filter = new FilterComposite([
        new MethodMatchFilter('isPublic'),
        new NumberOfParameterFilter(0),
    ], Condition::And);
    
    Grep your codebase for CONDITION_ — it is a cheap, complete migration check.
  3. Static properties became instance properties (PR #167). Code that toggled hydrator behaviour globally (e.g. static flags on ClassMethodsHydrator) must now be passed per instance, via the constructor or setNamingStrategy(). This is a silent behaviour change if you were setting statics in a service provider.
  4. Module Manager support and HydratorProviderInterface were removed (PR #137). Any config-driven getHydratorConfig() wiring is dead — register hydrators in AppServiceProvider instead. This also means there is no config file to publish; everything is explicit.
  5. Dependency floors moved: psr/container ^2, laminas/laminas-servicemanager ^4, laminas/laminas-serializer ^4. In a Laravel app this is mostly noise, but if another package requires the older Laminas service manager, you will get a solver conflict rather than a runtime error.
  6. Legacy v3 short class names (ClassMethods, Reflection, Aggregate) have been off the documentation path for two majors. Standardise on the *Hydrator names and treat the aliases as unsupported.

Pitfalls

  1. It does not work on Eloquent models. ClassMethodsHydrator looks for setName(); Eloquent has setAttribute() and magic __set, so hydration silently no-ops and you get an unsaved-changes bug that looks like a database problem. Hydrate DTOs, then map to models with fill()/create().
  2. ReflectionHydrator on an Eloquent model bypasses mutators, casts and $fillable, and will create dynamic properties on PHP 8.2+ (deprecation notices). Avoid entirely for models.
  3. Filters are evaluated in registration order, first match wins. Combining an include-style filter with an exclude-style filter without an explicit composite condition gives surprising results. Use FilterComposite with an explicit condition and test the edge case.
  4. Naming strategy conflicts. If you set an UnderscoreNamingStrategy and ClassMethodsHydrator is already doing underscore separation, names get mangled. Pick one layer: either the constructor flag or a naming strategy, never both.
  5. Nested/bidirectional structures. AggregateHydrator has no cycle detection — hydrating User → Post → User will recurse forever. Break the cycle in your own code (hydrate IDs only, hydrate the child side lazily) rather than trying to teach the hydrator graph semantics.
  6. Missing setters fail silently unless you opt in:
    $hydrator = new ClassMethodsHydrator(methodExistsCheck: true);
    
    Turn this on in development at minimum; it converts a whole class of "why is my DTO empty" bugs into an immediate exception. Remember to pass it per instance in v5.

Debugging Tips

  1. Log inside a decorator, not inside the hydrator. Subclassing or monkey-patching extract() is fragile across majors. Wrap instead:

    final class LoggingHydrator implements \Laminas\Hydrator\HydratorInterface
    {
        public function __construct(private \Laminas\Hydrator\HydratorInterface $inner) {}
    
        public function hydrate(array $data, object $object): object
        {
            logger()->debug('hydrate', ['keys' => array_keys($data), 'target' => $object::class]);
    
            return $this->inner->hydrate($data, $object);
        }
    
        public function extract(object $object): array
        {
            $out = $this->inner->extract($object);
            logger()->debug('extract', ['keys' => array_keys($out)]);
    
            return $out;
        }
    }
    

    It also makes the mapper swappable in tests.

  2. Diff keys, don't eyeball values. When a field "doesn't map", log array_diff(array_keys($input), array_keys($extracted)) — naming-strategy bugs show up as key mismatches, not value mismatches.

  3. Reject bad values in a strategy. Because strategies run per property during hydration, a throwing strategy is the cheapest place to enforce a domain invariant before it reaches your model:

    public function hydrate(mixed $value, ?array $data = null): mixed
    {
        if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email');
        }
    
        return $value;
    }
    

    Still validate at the request boundary — the hydrator is a mapper, not a gatekeeper.

Extension Points

  1. Custom hydrator — extend AbstractHydrator and keep the v5 typed signatures:
    final class ApiHydrator extends \Laminas\Hydrator\AbstractHydrator
    {
        public function hydrate(array $data, object $object): object
        {
            foreach ($data as $key => $value) {
                $method = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
    
                if (method_exists($object, $method)) {
                    $object->{$method}($this->hydrateValue($key, $value, $data));
                }
            }
    
            return $object;
        }
    
        public function extract(object $object): array
        {
            $data = [];
    
            foreach (get_class_methods($object) as $method) {
                if (str_starts_with($method, 'get') && $this->getFilter($method)->filter($method)) {
                    $key = $this->extractName($method);
                    $data[$key] = $this->extractValue($key, $object->{$method}(), $object);
                }
            }
    
            return $data;
        }
    }
    
  2. Strategies are the right extension point 90% of the time. If you are reaching for a custom hydrator, check first whether a naming strategy plus two or three property strategies would do the job — they survive majors far more easily.
  3. Pin the major. Given three BC-breaking majors in the package's recent history, use "laminas/laminas-hydrator": "^5.0" and let Dependabot open the v6 PR instead of discovering the break in production.
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