الأخطاء والاستثناءات
كيف تفشل العملية بوضوح، ويبقى التطبيق مستقرًّا؟ ⏱ نحو 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_errors | On | Off |
log_errors | On | On |
error_reporting | E_ALL | E_ALL (تُسجَّل ولا تُعرض) |
توقّع وتحقّق
ماذا يحدث بعد throw مباشرة؟
catch مناسبًا.أين نجد رسالة الاستثناء داخل catch؟
استثناء لم يلتقطه أحد يسبب:
Uncaught exception.على الخادم الحقيقي نضبط display_errors على:
الخلاصة
throwيوقف الدالة ويصعد بالاستثناء حتى يلتقطهcatch.try{ … }catch(نوع $e) { $e->getMessage()}finally{ … }- الاستثناء غير الملتقط = خطأ قاتل و 500.
- أصناف استثناءات خاصة
(extends Exception)تترجمها كتلة واحدة إلى 422 و 404 و 409. - الإنتاج:
display_errors Offوlog_errors On، ورسالة لطيفة للمستخدم.
تحدٍّ برمجي: مستودع الكتب
متقدمالدالة 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 داخلها).
👀 اعرض الاستجابة المطلوبة
GET /library.php?id=b1GET /library.phpGET /library.php?id=b9GET /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();}