Гідратація об'єктів здається вирішеною проблемою, поки ви не почнете її вимірювати. Ви дістаєте рядок із бази даних, мапите масив в об'єкт і йдете далі. Більшість із нас ставиться до цього як до технічної рутини — непомітної, нецікавої та такої, що за замовчуванням є достатньо швидкою. Але одного дня ви профілюєте пакетне завдання або воркер черги й помічаєте, що дивовижна частина часу процесора зникає в мапері. Це усвідомлення саме й призвело до створення HydraType — гідратора, побудованого навколо одного непохитного правила: клас ніколи не повинен платити за функції, які не використовують його властивості.
Якщо поле є простим рядком, який не потребує жодної трансформації, згенерований код має виглядати так, ніби його написала людина вручну. Пряме присвоєння. Жодних циклів, жодних конвеєрів, жодної рефлексії.
Скільки насправді коштує гідратація
На перший погляд, перетворення ['name' => 'Alice', 'age' => 30] на об'єкт User здається тривіальним. Проблеми починаються, коли вам потрібна гнучкість. Більшість універсальних гідраторів покладаються на рефлексію для перевірки класів під час виконання. Вони будують карти метаданих, узгоджують приведення типів і розв'язують вкладені графи об'єктів. Вони також обожнюють конвеєри. Кожне значення проходить через послідовність мутаторів, асерцій та трансформерів, навіть якщо ця послідовність порожня. Сама структура циклу створює додаткові витрати.
Якщо ви отримаєте кілька десятків записів, ви ніколи цього не помітите. Але сучасні PHP-додатки регулярно обробляють тисячі повідомлень черги, імпортують масивні набори CSV або гідратують глибокі набори результатів з Elasticsearch. У таких сценаріях мапер, який витрачає навіть кілька мікросекунд на кожне поле, стає справжнім вузьким місцем. Тисяча об'єктів, кожен з яких має вісім полів, перетворюється на податок, який ви відчуєте на практиці.
Філософія нульових накладних витрат
Центральною ідеєю HydraType є ізоляція функцій. Перетворення типів, гідратація вкладених об'єктів та асерції — усе це підтримується, проте жодне з них не є обов'язковим. Якщо властивості не потрібно нічого, крім прямого копіювання з масиву в об'єкт, згенерований writer містить саме це. Немає спільного конвеєра, який змушував би просте скалярне поле чекати в черзі за валідаторами, які йому не потрібні.
Це складніше, ніж здається. Багато бібліотек використовують єдиний шлях виконання для кожного поля, оскільки це дозволяє тримати кодову базу маленькою та однорідною. HydraType обирає протилежний підхід. Він генерує унікальний PHP-клас для кожного цільового DTO, створюючи саме ті операції, які потрібні конкретному класу. Складність стає опціональною, а не загальним податком на кожну властивість.
Як досягається швидкість
Чотири конкретні рішення відрізняють HydraType як від загальних інструментів на основі рефлексії, так і від інших підходів із генерацією коду.
1. Генеруйте код один раз, запускайте назавжди
HydraType перевіряє ваш цільовий клас лише один раз, а потім записує спеціалізований PHP-клас, адаптований саме під його структуру. Якщо ваш DTO містить публічний рядок $name, ціле число $status та приватний DateTime $createdAt, згенерований writer знає про це заздалегідь. Немає повторної рефлексії, парсингу рядків чи лінивого побудування метаданих під час виконання. Як тільки OPCache компілює згенерований файл, HydraType стає невідрізним від написаного вручну PHP-коду.
2. Усунення порожніх конвеєрів
Більшість гідраторів будують свою роботу навколо конвеєра під час виконання. Вони підтримують масив callables — мутаторів, валідаторів, трансформерів — і ітерують по кожному значенню. Навіть коли ці масиви порожні, foreach або array_reduce все одно виконуються. HydraType повністю усуває це. Під час генерації коду він перевіряє, що насправді потрібно властивості. Якщо список операцій порожній, він видає звичайне присвоєння, наприклад: $object->name = $data['name'];. Під час виконання нічого не ітерується, тому що генератор уже все продумав.
3. Використання області видимості замикань для усунення рефлексії
Приватні властивості — це класична головна біль для зовнішніх маперів. Звичним виходом є ReflectionProperty::setValue(), але цей метод несе великі витрати порівняно з прямим доступом до властивості. HydraType обходить це за допомогою Closure::bind. Згенерований writer містить замикання (closures), обмежені областю видимості цільового класу, що дозволяє їм безпосередньо звертатися до приватних та захищених (protected) властивостей без використання рефлексії. Прив'язка відбувається один раз під час конструювання; після цього замикання працює майже нативною швидкістю.
4. Повторне використання writer
Деякі бібліотеки створюють нову стратегію або граф рефлексії для кожного окремого об'єкта, який вони гідратують. HydraType створює свій writer один раз, а потім повторно використовує цей екземпляр для тисяч об'єктів. Витрати на кожен об'єкт зводяться до мінімуму: встановлення значень властивостей та повернення результату.
Цифри
На PHP 8.2 різниця є разючою:
- HydraType: 267,9 нс
- Ocramius GeneratedHydrator: 307,9 нс
- Symfony PropertyNormalizer: 8 499,0 нс
- Valinor: 10 755,2 нс
Symfony PropertyNormalizer та Valinor — це потужні інструменти, але вони є універсальними. PropertyNormalizer є частиною компонента серіалізації, який відповідає за визначення формату, нормалізатори та глибокі графіки об'єктів. Valinor суворо ставиться до дерев типів та приведення типів. Ця потужність має свою ціну. У цьому тесті вони були приблизно в тридцять-сорок разів повільнішими за HydraType. Навіть Ocramius GeneratedHydrator, який уже використовує генерацію коду, трохи відстає через архітектурні відмінності у конвеєрах (pipelines) та доступі до властивостей.
Контекст має значення. Один-єдиний запит до бази даних або HTTP-запит (roundtrip) затьмарює всі ці цифри. Якщо ви гідратуєте три об'єкти після запиту, гонитва за наносекундами — це марна трата енергії. Розрив стає суттєвим при масштабуванні. Пакетний процес, що гідратує п'ятдесят тисяч записів, або споживач черги (queue consumer), що відновлює об'єкти з масивів кешу, відчує різницю між 267 наносекундами та десятьма мікросекундами. Помножте це на кількість полів і рядків, і швидший мапер зможе скоротити час виконання завдання на кілька секунд, не змінюючи жодної бізнес-логіки.
Головний висновок
Урок тут не в тому, що кожному проєкту потрібен власний гідратор. А в тому, що архітектурні рішення мають накопичувальний ефект. Завдяки генерації коду, що включає лише те, про що ви просите, HydraType зберігає швидкість звичайних властивостей, водночас пропонуючи «запасний вихід» для складних випадків. Перетворення типів та вкладені об'єкти з'являються тоді, коли вони вам потрібні, але вони не стають перешкодою для продуктивності кожного скалярного рядка в об'єкті.
Перш ніж змінювати інструменти, проведіть профілювання. Якщо гідратація не фігурує у ваших реальних трасуваннях, то й виправляти нічого. Але якщо ви пропускаєте великі набори даних через універсальні мапери та спостерігаєте за перегрівом процесора, пам'ятайте, що звичайне присвоєння в PHP нікуди не зникло. Іноді найшвидший код — це просто той код, який ви написали б самі, згенерували один раз і забули.
