yiisoft/hydrator
Yii Hydrator populates typed objects from arrays and other input using attributes and property mapping. It handles type casting, nested hydration, and dependency injection, offering PHP and Laravel apps a fast, flexible alternative to manual DTO construction.
Install the package and confirm your PHP version is compatible (this is a PHP 8.1+ package):
composer require yiisoft/hydrator
Start with a small DTO and the two core methods: create() for new objects and hydrate() for existing ones.
use Yiisoft\Hydrator\Hydrator;
final class UserInput
{
public string $name = '';
public int $age = 0;
}
$hydrator = new Hydrator();
$input = $hydrator->create(UserInput::class, [
'name' => 'Ada',
'age' => '36', // cast to int by the hydrator
]);
// $input->age === 36
Where to look first:
README.md for the quick-start usage and attribute examples.src/Hydrator.php for create() / hydrate() behavior.src/Attribute/Parameter/ for built-in attributes such as Map and Cast.src/TypeCaster/ for casting behavior and custom caster contracts.tests/ for edge cases around missing data, nulls, nested objects, and DI.HydratorFactory when integrating with a PSR-11 container.First day-to-day use case: map an HTTP request payload into a typed request DTO.
use Yiisoft\Hydrator\Attribute\Parameter\Cast;
use Yiisoft\Hydrator\Attribute\Parameter\Map;
final class CreateUserRequest
{
#[Map('first_name')]
public string $firstName = '';
#[Map('last_name')]
public string $lastName = '';
#[Cast('int')]
public int $age = 0;
}
$request = $hydrator->create(CreateUserRequest::class, $payload);
If your application already has a container, use the package factory so constructor dependencies can be resolved:
use Yiisoft\Hydrator\HydratorFactory;
$hydrator = (new HydratorFactory($container))->create();
Use DTOs at the boundary, not domain entities. The most reliable pattern is to hydrate dedicated request/command DTOs and then map those into domain objects explicitly. This gives you a natural allowlist and avoids mass-assignment surprises.
Hydrate partially for PATCH-style updates. hydrate() works well for updating an existing object from only the fields present in the payload:
$hydrator->hydrate($user, [
'firstName' => 'Grace',
]);
Design DTO properties with defaults so omitted keys keep their current/default values rather than becoming null.
Map snake_case payloads to camelCase properties. Keep transport naming out of PHP classes:
final class UpdateProfileRequest
{
#[Map('display_name')]
public string $displayName = '';
#[Map('postal_code')]
public string $postalCode = '';
}
Use constructor promotion for required values. Constructor parameters are hydrated before properties, which is a good fit for invariants:
final class Money
{
public function __construct(
#[Cast('int')]
public int $amount,
public string $currency = 'USD',
) {}
}
Hydrate nested DTOs deliberately. For small, stable structures you can use nesting support:
use Yiisoft\Hydrator\Attribute\Parameter\Nested;
final class OrderInput
{
#[Nested]
public CustomerInput $customer;
/** @var ItemInput[] */
public array $items = [];
}
For arrays of objects, confirm the version’s nested-array support or hydrate elements explicitly in a small mapper. Do not assume docblock generics alone will always drive runtime hydration.
Write custom casters for dates, enums, and booleans. Built-in casts cover scalar cases; custom casters keep conversion logic testable:
use Yiisoft\Hydrator\Attribute\Parameter\Cast;
final class EventInput
{
#[Cast(IsoDateCaster::class)]
public DateTimeImmutable $startsAt;
#[Cast(StatusCaster::class)]
public Status $status;
}
Implement the package’s caster interface and check src/TypeCaster/ for the exact method signature in your installed version.
Let DI resolve services and data resolve scalars. A container-aware factory/object factory is useful for command and handler objects:
final class GenerateReport
{
public function __construct(
private ReportRepository $reports, // from container
#[Map('report_id')] private string $reportId, // from input
) {}
}
Keep payload keys and service type names clearly separated, and map payload parameters explicitly when there is any ambiguity.
Reuse the hydrator for bulk rows. Hydrating database rows into read models or test fixtures is straightforward:
$users = array_map(
static fn(array $row) => $hydrator->create(User::class, $row),
$rows,
);
Create the hydrator once per request/process, not once per row.
Integrate with validation, not instead of it. Hydration and validation are separate concerns. Cast first only when that is safe for your validation rules; otherwise validate raw input and hydrate after validation.
yiisoft/validator or your existing validation layer.null are different. A missing key usually leaves a default in place; an explicit null can still cause a TypeError for a non-nullable property. Use nullable types and defaults intentionally.'0', 'false', '', and 'off' do not always mean what you expect. Use a custom boolean caster for form/query data.Map on a promoted constructor parameter is often the cleanest fix when payload keys do not match PHP parameter names.#[Nested] handles straightforward object nesting, but arrays of DTOs may require an explicit loop or custom caster. Avoid relying on docblock generics unless your version documents support.create() with constructor promotion instead of hydrate() for those models.new Hydrator().array_keys($data) alongside the target class constructor/property names. Most issues are naming mismatches, missing defaults, or an unexpected null.src/ for your installed version, since the package API has evolved.How can I help you explore Laravel packages today?