symplify/easy-hydrator
Symplify Easy Hydrator turns arrays into typed PHP objects via native attributes and constructor promotion. Map nested objects, arrays of objects, and custom keys without boilerplate or reflection code. A lightweight alternative to heavy serializers for clean DTO mapping in Laravel, Symfony, and pla...
Install and verify the baseline. This package is framework-agnostic and ships no Laravel service provider, so installation is just Composer:
composer require symplify/easy-hydrator
Before adopting it, confirm your PHP version (the package targets PHP 7.4+ typed properties) and note the repository location: it lives under deprecated-packages/easy-hydrator, so treat it as a utility you may later replace rather than a long-term core dependency.
Where to look first. Open EasyHydrator\EasyHydrator and read its hydrate(string $class, array $data) method. The whole package is essentially reflection over the target class constructor: parameter names become array keys, and parameter types drive recursion (a class-typed parameter is hydrated from a nested array). Once you understand that contract, everything else is predictable.
Minimal first use case — request input to a DTO. The highest-value daily use in Laravel is replacing manual new FooDto(...) boilerplate in controllers with a single hydration call against validated input:
use EasyHydrator\EasyHydrator;
final class CreateUserDto
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
}
// In a controller:
public function store(StoreUserRequest $request): JsonResponse
{
$dto = (new EasyHydrator())->hydrate(
CreateUserDto::class,
$request->validated()
);
$this->createUserAction->execute($dto);
return response()->json(['ok' => true], 201);
}
The keys in validated() must match constructor parameter names exactly (name, email, age above). That one-to-one contract is the package's entire value proposition and its main source of friction.
Register a singleton and inject it. Reflection is not free; reuse one instance rather than constructing it per request in hot paths.
// AppServiceProvider::register()
$this->app->singleton(\EasyHydrator\EasyHydrator::class);
Then inject EasyHydrator $hydrator into controllers, actions, or a base controller helper:
abstract class Controller extends BaseController
{
protected function hydrate(string $class, array $data): object
{
return app(\EasyHydrator\EasyHydrator::class)->hydrate($class, $data);
}
}
Nested DTOs hydrate recursively. A class-typed constructor parameter is hydrated from the corresponding nested array, which makes it a good fit for grouped payloads:
final class OrderDto
{
public function __construct(
public string $id,
public AddressDto $billing,
public AddressDto $shipping,
) {}
}
$order = $hydrator->hydrate(OrderDto::class, [
'id' => 'ORD-1',
'billing' => ['street' => '...', 'city' => '...'],
'shipping' => ['street' => '...', 'city' => '...'],
]);
Normalize keys before hydrating. Because there is no automatic camelCase mapping, snake_case API payloads (first_name) will not match $firstName. Handle this in one place — a small mapper, or a FormRequest that renames keys — instead of renaming constructor parameters to match the wire format:
$payload = collect($request->validated())
->mapWithKeys(fn ($v, $k) => [Str::camel($k) => $v])
->all();
$dto = $hydrator->hydrate(CreateUserDto::class, $payload);
FormRequest as the validation boundary, hydrator as the mapping layer. Validate first (types, required fields, nested arrays), then hydrate. This keeps the hydrator doing what it does best — mapping — and keeps untrusted input out of reflection-based coercion.
Use it for config/value objects too. Hydrating from config('services.foo') into a typed DTO in a service provider is a clean way to remove ad-hoc array access and typo risk:
$this->app->singleton(PaymentConfig::class, fn () => (new EasyHydrator())
->hydrate(PaymentConfig::class, config('services.payment')));
Keep DTOs final, typed, and immutable. Constructor-only injection pairs naturally with read-only DTOs. On PHP 8.1+, readonly promoted properties give you hydration plus immutability with no extra code.
Test the contract, not the reflection. A small factory-style test that hydrates the DTO from a representative array catches key renames immediately:
it('hydrates from validated payload', function () {
$dto = (new EasyHydrator())->hydrate(CreateUserDto::class, [
'name' => 'Ada', 'email' => '[email protected]', 'age' => 36,
]);
expect($dto->name)->toBe('Ada');
});
Avoid it for queued job constructors. Laravel serializes job properties; passing a hydrated DTO into a job constructor forces serialization concerns on classes the package does not know how to rebuild. Prefer hydrating inside the job from a plain array payload, or pass an Eloquent identifier and resolve.
Deprecation is the headline risk. The repository has moved to deprecated-packages, with no active maintenance guarantees and no security response. Before wiring it into a long-lived codebase, evaluate replacements: spatie/laravel-data (Laravel-native, validation + casts + transformers), cuyz/valinor (powerful, framework-agnostic, better error reporting), or plain new Dto(...) for small DTOs where the boilerplate cost is low. The hydrator works well as a transitional tool, but plan an exit.
Parameter names must match array keys exactly. first_name will not populate $firstName; you'll get a missing-data error rather than silently wrong data. Centralize key mapping and test it — a rename in a DTO constructor is a runtime break, not a compile-time one.
No automatic casting for DateTime, Carbon, enums, or Eloquent models. The package predates enums and does not know about DateTimeInterface. Supply already-constructed values or accept strings in the DTO and convert in the domain layer. Similarly, convert Eloquent models and collections with ->toArray() before hydrating.
Nullable and defaulted parameters need intent. A null payload value is only acceptable where the parameter type is nullable (?string). Required parameters without a corresponding key will fail — verify how your installed version handles missing optional keys before relying on defaults, and cover it with a test.
Arrays of objects depend on docblock annotations. For AddressDto[] $addresses, the hydrator needs the element type from an @param AddressDto[] $addresses docblock. Malformed or missing docblocks produce confusing failures, so treat the annotation as part of the DTO's public contract.
Nested hydration expects arrays. Passing an already-constructed AddressDto where the hydrator expects a nested array is a common mismatch. Decide at the boundary whether values are "arrays in" or "objects in" and stay consistent.
Error messages are low-level, not field-aware. Failures surface as reflection/missing-data exceptions without the field-level context Laravel developers expect. Wrap hydration in a try/catch and translate to ValidationException (or a domain exception) so API clients get structured 422 responses instead of a 500:
try {
$dto = $hydrator->hydrate(CreateUserDto::class, $data);
} catch (\Throwable $e) {
throw ValidationException::withMessages(['payload' => $e->getMessage()]);
}
Do not hydrate unvalidated request data. Reflection-based coercion can accept values you did not intend. Always hydrate from $request->validated() or an equivalent whitelisted array.
No cycle detection. Self-referencing structures (parent/child trees) will recurse until failure. Hydrate leaf DTOs first and assemble the tree yourself.
No auto-discovery or config. There is nothing to publish, no config to cache-bust, and no facade. Register the instance in the container and keep a single injection point; if you later swap to spatie/laravel-data, that single seam is where the migration happens.
How can I help you explore Laravel packages today?