Rule Builders في Laravel 13.36: قواعد التحقق بدون سلاسل نصية

أضاف Laravel 13.36 دوال Rule:: لكل قاعدة تحقق كانت نصية فقط، فتحصل على autocomplete وأخطاء إملائية تُكتشف مبكرًا وحلٍّ لمشكلة الفاصلة.

إذا كتبت يومًا 'required_without:phone|email' ثم قضيت وقتًا تبحث عن سبب فشل التحقق لأنك كتبت required_withot، فهذا التحديث موجّه إليك. في هذا المقال نشرح ما الذي تغيّر بالضبط، وكيف تستخدم Rule builders في تطبيقك، وما الحالات الحدّية التي يجب أن تعرفها قبل أن تنقل كل قواعدك إلى الأسلوب الجديد.

ما الذي أُضيف في Laravel 13.36؟

ضمن إصدار Laravel 13.36، أضاف المساهم @ericmp33 في PR #61833 دوالًا ثابتة (static methods) على الكلاس Illuminate\Validation\Rule لكل قاعدة تحقق لم يكن لها دالة مقابلة. أي أن ما كنت تكتبه كنصوص مفصولة بـ | صار بإمكانك كتابته كاستدعاءات دوال.

الأسلوب القديم لا يزال يعمل، ويمكنك الخلط بين الأسلوبين في المصفوفة نفسها، فلا يوجد ما يجبرك على ترحيل كل شيء دفعة واحدة.

قبل وبعد: مقارنة سريعة

// القواعد النصية
$request->validate([
    'email' => 'required_without:phone|email',
    'phone' => 'required_without:email',
    'website' => 'nullable|url:https',
    'device_id' => 'uuid:4',
    'tags.*' => 'distinct:ignore_case',
    'password' => 'required|confirmed',
    'timezone' => 'timezone:per_country,US',
]);

// باستخدام Rule builders
use Illuminate\Validation\Rule;

$request->validate([
    'email' => [Rule::requiredWithout('phone'), 'email'],
    'phone' => [Rule::requiredWithout('email')],
    'website' => [Rule::nullable(), Rule::url()->protocols(['https'])],
    'device_id' => [Rule::uuid()->version(4)],
    'tags.*' => [Rule::distinct()->ignoreCase()],
    'password' => [Rule::required(), Rule::confirmed()],
    'timezone' => [Rule::timezone(['per_country', 'US'])],
]);

الفائدة العملية الأولى هي الـ autocomplete: المحرر يقترح أسماء القواعد وخياراتها، وأي خطأ إملائي في اسم الدالة يظهر فورًا بدل أن يتحول إلى قاعدة غير معروفة وقت التشغيل.

أنواع Rule builders وكيف تعمل

1) قواعد بلا معاملات

هذه ترجع اسم القاعدة كنص عادي، فـ Rule::required() ترجع required. ومنها accepted() وbail() وboolean() وfilled() وjson() وnullable() وpresent() وsometimes() وulid() وغيرها. وهناك استثناء بسيط: Rule::boolean(true) ترجع boolean:strict، فتقبل true وfalse فقط.

2) قواعد تأخذ حقولًا أو قيمًا

يمكنك تمرير المعاملات منفصلة أو داخل مصفوفة:

Rule::requiredWith('first_name', 'last_name');
Rule::requiredWith(['first_name', 'last_name']);

Rule::acceptedIf('plan', 'pro');
Rule::prohibits('coupon_code');
Rule::requiredArrayKeys('street', 'city', 'postcode');

تشمل هذه المجموعة عائلات required* وpresent* وmissing* وprohibited* وexclude*، إضافة إلى regex() وnotRegex() وinArray() وtimezone().

3) قواعد لها خيارات متسلسلة (chainable)

Rule::url()->protocols(['https']);              // url:https
Rule::uuid()->version(7);                       // uuid:7
Rule::ip()->version(6);                         // ipv6
Rule::distinct()->strict()->ignoreCase();       // distinct:strict,ignore_case
Rule::confirmed()->customField('repeat_pass');  // confirmed:repeat_pass
Rule::currentPassword()->guard('admin');        // current_password:admin

كما حصل الـ builder الخاص بالتواريخ على دالة equals() التي تضيف القاعدة date_equals:

Rule::date()->equals('2026-12-31');
Rule::date()->equals(today());

المشكلة الأقل شهرة: الفاصلة داخل القيمة

الصيغة النصية تقسّم المعاملات عند كل فاصلة. لذلك لو أردت أن تقبل القيمة Team, Annual كقيمة واحدة فلن تستطيع:

'terms' => 'accepted_if:plan,Team, Annual',                 // تُقرأ كقيمتين: "Team" و"Annual"
'terms' => [Rule::acceptedIf('plan', 'Team, Annual')],      // تبقى قيمة واحدة

السبب أن الـ builders التي تأخذ حقولًا أو قيمًا تطبّق عقدًا (contract) جديدًا اسمه ParameterizedRule، دالته toArray() ترجع اسم القاعدة ومعاملاتها كمصفوفة يستعملها الـ validator مباشرة دون المرور بتقسيم النص. وبحسب المصدر، يشمل ذلك أيضًا علامات الاقتباس والشرطة العمودية والشرطة المائلة العكسية وأسماء الحقول أو المفاتيح التي تحتوي فاصلة.

