تبدو عملية الـ hydration للكائنات وكأنها مشكلة محلولة حتى تبدأ في قياسها. أنت تجلب صفاً من قاعدة البيانات، ثم تقوم بربط المصفوفة بكائن (object)، وتستمر في عملك. معظمنا يتعامل مع الأمر كأنه مجرد "أعمال سباكة" خلف الكواليس—غير مرئية، وغير مثيرة للاهتمام، ويُفترض أنها سريعة بما يكفي. ثم يأتي يوم تقوم فيه بتحليل أداء وظيفة دفعية (batch job) أو عامل طابور (queue worker) وتلاحظ أن جزءاً مفاجئاً من وقت وحدة المعالجة المركزية (CPU) يضيع في عملية الربط (mapper). هذا الإدراك هو بالضبط ما أدى إلى إنشاء HydraType، وهو محول (hydrator) بُني حول قاعدة واحدة صارمة: يجب ألا تدفع الفئة (class) ثمن ميزات لا تستخدمها خصائصها.

إذا كان الحقل عبارة عن سلسلة نصية (string) بسيطة لا تحتاج إلى أي تحويل، فيجب أن يبدو الكود المولد كما لو أن بشراً كتبه يدوياً. تعيين مباشر. لا حلقات تكرارية، لا أنابيب معالجة، ولا انعكاس (reflection).

التكلفة الحقيقية لعملية الـ Hydration

للوهلة الأولى، يبدو تحويل ['name' => 'Alice', 'age' => 30] إلى كائن User أمراً تافهاً. لكن المشكلة تبدأ عندما تريد المرونة. تعتمد معظم أدوات الـ hydration عامة الغرض على الـ reflection لفحص الفئات في وقت التشغيل. فهي تبني خرائط البيانات الوصفية (metadata maps)، وتتعامل مع إكراه الأنواع (type coercion)، وتحل الرسوم البيانية للكائنات المتداخلة (nested object graphs). كما أنها تعشق أنابيب المعالجة (pipelines)؛ حيث تمر كل قيمة عبر سلسلة من المغيرات (mutators)، والتحققات (assertions)، والمحولات (transformers)، حتى لو كانت تلك السلسلة فارغة. بل إن هيكل الحلقة التكرارية بحد ذاته يضيف عبئاً إضافياً.

إذا قمت بجلب بضع عشرات من السجلات، فلن تلاحظ ذلك أبداً. ولكن تطبيقات PHP الحديثة تعالج بشكل روتيني آلاف الرسائل في الطوابير، أو تستورد مجموعات ضخمة من ملفات CSV، أو تقوم بعمل hydration لمجموعات نتائج عميقة من Elasticsearch. في هذه السيناريوهات، يصبح الـ mapper الذي يستهلك ولو بضعة ميكروثانية لكل حقل بمثابة عنق زجاجة حقيقي. ألف كائن، يحتوي كل منها على ثمانية حقول، يضاعف هذا الاستهلاك ليصبح ضريبة تشعر بها فعلياً.

فلسفة "العبء الصفري" (Zero-Overhead)

الفكرة المركزية وراء HydraType هي عزل الميزات. يتم دعم تحويل الأنواع، و الـ hydration للكائنات المتداخلة، والتحققات، ومع ذلك لا يوجد أي منها إلزامي. إذا كانت الخاصية لا تحتاج سوى لنسخ مباشر من المصفوفة إلى الكائن، فإن الـ writer المولد يحتوي على ذلك بالضبط. لا توجد أنابيب معالجة مشتركة تجبر حقلاً قياسياً (scalar field) بسيطاً على الانتظار في طابور خلف أدوات التحقق التي لا يحتاجها.

تحقيق هذا الأمر أصعب مما يبدو. تشترك العديد من المكتبات في مسار تنفيذ واحد لكل حقل لأن ذلك يحافظ على صغر حجم الكود وتجانسه. يتخذ HydraType النهج المعاكس؛ فهو يولد فئة PHP فريدة لكل DTO مستهدف، مُصدراً العمليات التي تتطلبها تلك الفئة تحديداً. تصبح التعقيدات اختيارية، وليست ضريبة شاملة على كل خاصية.

كيف تتحقق هذه السرعة

هناك أربعة خيارات ملموسة تميز HydraType عن الأدوات العامة القائمة على الـ reflection وحتى عن أساليب التوليد الأخرى.

1. توليد الكود مرة واحدة، وتشغيله للأبد

يقوم HydraType بفحص الفئة المستهدفة مرة واحدة فقط، ثم يكتب فئة PHP مخصصة مصممة لتناسب شكلها تماماً. إذا كان الـ DTO الخاص بك يحتوي على $name من نوع string عام، و $status من نوع int، و $createdAt من نوع DateTime خاص، فإن الـ writer المولد يعرف كل هذا مسبقاً. لا يوجد انعكاس (reflection) متكرر، ولا تحليل للنصوص، ولا بناء بيانات وصفية كسول أثناء وقت التشغيل. وبمجرد أن يقوم OPCache بتجميع الملف المولد، يصبح الـ Hydrator غير قابل للتمييز عن كود PHP المكتوب يدوياً.

2. التخلص من أنابيب المعالجة الفارغة

