طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 132 · تقنيات تطوير الواجهات الخلفية
الدرس 12 · الوحدة 5: تنفيذ المنطق التجاري وقواعد التطبيق

الأخطاء والاستثناءات

كيف تفشل العملية بوضوح، ويبقى التطبيق مستقرًّا؟ ⏱ نحو 35 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح فكرة الاستثناء ولماذا تفضل على الطباعة داخل الدوال.
  • ترمي الاستثناءات وتلتقطها بـ try و catch و finally.
  • تعرّف أصناف استثناءات خاصة وتحوّلها إلى رموز HTTP في مكان واحد.
  • تضبط عرض الأخطاء وتسجيلها بين بيئة التطوير والإنتاج.

١حين تفشل العملية

في الدرس السابق أوقفنا التنفيذ بـ exit عند مخالفة القاعدة. لكن ماذا لو كانت القاعدة داخل دالة تستدعيها دالة أخرى؟ الدالة العميقة لا تعرف هل نحن في صفحة HTML أم في API، ولا يصح أن تطبع أو تقرر رمز الحالة. الحل: أن ترمي استثناءً Exception يقول «فشلت ولهذا السبب»، ويقرر من استدعاها كيف يرد.

<?phpfunction withdraw(float $balance, float $amount): float{    if ($amount <= 0) {        throw new InvalidArgumentException('المبلغ يجب أن يكون موجبًا');    }    if ($amount > $balance) {        throw new RuntimeException('الرصيد غير كافٍ');    }    return $balance - $amount;} try {    echo withdraw(100, 30) . "\n";    echo withdraw(100, 500) . "\n";     // يرمي استثناءً    echo "لن يُطبع هذا السطر\n";} catch (RuntimeException $e) {    echo "فشلت العملية: " . $e->getMessage() . "\n";}echo "التطبيق مستمر";
الكلمةمعناها
throw new X("رسالة")ارمِ استثناءً: توقف الدالة فورًا ويصعد الاستثناء لمن استدعاها.
try { … }جرّب هذا الكود…
catch (X $e) { … }…وإن رُمي استثناء من النوع X فنفّذ هذا. ورسالته في $e->getMessage().
finally { … }يُنفَّذ في الحالتين، نجاحًا أو فشلًا (مثل إغلاق ملف).

٢الاستثناء غير الملتقط

إن لم يلتقط أحد الاستثناء يصعد حتى القمة، فيتوقف التطبيق بخطأ قاتل ورمز 500، ويرى المستخدم رسالة تقنية لا تعنيه:

<?phpfunction findCourse(string $code): string{    $courses = ['132' => 'الواجهات الخلفية', '131' => 'الواجهات الأمامية'];    if (!isset($courses[$code])) {        throw new RuntimeException("المقرر $code غير موجود");    }    return $courses[$code];} echo findCourse('999');

وكثير من أخطاء PHP 8 نفسها استثناءات يمكنك التقاطها، مثل DivisionByZeroError و TypeError، وكلها تحت النوع العام Throwable.

٣أنواع استثناءاتك الخاصة

بدل فحص نص الرسالة، أنشئ أصنافًا للاستثناءات تعبّر عن نوع المشكلة، ثم حوّل كل نوع إلى رمز HTTP مناسب في مكان واحد:

<?phpclass ValidationError extends Exception {}class NotFound extends Exception {}class Conflict extends Exception {} function reserve(string $course, int $seats): string{    $free = ['PHP' => 4, 'SQL' => 0];    if ($seats < 1 || $seats > 3) throw new ValidationError('عدد المقاعد من 1 إلى 3');    if (!isset($free[$course]))   throw new NotFound("لا توجد دورة $course");    if ($free[$course] < $seats)  throw new Conflict("المقاعد المتاحة في $course: {$free[$course]}");    return "تم حجز $seats مقعد في $course";} try {    $msg = reserve($_GET['course'] ?? '', (int) ($_GET['seats'] ?? 0));    http_response_code(201);    echo $msg;} catch (ValidationError $e) {    http_response_code(422);    echo $e->getMessage();} catch (NotFound $e) {    http_response_code(404);    echo $e->getMessage();} catch (Conflict $e) {    http_response_code(409);    echo $e->getMessage();}

الدالة reserve لا تعرف شيئًا عن HTTP، وكتلة try/catch في الأسفل هي وحدها التي تترجم كل نوع إلى رمز. هذا الفصل يجعل الدالة صالحة لصفحة ولـ API ولسطر الأوامر.

٤ما يراه المستخدم وما يراه المطوّر

في بيئة التطوير نعرض تفاصيل الخطأ لنصلحه. أما على الخادم الحقيقي فعرض التفاصيل خطر (مسارات الملفات، وأسماء الجداول) ومنفّر للمستخدم. فنفعل شيئين: نسجّل التفاصيل في ملف للمطوّر، ونعرض رسالة لطيفة للمستخدم:

<?php// في أعلى التطبيق على الخادم الحقيقي (أو في php.ini)ini_set('display_errors', '0');      // لا تعرض الأخطاء للزوارini_set('log_errors', '1');          // سجّلهاini_set('error_log', __DIR__ . '/../logs/php-errors.log'); // شبكة أمان أخيرة لأي استثناء لم يُلتقطset_exception_handler(function (Throwable $e) {    error_log($e);                   // التفاصيل كاملة في ملف السجل    http_response_code(500);    echo 'حدث خطأ غير متوقع. حاول لاحقًا.';});
الإعدادالتطوير (XAMPP)الإنتاج (الخادم الحقيقي)
display_errorsOnOff
log_errorsOnOn
error_reportingE_ALLE_ALL (تُسجَّل ولا تُعرض)

توقّع وتحقّق

ماذا يحدث بعد throw مباشرة؟

حتى يجد catch مناسبًا.

أين نجد رسالة الاستثناء داخل catch؟

دالة في كائن الاستثناء.

استثناء لم يلتقطه أحد يسبب:

Uncaught exception.

على الخادم الحقيقي نضبط display_errors على:

نسجّل ولا نعرض.

الخلاصة

تحدٍّ برمجي: مستودع الكتب

متقدم

الدالة borrow() في library.php تطبّق قواعد استعارة الكتب. عدّلها لترمي استثناءات بدل طباعة الأخطاء، ثم حوّلها إلى ردود HTTP.

المطلوب:
  • عرّف ثلاثة أصناف استثناءات: BadInput و BookNotFound و NotAvailable (كلها ترث Exception).
  • borrow(string $id): ترمي BadInput إن كان id فارغًا، و BookNotFound إن لم يوجد الكتاب، و NotAvailable إن كانت نسخه 0، وإلا تعيد «تمت استعارة: اسم الكتاب».
  • الرد: النجاح 200 مع الرسالة. BadInput ← 422، و BookNotFound ← 404، و NotAvailable ← 409، ومع كل منها رسالة الاستثناء.
  • يجب ألا تطبع borrow() شيئًا بنفسها (لا echo داخلها).
👀 اعرض الاستجابة المطلوبة
✓ التحقق الآلي: يرسل المختبر هذه الطلبات إلى خادمك بالترتيب ثم يفحص الاستجابات
  1. GET /library.php?id=b1
  2. GET /library.php
  3. GET /library.php?id=b9
  4. GET /library.php?id=b2
  • الكود يعمل بلا أخطاء أو تحذيرات
  • b1 ← تمت استعارة: تعلم PHP
  • بلا id ← 422
  • b9 ← 404
  • b2 ← 409
  • b2 ← رسالة: لا توجد نسخ متاحة
  • الأصناف الثلاثة ترث Exception
  • تستخدم throw و try/catch
  • لا echo داخل الدالة borrow

💡 تلميح 1
class BadInput extends Exception {} يكفي لتعريف نوع استثناء.
💡 تلميح 2
ضع echo borrow(...) داخل try، وثلاث كتل catch بعدها.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

<?php$books = ['b1' => ['تعلم PHP', 2], 'b2' => ['قواعد البيانات', 0]]; class BadInput extends Exception {}class BookNotFound extends Exception {}class NotAvailable extends Exception {} function borrow(string $id): string{    global $books;    if ($id === '') throw new BadInput('رقم الكتاب مطلوب');    if (!isset($books[$id])) throw new BookNotFound('الكتاب غير موجود');    if ($books[$id][1] < 1) throw new NotAvailable('لا توجد نسخ متاحة');    return 'تمت استعارة: ' . $books[$id][0];} try {    echo borrow($_GET['id'] ?? '');} catch (BadInput $e) {    http_response_code(422);    echo $e->getMessage();} catch (BookNotFound $e) {    http_response_code(404);    echo $e->getMessage();} catch (NotAvailable $e) {    http_response_code(409);    echo $e->getMessage();}
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس