Hydration đối tượng có vẻ như là một vấn đề đã được giải quyết cho đến khi bạn bắt đầu đo lường nó. Bạn lấy một hàng từ cơ sở dữ liệu, ánh xạ mảng đó thành một đối tượng, và tiếp tục công việc. Hầu hết chúng ta coi nó như những đường ống kỹ thuật—vô hình, không mấy hấp dẫn, và được mặc định là đủ nhanh. Thế rồi một ngày nọ, khi bạn profile một batch job hoặc một queue worker, bạn nhận thấy một phần đáng kể thời gian CPU bị tiêu tốn vào bộ mapper. Sự nhận thức đó chính là điều đã dẫn đến HydraType, một bộ hydrator được xây dựng dựa trên một quy tắc cứng nhắc: một class không bao giờ nên phải trả giá cho những tính năng mà các thuộc tính của nó không sử dụng.

Nếu một trường là một chuỗi (string) thuần túy không cần bất kỳ sự biến đổi nào, mã được tạo ra phải trông như thể do con người viết tay. Gán trực tiếp. Không vòng lặp, không pipeline, không reflection.

Chi phí thực sự của Hydration

Thoạt nhìn, việc chuyển đổi ['name' => 'Alice', 'age' => 30] thành một đối tượng User có vẻ rất đơn giản. Rắc rối bắt đầu khi bạn muốn sự linh hoạt. Hầu hết các bộ hydrator đa năng đều dựa vào reflection để kiểm tra các class tại thời điểm runtime. Chúng xây dựng các bản đồ metadata, thực hiện ép kiểu (type coercion) và giải quyết các đồ thị đối tượng lồng nhau (nested object graphs). Chúng cũng rất ưa chuộng các pipeline. Mọi giá trị đều phải đi qua một chuỗi các mutator, assertion và transformer, ngay cả khi chuỗi đó trống rỗng. Bản thân cấu trúc vòng lặp cũng tạo ra chi phí overhead.

Nếu chỉ lấy vài chục bản ghi, bạn sẽ không bao giờ nhận ra. Nhưng các ứng dụng PHP hiện đại thường xuyên xử lý hàng nghìn tin nhắn hàng đợi, nhập các tập dữ liệu CSV khổng lồ, hoặc hydrate các tập kết quả sâu từ Elasticsearch. Trong những kịch bản đó, một bộ mapper tiêu tốn dù chỉ vài micro giây cho mỗi trường sẽ trở thành một nút thắt cổ chai thực sự. Một nghìn đối tượng với tám trường mỗi đối tượng sẽ nhân lên thành một loại "thuế" mà bạn có thể cảm nhận rõ rệt.

Triết lý Zero-Overhead

Ý tưởng cốt lõi đằng sau HydraType là sự cô lập tính năng. Việc chuyển đổi kiểu, hydration đối tượng lồng nhau và các assertion đều được hỗ trợ, nhưng không có cái nào là bắt buộc. Nếu một thuộc tính không cần gì ngoài việc sao chép trực tiếp từ mảng sang đối tượng, bộ writer được tạo ra sẽ chỉ chứa chính xác thao tác đó. Không có pipeline dùng chung nào ép buộc một trường scalar thuần túy phải xếp hàng chờ đợi sau các validator mà nó không cần đến.

Điều này khó đạt được hơn vẻ ngoài của nó. Nhiều thư viện sử dụng chung một đường dẫn thực thi cho mọi trường vì nó giúp mã nguồn nhỏ gọn và đồng nhất. HydraType đi theo hướng ngược lại. Nó tạo ra một class PHP duy nhất cho mỗi DTO mục tiêu, phát ra chính xác các thao tác mà class cụ thể đó yêu cầu. Sự phức tạp trở thành một lựa chọn (opt-in), chứ không phải là một loại thuế áp đặt lên mọi thuộc tính.

Làm thế nào để đạt được tốc độ

Bốn lựa chọn cụ thể giúp phân biệt HydraType với cả các công cụ dựa trên reflection thông thường và cả các phương pháp tạo mã khác.

1. Tạo mã một lần, chạy mãi mãi

HydraType kiểm tra class mục tiêu của bạn duy nhất một lần, sau đó viết một class PHP chuyên dụng được tinh chỉnh theo đúng cấu trúc của nó. Nếu DTO của bạn chứa một string public $name, một int $status, và một DateTime private $createdAt, bộ writer được tạo ra sẽ biết tất cả những điều này ngay từ đầu. Không có việc lặp lại reflection, không có phân tích chuỗi, và không có việc xây dựng metadata lười biếng (lazy metadata building) trong quá trình runtime. Một khi OPCache biên dịch tệp được tạo ra, Hydrator sẽ không khác gì mã PHP được viết tay.

2. Loại bỏ các pipeline trống

Hầu hết các bộ hydrator cấu trúc công việc của chúng xoay quanh một pipeline tại runtime. Chúng duy trì một mảng các callables—mutators, validators, transformers—và lặp qua mọi giá trị. Ngay cả khi các mảng đó trống, foreach hoặc array_reduce vẫn phải thực thi. HydraType loại bỏ hoàn toàn điều này. Trong quá trình tạo mã, nó kiểm tra xem một thuộc tính thực sự yêu cầu những gì. Nếu danh sách thao tác trống, nó sẽ phát ra một lệnh gán thuần túy như $object->name = $data['name'];. Runtime không bao giờ phải lặp qua bất cứ thứ gì, vì bộ tạo mã (generator) đã thực hiện việc tính toán đó rồi.

3. Sử dụng Closure Scoping để loại bỏ việc ghi bằng Reflection

Các thuộc tính private là một vấn đề đau đầu kinh điển đối với các mapper bên ngoài. Giải pháp thoát hiểm thông thường là ReflectionProperty::setValue(), nhưng phương thức đó đi kèm với một cái giá rất đắt so với việc truy cập thuộc tính trực tiếp. HydraType né tránh điều này bằng cách sử dụng Closure::bind. Bộ writer được tạo ra chứa các closure có phạm vi (scoped) trong class mục tiêu, cho phép chúng chạm trực tiếp vào các thuộc tính private và protected mà không cần reflection. Việc binding diễn ra một lần duy nhất trong quá trình khởi tạo; sau đó, closure sẽ chạy với tốc độ gần như bản địa (near-native speed).

4. Tái sử dụng Writer

Một số thư viện khởi tạo một chiến lược mới hoặc một đồ thị reflection cho mỗi đối tượng mà chúng hydrate. HydraType tạo ra bộ writer của nó một lần, sau đó tái sử dụng instance đó cho hàng nghìn đối tượng. Chi phí trên mỗi đối tượng giảm xuống mức tối thiểu, chỉ còn là việc gán giá trị thuộc tính và trả về kết quả.

Các con số

Trên PHP 8.2, sự khác biệt là rất rõ rệt:

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

Symfony PropertyNormalizer và Valinor là những công cụ mạnh mẽ, nhưng chúng mang tính đa năng. PropertyNormalizer là một phần của thành phần serializer xử lý việc phát hiện định dạng, các bộ chuẩn hóa (normalizers) và các đồ thị đối tượng sâu (deep object graphs). Valinor rất khắt khe về cây kiểu dữ liệu (type trees) và ép kiểu (coercion). Sức mạnh đó đi kèm với cái giá phải trả. Trong bài kiểm tra này, chúng chậm hơn HydraType khoảng ba mươi đến bốn mươi lần. Ngay cả Ocramius GeneratedHydrator, vốn đã sử dụng tạo mã (code generation), cũng tụt lại phía sau một chút do những khác biệt về kiến trúc xoay quanh các pipeline và việc truy cập thuộc tính.

Ngữ cảnh rất quan trọng. Một truy vấn cơ sở dữ liệu duy nhất hoặc một vòng lặp HTTP (HTTP roundtrip) sẽ làm lu mờ tất cả những con số này. Nếu bạn chỉ đang hydrate ba đối tượng sau một truy vấn, việc chạy theo từng nano giây là một sự lãng phí năng lượng. Khoảng cách này chỉ trở nên có ý nghĩa khi bạn mở rộng quy mô. Một tiến trình xử lý hàng loạt (batch process) hydrate năm mươi nghìn bản ghi, hoặc một trình tiêu thụ hàng đợi (queue consumer) tái tạo các đối tượng từ mảng cache, sẽ cảm nhận rõ sự khác biệt giữa 267 nano giây và mười micro giây. Nhân lên qua các trường và các hàng, và bộ ánh xạ (mapper) nhanh hơn có thể cắt giảm được vài giây cho một công việc mà không cần thay đổi bất kỳ logic nghiệp vụ nào.

Bài học thực sự

Bài học ở đây không phải là mọi dự án đều cần một hydrator tùy chỉnh. Mà là các lựa chọn về kiến trúc sẽ có tác động cộng dồn. Bằng cách tạo ra mã chỉ bao gồm những gì bạn yêu cầu, HydraType giữ cho các thuộc tính thông thường (plain properties) luôn nhanh chóng trong khi vẫn cung cấp một "lối thoát" (escape hatch) cho các thuộc tính phức tạp. Việc chuyển đổi kiểu và các đối tượng lồng nhau sẽ xuất hiện khi bạn cần, nhưng chúng không làm ảnh hưởng đến hiệu suất kiểm tra cho mọi chuỗi vô hướng (scalar string) trên đối tượng.

Trước khi bạn chuyển đổi công cụ, hãy thực hiện profiling. Nếu việc hydrate không xuất hiện trong các vết (traces) thực tế của bạn, thì không có gì cần phải sửa chữa. Nhưng nếu bạn đang đẩy các tập dữ liệu lớn qua các bộ ánh xạ chung chung và thấy CPU bị quá tải, hãy nhớ rằng việc gán giá trị PHP thông thường vẫn luôn tồn tại. Đôi khi, mã nhanh nhất đơn giản là mã mà chính bạn sẽ viết, được tạo ra một lần và sau đó được quên đi.