تعتمد معظم أدوات الـ hydration في عملها على أنابيب معالجة أثناء وقت التشغيل. فهي تحتفظ بمصفوفة من الدوال القابلة للاستدعاء (callables)—المغيرات، والتحققات، والمحولات—وتقوم بالتكرار عبر كل قيمة. وحتى عندما تكون تلك المصفوفات فارغة، فإن foreach أو array_reduce لا تزال تُنفذ. يقوم HydraType بإزالة هذا تماماً؛ فخلال عملية توليد الكود، يتحقق مما تتطلبه الخاصية فعلياً. إذا كانت قائمة العمليات فارغة، فإنه يصدر تعييناً بسيطاً مثل $object->name = $data['name'];. لا يقوم وقت التشغيل بالتكرار عبر أي شيء، لأن المولد قد قام بعملية التفكير مسبقاً.

3. استخدام نطاق الإغلاق (Closure Scoping) لتجنب عمليات الكتابة عبر الـ Reflection

تعتبر الخصائص الخاصة (private properties) صداعاً كلاسيكياً لأدوات الربط الخارجية. المخرج المعتاد هو استخدام ReflectionProperty::setValue()، ولكن هذه الطريقة تحمل عقوبة ثقيلة مقارنة بالوصول المباشر إلى الخاصية. يتجنب HydraType ذلك باستخدام Closure::bind. يحتوي الـ writer المولد على closures محصورة في نطاق الفئة المستهدفة، مما يسمح لها بالوصول إلى الخصائص الخاصة والمحمية (protected) مباشرة دون الحاجة إلى reflection. يتم الربط مرة واحدة أثناء الإنشاء؛ وبعد ذلك، يعمل الـ closure بسرعة تقارب السرعة الأصلية للغة.

4. إعادة استخدام الـ Writer

تقوم بعض المكتبات بإنشاء استراتيجية جديدة أو رسم بياني للانعكاس (reflection graph) لكل كائن يتم عمل hydration له. أما HydraType فيقوم بإنشاء الـ writer الخاص به مرة واحدة، ثم يعيد استخدام تلك النسخة عبر آلاف الكائنات. تنخفض التكلفة لكل كائن إلى الحد الأدنى المتمثل في تعيين قيم الخصائص وإرجاع النتيجة.

الأرقام

في PHP 8.2، يبدو الفرق شاسعاً:

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

تُعد Symfony PropertyNormalizer و Valinor أدوات قوية، لكنها أدوات عامة. يُعد PropertyNormalizer جزءاً من مكون serializer يتعامل مع اكتشاف التنسيق (format detection)، والمُطبعات (normalizers)، ورسوم الكائنات البيانية العميقة (deep object graphs). أما Valinor، فهو صارم للغاية فيما يتعلق بأشجار الأنواع (type trees) والتحويل القسري (coercion). وتأتي هذه القوة بتكلفة معينة. في هذا الاختبار، كانت هذه الأدوات أبطأ بنحو ثلاثين إلى أربعين مرة من HydraType. وحتى Ocramius GeneratedHydrator، الذي يستخدم بالفعل توليد الكود (code generation)، يتأخر قليلاً بسبب الاختلافات المعمارية المتعلقة بمسارات المعالجة (pipelines) والوصول إلى الخصائص (property access).

السياق مهم. فاستعلام واحد لقاعدة البيانات أو رحلة ذهاب وإياب عبر HTTP (HTTP roundtrip) تجعل كل هذه الأرقام تبدو ضئيلة. إذا كنت تقوم بعملية hydration لثلاثة كائنات فقط بعد استعلام ما، فإن مطاردة النانو ثانية هي مضيعة للطاقة. لكن الفجوة تصبح ذات مغزى عند التوسع (scaling). فعملية معالجة دفعية (batch process) تقوم بعمل hydration لخمسين ألف سجل، أو مستهلك طابور (queue consumer) يعيد بناء الكائنات من مصفوفات التخزين المؤقت (cache arrays)، سيشعر بالفرق بين 267 نانو ثانية وعشرة ميكروثانية. وعند ضرب هذا الفرق في عدد الحقول والصفوف، يمكن للمُحوّل (mapper) الأسرع أن يوفر ثوانٍ من وقت المهمة دون تغيير أي منطق عمل (business logic).

الخلاصة الحقيقية

الدرس هنا ليس أن كل مشروع يحتاج إلى hydrator مخصص، بل هو أن الخيارات المعمارية تتراكم آثارها. من خلال توليد كود يتضمن فقط ما تطلبه، يحافظ HydraType على سرعة الخصائص البسيطة (plain properties) مع توفير مخرج للتعامل مع الخصائص المعقدة. عمليات تحويل الأنواع (type conversion) والكائنات المتداخلة (nested objects) موجودة عندما تحتاج إليها، لكنها لا تفرض ضريبة الأداء على كل سلسلة نصية بسيطة (scalar string) في الكائن.

قبل أن تغير أدواتك، قم بإجراء تحليل للأداء (profile). إذا لم تظهر عملية الـ hydration في تتبع الأداء الواقعي الخاص بك، فلا يوجد شيء لإصلاحه. ولكن إذا كنت تقوم بتمرير مجموعات بيانات ضخمة عبر محولات عامة (generic mappers) وتراقب استهلاك المعالج (CPU) المرتفع، فتذكر أن عملية التعيين البسيطة في PHP (plain PHP assignment) لا تزال موجودة. أحياناً يكون الكود الأسرع هو ببساطة الكود الذي كنت ستكتبه بنفسك، والذي يتم توليده مرة واحدة ثم نسيانه.