nutgram/hydrator
nutgram/hydrator is a lightweight PHP library for turning arrays into typed objects, with support for nested DTOs, type casting, and attribute-driven mapping. It integrates smoothly with Nutgram and Laravel-style workflows, reducing boilerplate when working with API payloads and Telegram updates.
Install with Composer. It has no framework dependencies, so it works in plain PHP, Laravel, Symfony, or inside a Nutgram bot:
composer require nutgram/hydrator
The entry point is a single Hydrator instance. You give it a target class and an associative array (already json_decoded), and it reflects over the class to build the object:
use SergiX44\Hydrator\Hydrator;
$hydrator = new Hydrator();
$payload = json_decode($json, true);
$user = $hydrator->hydrate(User::class, $payload);
Your DTO is a normal PHP class. Properties can be public, typed, readonly, or promoted through the constructor — the hydrator walks the class definition and assigns from matching array keys:
final class User
{
public string $name;
public int $age;
public ?string $bio = null;
}
Where to look first:
README.md for the attribute namespace and the hydrate() signature.src/Annotation/ (or src/Attributes/) — this is where MapFrom, ArrayType, HydrateWith, etc. live. These attributes are the entire public API surface you'll use day-to-day.First real use case: replacing hand-written fromArray() / fromJson() factory methods on API response DTOs. Instead of dozens of $this->foo = $data['foo'] ?? null; lines, the DTO declares its shape and the hydrator does the wiring. This is exactly how Nutgram uses it internally to turn Telegram update JSON into typed update objects, so hydrating webhook payloads is the canonical scenario.
Map non-matching JSON keys with attributes. Snake_case APIs and camelCase DTOs are the common mismatch:
use SergiX44\Hydrator\Annotation\MapFrom;
final class User
{
#[MapFrom('first_name')]
public string $firstName;
#[MapFrom('last_name')]
public string $lastName;
}
Nested DTOs hydrate automatically. Just type the property as another class; the hydrator recurses when the value is an array:
final class Order
{
public int $id;
public User $customer;
public Address $shippingAddress;
}
Typed arrays need an explicit attribute. PHP can't express User[], so the hydrator can't infer the element type. Use the array-type attribute on list properties:
use SergiX44\Hydrator\Annotation\ArrayType;
final class Order
{
/** @var OrderItem[] */
#[ArrayType(OrderItem::class)]
public array $items;
#[ArrayType('string')]
public array $tags = [];
}
Enums and scalars are handled by type declaration. Backed enums can be declared directly on the property and are converted from their backing value. Casts to int, float, bool, and string follow the declared type.
Custom casting via #[HydrateWith]. For value objects (Money, UUID, Carbon dates) implement the package's hydrator contract and reference the class on the property. Keep casters stateless and side-effect free so they're safe to reuse across the whole app:
#[HydrateWith(MoneyHydrator::class)]
public Money $total;
Integration with HTTP clients and Laravel. Wrap hydration in a small helper so controllers and jobs don't repeat json_decode:
function hydrate(string $class, array|string $data, ?Hydrator $hydrator = null): object
{
$hydrator ??= app(Hydrator::class); // or a static singleton
return $hydrator->hydrate(
$class,
is_string($data) ? json_decode($data, true, flags: JSON_THROW_ON_ERROR) : $data
);
}
Register the Hydrator as a singleton in the container — reflection metadata is cached per class, and one instance keeps that cache warm across requests in long-running workers (Octane, Swoole, queue workers).
Nutgram bots. Inside a bot handler, incoming updates arrive already hydrated as Nutgram\Telegram\Types\Update objects. Use nutgram/hydrator explicitly when you hydrate your own domain DTOs from callback_data, inline query payloads, or third-party API responses inside a handler — the API is identical, and it keeps your DTO layer consistent with the framework's internals.
Validate after hydrating, not during. The hydrator is a mapper, not a validator. Hydrate into the DTO, then run Laravel's Validator or a value-object constructor check. This separation means a malformed payload fails with a validator error you control, not a TypeError deep in reflection.
json_decode($json, true). Hydrating from stdClass or from a JSON string directly won't work if the package expects arrays.= null default or a nullable type — the hydrator won't invent one."42" may hydrate into an int property, and 1 into a bool. Don't rely on hydration to reject loosely-typed API data; validate if it matters.#[ArrayType(Foo::class)] handles a list of objects; a single nested Foo handles an object. Forgetting ArrayType gives you arrays of raw arrays (or a type error), not Foo[]. This is the number-one source of confusion.#[MapFrom] overrides the property name, but if both user_name and userName exist in the payload, be explicit about which one wins.?Foo, the hydrator can't know whether a null key should skip hydration or produce null; test both the "key present with null" and "key absent" cases.Hydrator per request you lose the benefit; bind it as a singleton.JSON_THROW_ON_ERROR on decode so JSON syntax errors aren't mistaken for hydration errors.#[HydrateWith] caster over subclassing the hydrator. Custom casters compose and are registered per-property, so they're easy to test in isolation.composer.json and read the changelog before upgrading; the attribute namespace is the part most likely to move.How can I help you explore Laravel packages today?