การทำ Object hydration ดูเหมือนจะเป็นปัญหาที่ได้รับการแก้ไขแล้ว จนกว่าคุณจะเริ่มวัดผลมันจริงๆ คุณดึงข้อมูลแถวหนึ่งจากฐานข้อมูล แมปอาร์เรย์เป็นออบเจกต์ แล้วก็ไปต่อ พวกเราส่วนใหญ่มองว่ามันเป็นแค่เรื่องงานระบบพื้นฐาน (plumbing)—มองไม่เห็น ไม่น่าตื่นเต้น และคิดว่ามันเร็วพออยู่แล้ว แต่แล้ววันหนึ่ง เมื่อคุณทำ profiling งานแบบ batch job หรือ queue worker คุณจะสังเกตเห็นว่าเวลาของ CPU ส่วนใหญ่หายไปกับตัว mapper อย่างน่าตกใจ ความตระหนักรู้นี้เองที่นำไปสู่การสร้าง HydraType ซึ่งเป็น hydrator ที่สร้างขึ้นภายใต้กฎเหล็กข้อหนึ่งคือ: คลาสไม่ควรต้องแบกรับภาระจากฟีเจอร์ที่พร็อพเพอร์ตี้ (properties) ของมันไม่ได้ใช้งาน
หากฟิลด์นั้นเป็นสตริงธรรมดาที่ไม่ต้องมีการแปลงค่าใดๆ โค้ดที่ถูกสร้างขึ้นควรจะดูเหมือนเขียนด้วยมือโดยมนุษย์ คือการกำหนดค่าโดยตรง (Direct assignment) ไม่มีการใช้ลูป ไม่มีการใช้ pipeline และไม่มีการใช้ reflection
What Hydration Actually Costs
มองเผินๆ การเปลี่ยน ['name' => 'Alice', 'age' => 30] ให้เป็นออบเจกต์ User ดูเหมือนจะเป็นเรื่องง่ายๆ แต่ปัญหาจะเริ่มขึ้นเมื่อคุณต้องการความยืดหยุ่น Hydrator ทั่วไปส่วนใหญ่มักพึ่งพา reflection เพื่อตรวจสอบคลาสในขณะรันไทม์ (runtime) พวกมันสร้าง metadata maps, จัดการเรื่อง type coercion และจัดการกับ nested object graphs นอกจากนี้พวกมันยังชอบใช้ pipeline อีกด้วย ทุกๆ ค่าจะต้องวิ่งผ่านลำดับของ mutators, assertions และ transformers แม้ว่าลำดับเหล่านั้นจะว่างเปล่าก็ตาม ซึ่งโครงสร้างของลูปเองก็สร้าง overhead เพิ่มขึ้นด้วย
หากดึงข้อมูลเพียงไม่กี่สิบเรคคอร์ด คุณจะไม่สังเกตเห็นเลย แต่แอปพลิเคชัน PHP สมัยใหม่ต้องประมวลผลข้อความในคิว (queue messages) เป็นพันๆ ข้อความ นำเข้าไฟล์ CSV ขนาดมหึมา หรือทำ hydration กับชุดผลลัพธ์ที่ซับซ้อนจาก Elasticsearch เป็นประจำ ในสถานการณ์เหล่านั้น ตัว mapper ที่กินเวลาแม้เพียงไม่กี่ไมโครวินาทีต่อฟิลด์จะกลายเป็นคอขวด (bottleneck) ที่แท้จริง ออบเจกต์หนึ่งพันชิ้นที่มีแปดฟิลด์ต่อชิ้น จะกลายเป็นภาระ (tax) ที่คุณสัมผัสได้ชัดเจน
The Zero-Overhead Philosophy
แนวคิดหลักเบื้องหลัง HydraType คือการแยกฟีเจอร์ออกจากกัน (feature isolation) แม้จะรองรับทั้งการแปลงประเภทข้อมูล (type conversion), การทำ nested object hydration และ assertions แต่สิ่งเหล่านี้ก็ไม่ใช่สิ่งที่บังคับต้องมีเสมอไป หากพร็อพเพอร์ตี้ต้องการเพียงแค่การคัดลอกค่าจากอาร์เรย์ไปยังออบเจกต์โดยตรง ตัว writer ที่ถูกสร้างขึ้นก็จะทำเพียงแค่นั้น โดยไม่มี pipeline ส่วนกลางที่บังคับให้ฟิลด์ประเภท scalar ธรรมดาต้องไปต่อคิวรอ validator ที่มันไม่ได้ใช้งาน
เรื่องนี้ทำได้ยากกว่าที่คิด ไลบรารีหลายแห่งใช้เส้นทางการทำงาน (execution path) เดียวกันสำหรับทุกฟิลด์เพื่อให้โค้ดมีขนาดเล็กและเป็นระเบียบ แต่ HydraType เลือกใช้วิธีที่ตรงกันข้าม โดยจะสร้างคลาส PHP ที่เป็นเอกลักษณ์สำหรับแต่ละ DTO เป้าหมาย และสร้างเฉพาะการทำงานที่คลาสนั้นๆ ต้องการเท่านั้น ความซับซ้อนจึงเป็นสิ่งที่เลือกใช้ได้ (opt-in) ไม่ใช่ภาระที่ถูกบังคับใช้กับทุกพร็อพเพอร์ตี้
How the Speed Happens
มีทางเลือกที่เป็นรูปธรรม 4 ประการที่ทำให้ HydraType แตกต่างจากเครื่องมือที่ใช้ reflection ทั่วไป หรือแม้แต่แนวทางการสร้างโค้ด (generated approaches) แบบอื่นๆ
1. Generate Code Once, Run It Forever
HydraType จะตรวจสอบคลาสเป้าหมายของคุณเพียงครั้งเดียว จากนั้นจะเขียนคลาส PHP เฉพาะทางที่ปรับแต่งให้เข้ากับโครงสร้างของคลาสนั้นโดยเฉพาะ หาก DTO ของคุณมี public string $name, int $status และ private DateTime $createdAt ตัว writer ที่ถูกสร้างขึ้นจะทราบข้อมูลเหล่านี้ล่วงหน้าทั้งหมด จะไม่มีการใช้ reflection ซ้ำซ้อน ไม่มีการ parse string และไม่มีการสร้าง metadata แบบ lazy ในขณะรันไทม์ เมื่อ OPCache ทำการ compile ไฟล์ที่ถูกสร้างขึ้นแล้ว ตัว Hydrator จะทำงานได้ไม่ต่างจากโค้ด PHP ที่เขียนด้วยมือเลย
2. Eliminate Empty Pipelines
Hydrator ส่วนใหญ่มักวางโครงสร้างการทำงานไว้รอบๆ runtime pipeline โดยจะเก็บอาร์เรย์ของ callables เช่น mutators, validators และ transformers แล้ววนลูปผ่านทุกๆ ค่า แม้ว่าอาร์เรย์เหล่านั้นจะว่างเปล่า แต่ foreach หรือ array_reduce ก็ยังต้องทำงานอยู่ดี HydraType ตัดส่วนนี้ออกไปทั้งหมด ในระหว่างการสร้างโค้ด มันจะตรวจสอบว่าพร็อพเพอร์ตี้ต้องการอะไรจริงๆ หากรายการการทำงานว่างเปล่า มันจะสร้างการกำหนดค่าแบบธรรมดา เช่น $object->name = $data['name']; ในขณะรันไทม์จะไม่มีการวนลูปใดๆ ทั้งสิ้น เพราะตัว generator ได้คิดแทนให้เรียบร้อยแล้ว
3. Use Closure Scoping to Kill Reflection Writes
พร็อพเพอร์ตี้ที่เป็น private คือปัญหาคลาสสิกสำหรับ mapper ภายนอก ทางออกปกติคือการใช้ ReflectionProperty::setValue() แต่เมธอดนั้นมีต้นทุน (penalty) ที่สูงมากเมื่อเทียบกับการเข้าถึงพร็อพเพอร์ตี้โดยตรง HydraType หลีกเลี่ยงปัญหานี้โดยการใช้ Closure::bind ตัว writer ที่ถูกสร้างขึ้นจะมี closure ที่มี scope อยู่ในคลาสเป้าหมาย ซึ่งช่วยให้สามารถเข้าถึงพร็อพเพอร์ตี้ที่เป็น private และ protected ได้โดยตรงโดยไม่ต้องใช้ reflection การ bind จะเกิดขึ้นเพียงครั้งเดียวในขั้นตอนการสร้าง (construction) หลังจากนั้น closure จะทำงานด้วยความเร็วที่ใกล้เคียงกับโค้ดปกติ (near-native speed)
4. Reuse the Writer
ไลบรารีบางแห่งจะสร้าง instance ของ strategy หรือ reflection graph ใหม่สำหรับทุกๆ ออบเจกต์ที่ทำ hydration แต่ HydraType จะสร้าง writer ขึ้นมาเพียงครั้งเดียว แล้วนำ instance นั้นกลับมาใช้ซ้ำกับออบเจกต์นับพันชิ้น ต้นทุนต่อออบเจกต์จึงลดลงเหลือเพียงแค่การกำหนดค่าพร็อพเพอร์ตี้และส่งคืนผลลัพธ์เท่านั้น
ตัวเลขสถิติ
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.
