ocramius/generated-hydrator
Generates fast, specialized hydrator classes for PHP objects, letting you populate and extract properties quickly without reflection overhead. A good fit for ORMs, serializers, and data mappers.
ocramius/generated-hydrator compiles a per-class hydrator into plain PHP and requires it, so hydrate()/extract() become direct property assignments instead of reflection lookups. Private and protected properties are reachable because the generated class creates closures bound to the target class scope — not because you get to cheat visibility rules.
Install and verify the version first. The package is archived (last release 2024-01-09), so pin it explicitly in composer.json ("ocramius/generated-hydrator": "4.*") rather than letting a future resolution surprise you.
composer require ocramius/generated-hydrator:^4.0
Minimal usage — this is the whole API surface you need on day one:
use GeneratedHydrator\Configuration;
$config = new Configuration(App\Data\OrderData::class);
// Returns the FQCN of the generated class, generating + requiring it on first call
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator = new $hydratorClass();
$dto = $hydrator->hydrate($payload, new OrderData()); // (array $data, object $object)
$array = $hydrator->extract($dto); // ['id' => ..., 'total' => ...]
Where to look first, in this order:
GeneratedHydrator\Configuration — the only class you should touch. It exposes the knobs: setGeneratedClassesTargetDir(), setGeneratedClassesNamespace(), setAutoGenerate(bool), setGeneratedClassNamingStrategy(), and createFactory().GeneratedHydrator\Factory\HydratorFactory — getHydratorClass() is where generation-on-demand, file writing, and require happen.sys_get_temp_dir(), namespace GeneratedHydratorGeneratedClass, name includes a hash of the FQCN). Reading 20 lines of generated PHP answers most behavioural questions faster than the docs.GeneratedHydrator\ClassGenerator\HydratorGenerator — only relevant if you want to customise code output.First real use case in a Laravel app: turning raw arrays into typed read models instead of Model::fill(). Typical candidates are payloads from a queue job, rows from DB::table('orders')->get(), or an API response you want typed before it hits your domain:
$rows = DB::table('orders')->cursor();
foreach ($rows as $row) {
$orders[] = $hydrator->hydrate((array) $row, new OrderData());
}
That is the win: no per-property accessor calls, no ReflectionProperty::setAccessible(), no constructor with fifteen promoted arguments.
Memoise one hydrator instance per class in the container. Hydrator instances are stateless apart from the bound closures created in their constructor, so a single instance per class per process is correct and avoids re-running the constructor's closure setup.
namespace App\Support;
use GeneratedHydrator\Configuration;
final class HydratorLocator
{
/** @var array<class-string, object> */
private array $hydrators = [];
public function __construct(
private readonly string $targetDir,
private readonly bool $allowGeneration = true,
) {}
public function for(string $class): object
{
return $this->hydrators[$class] ??= (function () use ($class) {
$config = new Configuration($class);
$config->setGeneratedClassesTargetDir($this->targetDir);
$config->setAutoGenerate($this->allowGeneration);
$hydratorClass = $config->createFactory()->getHydratorClass();
return new $hydratorClass();
})();
}
public function hydrate(string $class, array $data): object
{
$object = (new \ReflectionClass($class))->newInstanceWithoutConstructor();
return $this->for($class)->hydrate($data, $object);
}
public function extract(object $object): array
{
return $this->for($object::class)->extract($object);
}
}
Register it in a service provider with a target dir under storage/ and generation disabled in production:
$this->app->singleton(HydratorLocator::class, fn () => new HydratorLocator(
targetDir: storage_path('framework/cache/hydrators'),
allowGeneration: ! $this->app->isProduction(),
));
Pre-generate at deploy time, not at request time. Add an Artisan command and run it in your deploy script before php artisan config:cache / Octane reload:
class WarmHydrators extends Command
{
protected $signature = 'hydrators:warm';
public function handle(HydratorLocator $hydrators): int
{
foreach (config('hydrators.classes', []) as $class) {
$hydrators->for($class);
$this->line("generated: {$class}");
}
return self::SUCCESS;
}
}
Keep the class list in config/hydrators.php (or scan a App\Data namespace with Finder once at build time) so production never writes files. This is also the only sane way to run on read-only filesystems — Lambda/Vapor images, distroless containers, or a root-owned storage/.
Snapshot and restore. extract() gives you a flat property-name array, which is ideal for caching or queue payloads where you want to rebuild a typed object later without touching the database:
Cache::put("order:{$id}", $hydrator->extract($order), 3600);
$order = $hydrators->hydrate(OrderData::class, Cache::get("order:{$id}"));
This pattern is the sweet spot for event-sourced aggregates and read models: extract() on write, hydrate() on read, no ORM in the loop.
Wrap it behind your own interface. Because the package is archived, do not let Configuration leak into twenty call sites. Define App\Contracts\Hydrator with hydrate(string $class, array $data): object and extract(object $object): array; the locator above is the only implementation. Swapping to Laminas\Hydrator\ReflectionHydrator, symfony/serializer + property-access, or cuyz/valinor later then becomes a one-file change.
Round-trip tests as a schema check. A test that does assertSame($entity, $hydrator->hydrate($hydrator->extract($entity), newInstanceWithoutConstructor(...))) catches renamed properties and forgotten parent properties cheaply. Do it once per DTO class in a data provider.
Archived and unmaintained. Last release January 2024; there is no one to fix a PHP 8.5/9 breakage. Treat it as a vendored utility: pin the version, keep usage behind your own interface, and be ready to fork or replace. For most new code, symfony/serializer + symfony/property-access (also generates and caches accessors), cuyz/valinor, or constructor-promoted DTOs with named arguments is a lower-risk default.
Argument order is hydrate(array $data, object $object). Data first, object second — the opposite of what most people guess and the opposite of Eloquent's fill(). extract() takes one argument and returns array.
It never constructs your objects. You must hand it an instance; it will not call the constructor. For classes with required constructor arguments use ReflectionClass::newInstanceWithoutConstructor(). Consequently no invariants are enforced: constructors, setters, validation, and readonly initialisation guards are all bypassed. Validate the input array before hydrating ($request->validated() is not enough — it is not a property whitelist either, see below).
No mass-assignment protection. Unlike Eloquent's $fillable, hydrate() writes every key in the array to a matching property, including internal ones. If the array comes from user input, filter keys explicitly first.
No filters, naming strategies, or casting. It is a bare Laminas\Hydrator\HydratorInterface implementation: array keys must match property names exactly (snake_case → camelCase conversion, DateTimeImmutable casting, nested object mapping all need to happen before or after). It has none of AbstractHydrator's naming strategies or filters.
Typed uninitialised properties will bite you on extract(). If newInstanceWithoutConstructor() leaves a typed property unset, extraction may throw rather than yield null. Give DTO properties defaults, or hydrate a fully constructed instance instead.
Magic methods are bypassed. Access is a direct property write inside class scope, so __set/__get never fire. If a class relies on magic for computed state, do not hydrate it.
readonly properties work exactly once, because the scope-bound closure counts as declaring-class scope — but re-hydrating the same instance or hydrating onto an already-initialised object throws an Error.
Filesystem and concurrency. Generation writes a file and requires it. In production with multiple FPM workers, concurrent first-requests race on the same path; a read-only filesystem throws outright; and a cached OPcache can serve a stale copy after redeploy. Solutions: pre-generate during deploy (setAutoGenerate(false) in production), put the target dir under storage/framework/cache/, and clear it before regenerating — class-name hashes change when a FQCN moves, so stale files accumulate forever otherwise.
Exclude the generated directory from git, PHPStan/Psalm paths, and PHP-CS-Fixer (.gitignore, phpstan.neon excludePaths). Generated classes are required by the factory, not autoloaded by Composer, so class_exists() on them will be false and static analysis will produce noise.
Don't use it on Eloquent models. It bypasses $casts, accessors/mutators, $fillable, $attributes, and dirty tracking — you would be writing properties that the ORM then overwrites. Use forceFill() there. GeneratedHydrator is for plain PHP objects only.
Benchmark before committing to it. Generation is the expensive part (measurable in milliseconds per class) and pays off only when you amortise it across thousands of hydration calls. A controller hydrating three DTOs will be slower with this package than with hand-written constructors. The win appears with cursor-streamed result sets, importers, queue consumers batching thousands of objects, and read-model rebuilds — measure your actual payload size, and remember Octane workers keep the memoised hydrators warm but must still not generate on the request path.
How can I help you explore Laravel packages today?