كيف تعمل داخليًا؟ نص أم كائن

تفرّق الدوال الجديدة بين حالتين. القواعد البسيطة بلا معاملات ترجع نصًا عاديًا، لذلك Rule::required() تعادل حرفيًا 'required'. أما القواعد التي تأخذ حقولًا أو قيمًا فترجع كائنًا يحمل المعاملات، ويسلّمها للـ validator كمصفوفة. هذا الفرق هو ما يجعل القيمة Team, Annual تبقى سليمة، لأن أحدًا لم يدمجها في نص ثم قسّمها عند الفواصل.

والهدف المعلن أن تعبّر القاعدة المبنية عن المعنى نفسه الذي تعبّر عنه صيغتها النصية، مع معاملات أكثر أمانًا؛ ومع ذلك اختبر نماذجك الحساسة بعد الترحيل.

مثال واقعي: Form Request

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class CheckoutRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'plan' => [Rule::required(), Rule::in(['starter', 'Team, Annual'])],
            'terms' => [Rule::acceptedIf('plan', 'Team, Annual')],
            'company_name' => [Rule::requiredIfAccepted('is_business')],
            'vat_number' => [Rule::nullable(), Rule::requiredWith('company_name')],
            'billing' => [Rule::required(), Rule::requiredArrayKeys('street', 'city', 'postcode')],
            'coupon_code' => [Rule::nullable(), Rule::prohibits('gift_card')],
            'website' => [Rule::nullable(), Rule::url()->protocols(['https'])],
            'emails.*' => [Rule::distinct()->ignoreCase(), 'email'],
        ];
    }
}

لاحظ أن المصفوفة تخلط بين Rule:: والنصوص مثل 'email' دون مشكلة.

ماذا أيضًا في Laravel 13.36؟

الإصدار لا يقتصر على الـ builders. من أبرز ما ورد في ملاحظات الإصدار:

  • DB::olap() (#61932): تضيف olap() وisVitess() إلى اتصال MySQL، وتبدّل إلى نمط OLAP طوال تنفيذ callback، وعلى قواعد البيانات غير Vitess تنفّذ الـ callback كما هو. ويذكر المصدر أن استعلامًا واحدًا على PlanetScale يعيد 100,000 صف كحد أقصى.
  • إعادة ضبط الـ sticky connections (#61915): بتفعيل Worker::$flushStickyConnections = true تُعاد حالة الـ sticky قبل كل job، والخيار معطّل افتراضيًا.
  • Mercure المتزامن (#61977): أحداث Mercure تُنشر فورًا افتراضيًا مثل ShouldBroadcastNow، ويمكن تفعيل الطابور بـ MERCURE_ASYNC=true.
  • الـ queued mailables (#61899): صارت تدعم #[DeleteWhenMissingModels] و#[FailOnTimeout].
  • تهريب الـ Markdown في البريد (#61755): تُهرَّب الشرطة المائلة العكسية مع الأقواس، فلا يستطيع مدخل المستخدم داخل نص الرابط تغيير وجهته.

القائمة الكاملة للتغييرات متاحة في مقارنة الإصدارين على GitHub.

تنبّهات قبل الاعتماد عليها

  • القيم غير النصية تتحول إلى النص الذي كانت الصيغة النصية ستستخدمه: true وfalse وnull تصبح 'true' و'false' و'null'. مثلًا Rule::presentIf('is_company', true) تنتج present_if:is_company,true.
  • تحويل الـ builder إلى نص (للتسجيل في اللوج مثلًا) يعطي الصيغة النصية، وتُحاط القيم التي تحتوي فاصلة أو علامة اقتباس مزدوجة بعلامات اقتباس.
  • Rule::ip()->version() تقبل 4 أو 6 فقط، وأي رقم آخر يرمي InvalidArgumentException.
  • Rule::uuid()->version() تقبل أيضًا النصين 'nil' و'max'.
  • المصدر لا يذكر مساحة الأسماء الكاملة لعقد ParameterizedRule، فإن احتجته في كود مخصص فارجع إلى الـ PR.

كيف تتبنّى الأسلوب الجديد؟

  1. حدّث إلى Laravel 13.36 أو أحدث.
  2. ابدأ بالقواعد التي تسبب لك ألمًا: التي تحتوي فواصل في قيمها، أو التي تتكرر أخطاؤها الإملائية.
  3. لا تحوّل كل شيء آليًا؛ قاعدة بسيطة مثل 'required|email' قد تكون أقصر وأوضح كنص. استخدم الـ builders حيث تضيف قيمة.
  4. اتفق مع فريقك على أسلوب واحد داخل الملف الواحد حتى لا يصعب قراءته.

الخلاصة

Rule builders لا تغيّر ما يمكن للـ validation فعله، لكنها تنقل قواعدك من نصوص هشة إلى كود يفهمه المحرر. أكبر مكسب عملي هو الـ autocomplete وكشف الأخطاء الإملائية مبكرًا، وثانيها حل مشكلة الفاصلة في القيم. جرّبها أولًا في Form Request واحد معقّد، وقارن النتيجة قبل تعميمها.

المصادر