تُعد خاصية $casts في Laravel واحدة من تلك الميزات المريحة والهادئة التي تشكل طريقة تعاملك مع البيانات دون لفت الكثير من الانتباه. بمجرد إضافة نوع مدمج مثل boolean أو datetime إلى المصفوفة، يقوم Eloquent تلقائيًا بتحويل نصوص قاعدة البيانات الخام إلى شيء أكثر سهولة في التعامل قبل أن تصل القيمة إلى المتحكمات (controllers) أو العروض (views). هذا يحافظ على نظافة الكود الخاص بك. ولكن في اللحظة التي تحاول فيها استدعاء دالة مساعدة (helper) خاصة بك مباشرة داخل تلك المصفوفة، سيعترض الإطار. كتابة شيء مثل 'username' => 'encrypt_data()' لن تنجح؛ إذ يتوقع Laravel إما كلمة مفتاحية لعملية تحويل (cast) أصلية أو فئة (class) تطبق واجهة CastsAttributes. هذا المتطلب موجود لأن طبقة التحويل تعمل في عمق دورة الـ hydration والـ serialization الخاصة بـ Eloquent؛ فهي تحتاج إلى كائن يمكن التنبؤ به مع عقد (contract) واضح بدلاً من سلسلة نصية لدالة عشوائية قد لا تكون موجودة وقت التشغيل.

لماذا تفشل الدوال المساعدة في مصفوفة $casts

عندما يقوم Eloquent بتحميل النموذج (model)، فإنه يفحص مصفوفة $casts ليقرر كيفية تحويل كل عمود. يتعرف الإطار على أنواع أولية محددة — مثل string و integer و array و encrypted وما إلى ذلك — كما يتعرف على أسماء الفئات كاملة المسار (fully-qualified class names) التي تطبق CastsAttributes. هو لا يقوم بتقييم السلسلة النصية ككود برمجي. لذا، يتم التعامل مع 'encrypt_data()' كنوع تحويل غير معروف، مما يؤدي إلى حدوث خطأ. وحتى لو قام Laravel بتحليلها، فلن تكون هناك طريقة موثوقة لتمرير مثيل النموذج (model instance)، ومفتاح الخاصية (attribute key)، والقيمة الحالية، والخصائص المحيطة إلى دالتك بالترتيب الصحيح. توجد هذه الواجهة تحديدًا لتوحيد عملية التنسيق (handshake) تلك بين منطقك المخصص والعمليات الداخلية لـ Eloquent.

لديك طريقتان قويتان لسد هذه الفجوة. إحداهما تفضل إعادة الاستخدام على المدى الطويل، والأخرى تفضل السرعة عندما تحتاج فقط إلى إصلاح سريع داخل نموذج واحد.

الطريقة 1: كتابة فئة تحويل مخصصة (Custom Cast Class)

إذا كان نفس التحويل ينطبق على حقول متعددة أو ينتشر عبر عدة نماذج، فإن فئة التحويل المخصصة هي الخيار الأنظف. فهي تعيش في ملفها الخاص، ويمكن اختبارها كوحدة (unit-tested) بشكل مستقل، وتحافظ على نماذجك خالية من الأكواد المتكررة (boilerplate).

ابدأ بإنشاء app/Casts/CustomEncrypt.php. يجب أن يتطابق مساحة الاسم (namespace) مع إعداد التحميل التلقائي لديك، وعادة ما تكون App\Casts. يجب أن تطبق الفئة الواجهة Illuminate\Contracts\Database\Eloquent\CastsAttributes مما يجبرك على تعريف دالتين: get و set.

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class CustomEncrypt implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return decrypt_data($value);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return encrypt_data($value);
    }
}

توقيعات الدوال (signatures) مهمة هنا. يمرر Eloquent أربعة وسائط (arguments) إلى كل دالة. $model هو المثيل الذي يتم ملؤه أو حفظه، مما يتيح لك فحص الحقول الأخرى إذا كان منطقك يعتمد عليها. $key هو اسم العمود الذي يتم تحويله حاليًا. $value هي السلسلة النصية الخام أو القيمة الفارغة (null) القادمة من قاعدة البيانات عند استدعاء get ، أو القيمة التي قدمها المستخدم عند استدعاء set. $attributes هي المصفوفة الخام الكاملة للأعمدة لذلك الصف. لست بحاجة لاستخدام الوسائط الأربعة في كل مرة، ولكن الواجهة تتطلب وجودها.

في دالة get أعلاه، يتم تشغيل decrypt_data($value) بعد أن يجلب Eloquent الصف من قاعدة البيانات وقبل أن تستقر القيمة في نموذجك. وفي دالة set ، يتم تشغيل encrypt_data($value) قبل عمليات INSERT أو UPDATE، مما يضمن أن قاعدة البيانات لن ترى أبدًا النص الصريح (plaintext).

لربطها، قم بالإشارة إلى الفئة داخل مصفوفة $casts في النموذج الخاص بك:

protected $casts = [
    'username' => CustomEncrypt::class,
    'password' => CustomEncrypt::class,
];

بما أنك تستخدم ثابت الفئة (class constant)، فإن المحمل التلقائي (autoloader) يتولى الباقي. إذا احتاج تطبيقك لاحقًا إلى تغيير نظام التشفير، فستقوم بتحرير ملف واحد فقط، وسيتغير سلوك كل حقل مرتبط فورًا. هذه المركزية يصعب التغلب عليها عندما تدير بيانات حساسة عبر جداول كثيرة.

الطريقة 2: استخدام الـ Accessor والـ Mutator

