L'idratazione degli oggetti sembra un problema risolto finché non la misuri. Recuperi una riga dal database, mappi l'array in un oggetto e vai avanti. La maggior parte di noi la considera semplice "idraulica": invisibile, poco attraente e presunta sufficientemente veloce. Poi un giorno analizzi un batch job o un worker di una coda e ti accorgi che una parte sorprendente del tempo di CPU svanisce nel mapper. Questa consapevolezza è esattamente ciò che ha portato a HydraType, un idratatore costruito attorno a una regola ostinata: una classe non dovrebbe mai pagare per funzionalità che le sue proprietà non utilizzano.

Se un campo è una semplice stringa che non richiede alcuna trasformazione, il codice generato dovrebbe sembrare scritto a mano da un essere umano. Assegnazione diretta. Niente loop, niente pipeline, niente riflessione.

Cosa costa realmente l'idratazione

A prima vista, trasformare ['name' => 'Alice', 'age' => 30] in un oggetto User sembra banale. Il problema inizia quando cerchi flessibilità. La maggior parte degli idratatori generici si affida alla riflessione (reflection) per ispezionare le classi a runtime. Costruiscono mappe di metadati, gestiscono la coercizione dei tipi e risolvono grafi di oggetti annidati. Amano anche le pipeline. Ogni valore attraversa una sequenza di mutatori, asserzioni e trasformatori, anche se quella sequenza è vuota. La struttura del loop stessa aggiunge overhead.

Recupera qualche decina di record e non te ne accorgerai mai. Ma le moderne applicazioni PHP elaborano regolarmente migliaia di messaggi in coda, importano enormi set di CSV o idratano profondi set di risultati da Elasticsearch. In questi scenari, un mapper che consuma anche solo pochi microsecondi per campo diventa un vero collo di bottiglia. Mille oggetti con otto campi ciascuno si moltiplicano in una "tassa" che si percepisce concretamente.

La filosofia "Zero-Overhead"

L'idea centrale dietro HydraType è l'isolamento delle funzionalità. La conversione dei tipi, l'idratazione di oggetti annidati e le asserzioni sono tutte supportate, eppure nessuna di esse è obbligatoria. Se una proprietà non richiede altro che una copia diretta dall'array all'oggetto, il writer generato contiene esattamente quello. Non esiste una pipeline condivisa che costringa un semplice campo scalare ad aspettare in coda dietro validatori di cui non ha bisogno.

È più difficile da ottenere di quanto sembri. Molte librerie condividono un unico percorso di esecuzione per ogni campo perché mantiene il codebase piccolo e uniforme. HydraType adotta l'approccio opposto. Genera una classe PHP unica per ogni DTO di destinazione, emettendo precisamente le operazioni richieste da quella specifica classe. La complessità diventa opzionale (opt-in), non una tassa generalizzata su ogni proprietà.

Come si ottiene la velocità

Quattro scelte concrete distinguono HydraType sia dagli strumenti generici basati sulla riflessione, sia da altri approcci basati sulla generazione di codice.

1. Genera il codice una volta, eseguilo per sempre

HydraType ispeziona la tua classe di destinazione una sola volta, poi scrive una classe PHP dedicata, adattata alla sua forma esatta. Se il tuo DTO contiene una stringa pubblica $name, un intero $status e un DateTime privato $createdAt, il writer generato lo sa già in anticipo. Non c'è riflessione ripetuta, né parsing di stringhe, né costruzione pigra (lazy) dei metadati durante l'esecuzione. Una volta che OPCache compila il file generato, l'idratatore è indistinguibile dal PHP scritto a mano.

2. Elimina le pipeline vuote

La maggior parte degli idratatori struttura il proprio lavoro attorno a una pipeline a runtime. Mantengono un array di callable — mutatori, validatori, trasformatori — e iterano su ogni valore. Anche quando quegli array sono vuoti, il foreach o array_reduce vengono comunque eseguiti. HydraType elimina completamente questo aspetto. Durante la generazione del codice, controlla ciò di cui una proprietà ha effettivamente bisogno. Se la lista delle operazioni è vuota, emette una semplice assegnazione come $object->name = $data['name'];. A runtime non viene iterato nulla, perché il generatore ha già fatto il lavoro di analisi.

3. Usa lo scoping delle Closure per eliminare la riflessione

Le proprietà private sono un classico mal di testa per i mapper esterni. La solita via d'uscita è ReflectionProperty::setValue(), ma quel metodo comporta un pesante costo rispetto all'accesso diretto alle proprietà. HydraType lo aggira utilizzando Closure::bind. Il writer generato contiene closure con scope limitato alla classe di destinazione, il che permette loro di accedere direttamente alle proprietà private e protette senza usare la riflessione. Il binding avviene una sola volta durante la costruzione; dopo di che, la closure viene eseguita a una velocità quasi nativa.

4. Riusa il writer

Alcune librerie istanziano una nuova strategia o un nuovo grafo di riflessione per ogni singolo oggetto che idratano. HydraType crea il suo writer una sola volta, per poi riutilizzare quell'istanza su migliaia di oggetti. Il costo per oggetto scende al minimo indispensabile: impostare i valori delle proprietà e restituire il risultato.

I numeri

On PHP 8.2, the difference is stark:

  • HydraType: 267.9 ns
  • Ocramius GeneratedHydrator: 307.9 ns
  • Symfony PropertyNormalizer: 8,499.0 ns
  • Valinor: 10,755.2 ns

Symfony PropertyNormalizer and Valinor are powerful tools, but they are generalists. PropertyNormalizer is part of a serializer component that handles format detection, normalizers, and deep object graphs. Valinor is rigorous about type trees and coercion. That power comes with cost. In this test, they were roughly thirty to forty times slower than HydraType. Even Ocramius GeneratedHydrator, which already uses code generation, sits slightly behind because of architectural differences around pipelines and property access.

Context matters. A single database query or an HTTP roundtrip dwarfs all of these numbers. If you are hydrating three objects after a query, chasing nanoseconds is a waste of energy. The gap becomes meaningful when you scale. A batch process hydrating fifty thousand records, or a queue consumer rebuilding objects from cache arrays, will feel the difference between 267 nanoseconds and ten microseconds. Multiply across fields and rows, and the faster mapper can shave seconds off a job without changing any business logic.

The Real Takeaway

The lesson here is not that every project needs a custom hydrator. It is that architecture choices compound. By generating code that includes only what you ask for, HydraType keeps plain properties fast while still offering an escape hatch for complex ones. Type conversion and nested objects exist when you need them, but they do not co-sign the performance check for every scalar string on the object.

Before you switch tools, profile. If hydration does not show up in your real-world traces, there is nothing to fix. But if you are pushing large datasets through generic mappers and watching CPU burn, remember that plain PHP assignment still exists. Sometimes the fastest code is simply the code you would have written yourself, generated once and then forgotten.