xactsystems/type-hint-hydrator
TypeHintHydrator maps arrays and request data into typed PHP objects by reading constructor and property type hints, so you can skip hand-written mapping code. Supports nested objects, enums, and collections, and fits neatly into Laravel or any PHP project.
This package is a thin Laravel integration over laminas-hydrator (with Symfony serialization/normalization support), so the fastest path to value is turning raw arrays — $request->validated(), $model->getAttributes(), a decoded webhook payload — into typed DTOs instead of hand-writing constructors.
composer require <vendor>/<package> — v5.0.7 now requires laminas-hydrator ^4.0 and Symfony ^8.1. Check these against your composer.lock before upgrading; a project pinned to laminas-hydrator 3.x or an older Symfony serializer will hard-conflict.php artisan vendor:publish --tag=<package>-config) and read it first — the default hydrator type, naming strategy, and date strategy are all set there.readonly DTO from a FormRequest.final readonly class CreateInvoiceData
{
public function __construct(
public string $number,
public int $amountCents,
public \DateTimeImmutable $dueAt,
) {}
}
// in the controller
$data = $this->hydrator->hydrate($request->validated(), CreateInvoiceData::class);
DTO-first controllers. Validate in a FormRequest, hydrate in the controller (or a dedicated action), pass the DTO into your service layer. This keeps array<string, mixed> out of your domain and makes the boundary between HTTP and business logic explicit — the single highest-leverage use of this package.
Constructor vs. property hydration. Laminas\Hydrator\ReflectionHydrator sets properties directly (fast, works with readonly via reflection in PHP 8.1+ but bypasses constructors), while ClassMethodsHydrator/constructor-based hydration goes through __construct. For DTOs with invariants (validation, normalization, computed defaults), use constructor hydration so the object can never exist in an invalid state. Reserve reflection hydration for trusted internal data (e.g. rehydrating from a queue payload).
Eloquent is not a hydrator target. Don't hydrate models. Map $model->only([...]) or an API Resource array into a DTO instead, and let Eloquent keep ownership of casts, mutators, and dirty tracking.
Dates need an explicit strategy. Register a date strategy (Laminas\Hydrator\Strategy\DateTimeFormatterStrategy) in config so ISO-8601 strings become DateTimeImmutable. Without it you get strings, or a fatal error on typed DateTimeInterface properties. Configure the format and timezone in one place — the package config, not per-DTO annotations.
Naming mismatches. External APIs are usually snake_case; PHP properties are camelCase. Use an underscore naming strategy globally, or compose per-field strategies where the mapping is irregular (user_id → userId but id → uuid).
Nested objects and collections. Compose hydrators: a parent hydrator with a strategy for a child DTO property, and an array-of-DTOs strategy for lists. Build these as small named classes bound in the container rather than closures, so they're testable and reusable across endpoints.
Test the DTO, not the hydrator. Unit-test DTOs with direct constructor calls; integration-test hydration once per DTO against a fixture array. If you use Pest, a dataset of [payload, expectedDto] pairs catches 90% of mapping regressions cheaply.
The uninitialised-date bug is the headline fix. Before 5.0.7, hydrating a DTO where an optional date property was absent left the typed property uninitialised, and any later read threw Typed property ... must not be accessed before initialization. If you're on 5.0.6 or earlier and see this intermittently, the fix is composer update — but verify your own defensive code: don't rely on hydration to set defaults, declare them in the property or constructor.
The Symfony 8.1 / laminas-hydrator 4.0 floors are hard constraints. Both are recent majors, and laminas-hydrator 4 dropped older PHP support. Run composer why-not before upgrading and check composer update --dry-run for transitive conflicts — Laminas-based packages (api-tools, old mezzio components) are the usual offenders. If you can't move to Symfony 8.1 yet, stay on the 5.0.6 line and pin explicitly.
Silent partial hydration. Reflection-based hydrators quietly ignore unknown keys and skip missing ones. A typo in a payload key yields a valid object with a defaulted property, not an error. Assert completeness in tests, or add a strict variant that throws on unknown keys during development.
Hydration bypasses casts and validation. The hydrator does no type coercion unless a strategy does it. A "42" from a JSON body will throw on an int property in strict types. Either add scalar strategies or normalize input before hydrating.
Don't hydrate from $request->all(). Always validated() (or safe()->all()). Hydrating unvalidated input gives attackers a path to set constructor arguments you never intended to accept.
Caching in production. If hydrator/strategy configuration lives in config/*.php, run php artisan config:cache in deploy — but remember env() calls inside config files return null after caching. Read from config() in your strategies, never env().
Extension point. Override the container binding for the hydrator interface in your AppServiceProvider to swap the default for a composed hydrator with your naming and date strategies pre-registered. That's cleaner than calling withStrategy() at every call site and keeps behaviour consistent across the app.
Debugging. When a property comes out null, check in this order: (1) was the key present in the source array, (2) does the naming strategy match it, (3) is there a strategy registered for the type, (4) is the property readonly and being set twice. Dumping the hydrator's strategy map usually short-circuits the hunt.
How can I help you explore Laravel packages today?