أحيانًا لا ترغب في إنشاء ملف جديد لتحويل يهمك في مكان واحد فقط. تتيح لك فئة Attribute في Laravel تعريف منطق الـ get والـ set مباشرة على النموذج باستخدام صيغة closure الخاصة بـ PHP 8+.

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function username(): Attribute
{
    return Attribute::make(
        get: fn ($value) => decrypt_data($value),
        set: fn ($value) => encrypt_data($value),
    );
}

هنا يجب أن يتطابق اسم الدالة مع العمود أو الخاصية التي تستهدفها. عندما يقرأ Eloquent قيمة username من قاعدة البيانات، فإنه يمرر القيمة الخام عبر الـ closure الخاص بـ get. وعندما تخصص قيمة جديدة لخاصية username في النموذج، يقوم الـ closure الخاص بـ set بتشفيرها قبل أن يقوم Eloquent ببناء الاستعلام.

يبرز هذا النهج في النماذج الأولية (prototypes) أو النماذج القديمة (legacy models) حيث يبدو إنشاء دليل كامل من فئات التحويل أمرًا مبالغًا فيه. العيب هو التكرار؛ فإذا قررت لاحقًا أن email و phone و backup_code تحتاج إلى نفس المعاملة، فسينتهي بك الأمر بنسخ تلك الـ closures عبر دوال أو نماذج متعددة. هذا الضجيج يتراكم، كما أنه يمنعك من إعادة استخدام المنطق في اختبار الوحدة (unit test) دون تشغيل النموذج بالكامل.

المفاضلة بين الخيارين

استخدم فئة cast مخصصة (custom cast class) عندما تهتم بإعادة الاستخدام، والاختبار، والحفاظ على النماذج (models) خفيفة. فهذا يعطي إشارة للمطورين الآخرين بأن هذا التحويل هو مفهوم أساسي في تطبيقك، وليس مجرد حل مؤقت (hack).

استخدم accessor عندما يكون المنطق (logic) محلياً تماماً، أو تجريبياً، أو من غير المرجح أن يستمر لما بعد الـ sprint الحالي. يتيح لك ذلك التحرك بسرعة دون تشتيت ملفات صغيرة عبر قاعدة الكود (codebase). فقط كن مستعداً لإعادة الهيكلة (refactor) إلى فئة cast بمجرد ظهور النمط نفسه للمرة الثانية أو الثالثة.

تفاصيل عملية يجب وضعها في الاعتبار

الـ custom casts قوية، لكنها تقدم سلوكاً قد يفاجئك إذا لم تكن منتبهاً. أولاً، تذكر أن get تستقبل أي شيء تعيده قاعدة البيانات، بما في ذلك null. إذا كانت decrypt_data لا تقبل مدخلات null ، فقم بالحماية ضد ذلك:

public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
    return is_null($value) ? null : decrypt_data($value);
}

ثانياً، تعمل الـ casts أثناء عملية الـ serialization للمصفوفات (arrays) و JSON. عندما تعيد model كـ API resource أو تستدعي toArray()، فإن منطق الـ get الخاص بالـ cast سيظل مطبقاً. هذا هو ما تريده عادةً، ولكن من الجدير بالذكر إذا كنت تعرض قيمًا مفكوكة التشفير وتحتاج إلى إضافة طبقات تحكم إضافية في الرؤية (visibility controls) فوقها.

ثالثاً، وهو الأهم، يحدث الـ casting بعد عملية الاسترجاع. لا يمكنك إجراء استعلام (query) بناءً على القيمة المحولة. استعلام مثل User::where('username', 'john_doe')->first() يرسل السلسلة النصية john_doe مباشرة إلى قاعدة البيانات، ولا يمر أبداً عبر منطق فك التشفير الخاص بك. إذا كان العمود مشفراً في حالة السكون (at rest)، فلن يجد هذا الاستعلام شيئاً ما لم تبحث في النص المشفر (ciphertext) نفسه. خطط لأنماط الوصول إلى قاعدة البيانات الخاصة بك بناءً على ذلك، لأن الـ casts ليست بديلاً عن وظائف قاعدة البيانات الأصلية (native database functions) أو أعمدة النصوص الصريحة المفهرسة (indexed plaintext columns).

كما تعد فئات الـ custom cast مكاناً رائعاً لقطع صغيرة من الإعدادات (configuration). إذا كنت بحاجة إلى وسائط للمنشئ (constructor arguments) — ربما تمرير وضع التشفير (cipher mode) أو سلسلة تنسيق (format string) — فإن Laravel يدعم ذلك من خلال مصفوفة $casts باستخدام تعبير مثل 'field' => CustomEncrypt::class . ':arg'، رغم أن ذلك يعد خطوة تتجاوز الإعداد الأساسي الموصوف هنا.

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

إن القيد الذي يمنع وضع دوال مساعدة (helper functions) مباشرة في $casts ليس مجرد بيروقراطية عشوائية، بل هو توجيه نحو كود صريح، وقابل للاختبار، وقابل لإعادة الاستخدام. تحول فئات الـ custom cast المنطق المشتت المضمن (inline logic) إلى مكونات موثوقة يمكنك مشاركتها عبر الـ models دون تكرار. أما الـ accessors فتترك الباب مفتوحاً للإصلاحات السريعة والمحلية عندما تشعر أن إنشاء ملف منفصل هو مجرد إجراءات شكلية (ceremony). أتقن كليهما، واختر بناءً على نطاق المشكلة، وستظل طبقة Eloquent الخاصة بك قابلة للقراءة لفترة طويلة بعد نمو التطبيق.