في PHP تضع الشيفرة التي قد تفشل داخل try { ... } وتعالج الفشل في catch (Exception $e) { ... }. إذا رمى أي شيء في كتلة try، تقفز PHP مباشرة إلى catch المطابقة، وتخبرك $e->getMessage() بما حدث. وإذا لم يُرمَ شيء، تُتخطى كتلة catch.
غيّر divide(10, 0) إلى divide(10, 5) وشغّله مرة أخرى: لا استثناء، فيُطبع not printed وتُتخطى كتلة catch. ودون try، سينهي الاستثناء السكربت بخطأ فادح "Uncaught". أما كيف ترمي استثناءاتك الخاصة وتكتب أصناف الاستثناءات فموجود في صفحة الاستثناءات.
صيغة try catch
للصيغة الكاملة ثلاثة أجزاء. لا يلزم إلا try، مع catch واحدة على الأقل أو finally:
try {
// code that may throw
} catch (SomeException $e) {
// runs if SomeException (or a subclass) was thrown
} catch (OtherException | ThirdException $e) {
// runs for either of these types
} finally {
// always runs, thrown or not
}
يحدد النوع في catch ما تلتقطه: الصنف نفسه وكل صنف يرثه. ومنذ PHP 8.0 صار المتغير اختياريًا حين لا تحتاج إليه: catch (JsonException) { ... }.
التقاط Exception مقابل Error مقابل Throwable
منذ PHP 7، الأعطال التي تكتشفها PHP نفسها (استدعاء تابع مفقود، نوع وسيط خاطئ، القسمة على صفر) ترمي Error لا Exception، وحوّلت PHP 8 كثيرًا من التحذيرات السابقة إلى TypeError وValueError. كلاهما ينفّذ الواجهة Throwable، لكن أيًّا منهما لا يرث الآخر، لذا لا تلتقط catch (Exception $e) الخطأ Error:
Throwable
├── Exception (problems a program expects and handles)
│ ├── InvalidArgumentException, RuntimeException, JsonException, ...
└── Error (engine errors, usually a bug in the code)
├── TypeError
├── ValueError
├── ArithmeticError
│ └── DivisionByZeroError
└── UnhandledMatchError, ...
شغّل هذا لترى أي catch تعالج كل مشكلة:
فأيها تلتقط؟
catch (SomeSpecificException $e)عندما تعرف ما قد يخطئ وما تفعله حياله. وهذا معظم الشيفرة.catch (Exception $e)لـ «أي شيء رمته مكتبة أو شيفرتي عمدًا».catch (Throwable $e)في المستوى الخارجي (معالج طلبات، مشغّل مهام) لتسجيل كل عطل، بما فيه أخطاء PHP نفسها، وعرض رسالة لطيفة.
التقاط عدة أنواع من الاستثناءات
اكتب عدة كتل catch لمعالجة الأنواع المختلفة بطرق مختلفة. تجرّبها PHP من الأعلى إلى الأسفل وتستخدم أول تطابق، لذا يجب أن يأتي النوع المحدد قبل أبيه، وإلا التقط الأب كل شيء أولًا. وعندما تحظى عدة أنواع بالمعالجة نفسها، اجمعها في catch واحدة بـ |:
الأصناف الثلاثة كلها ترث Exception، لذا ستلتقطها catch (Exception $e) واحدة أيضًا، لكن الشيفرة عندها لن تستطيع الإجابة بـ 404 لأحدها و400 للآخرين.
finally: شيفرة تعمل دائمًا
تعمل كتلة finally بعد try وcatch أيًّا كانت طريقة انتهائهما: طبيعيًا، أو باستثناء ملتقط، أو باستثناء لم يلتقطه أحد، أو بـ return. وهي المكان المناسب لتحرير ما حجزته كتلة try: إغلاق ملف، أو تحرير قفل، أو إيقاف مؤقت.
مع good.txt يُغلق الملف قبل طباعة saved، لأن finally تعمل قبل أن تعود الدالة فعلًا. ومع bad.txt لا توجد catch داخل process(): تغلق finally الملف، ثم يواصل الاستثناء صعودًا إلى catch عند المستدعي.
تجنّب return داخل finally: فهي تستبدل القيمة التي أعادتها كتلة try وتتخلص بصمت من استثناء كان في طريقه إلى الخارج:
function f(): string
{
try {
return 'from try';
} finally {
return 'from finally';
}
}
function g(): string
{
try {
throw new RuntimeException('lost');
} finally {
return 'no exception reaches the caller';
}
}
echo f(); // from finally
echo g(); // no exception reaches the caller
إعادة رمي استثناء أو تغليفه
أحيانًا لا تستطيع كتلة catch معالجة المشكلة إلا جزئيًا: تسجيلها، والتنظيف، ثم تركها تكمل. تعيد throw $e; رمي الاستثناء نفسه. وكثيرًا ما تريد إضافة سياق مع الاحتفاظ بالسبب الأصلي، وهذا هو غرض وسيط الباني الثالث، $previous:
يتعامل المستدعي مع ConfigException واحد ولا يحتاج إلى معرفة أن الإعدادات بصيغة JSON، بينما تحتفظ getPrevious() بالتفاصيل منخفضة المستوى للسجل.
الاستثناءات غير الملتقطة والمعالج العام
الاستثناء الذي لا تعالجه أي catch ينهي السكربت بخطأ فادح يتضمن الصنف، والرسالة، والملف والسطر، ومسار الاستدعاءات:
<?php
// No try/catch anywhere in this file
function charge(int $cents): void
{
if ($cents <= 0) {
throw new DomainException("Amount must be positive, got $cents");
}
}
charge(500);
echo "first charge ok\n";
charge(-1);
echo "never printed\n";
تطبع PHP first charge ok ثم:
PHP Fatal error: Uncaught DomainException: Amount must be positive, got -1 in /home/index.php:6
Stack trace:
#0 /home/index.php(12): charge()
#1 {main}
thrown in /home/index.php on line 6
على موقع حي يجب ألا تصل تلك الرسالة إلى الزوار أبدًا. تسجّل set_exception_handler() دالة تتلقى أي استثناء لم يلتقطه أحد، مكانًا واحدًا لتسجيله وطباعة صفحة مهذبة:
يعمل المعالج ثم يتوقف السكربت؛ لا سبيل للاستئناف بعد throw. وما إذا كان نص الخطأ الخاص بـ PHP يظهر على الصفحة أصلًا يتحكم فيه display_errors، المشروح في الإبلاغ عن الأخطاء.
التحذيرات ليست استثناءات
لا ترى try/catch إلا ما يُرمى. وكثير من دوال PHP الأقدم لا ترمي: تعيد false أو null وتطبع على الأكثر تحذيرًا. وjson_decode() حالة نموذجية، ووضعها داخل try لا يغيّر شيئًا حتى تطلب منها أن ترمي:
للدوال التي لا علم لها من هذا النوع، افحص القيمة المعادة (if ($handle === false))، أو افحص الشرط المسبق أولًا (file_exists() وisset()). ويحوّل set_error_handler() الذي يرمي ErrorException كل تحذير إلى استثناء حقيقي إذا أردت قاعدة واحدة لكل شيء.
وصفة: التحقق من مُدخلات النموذج بالاستثناءات
نمط عملي: ترمي دوال التحقق، وتلتقط الصفحة وتعرض الرسالة بجانب النموذج. شغّله، ثم اكتب عمرًا في النموذج واضغط Send: يعمل السكربت نفسه مرة أخرى مع امتلاء $_POST.
جرّب حقلًا فارغًا، وtwelve، و9، و30. كل قاعدة فاشلة ترمي برسالتها الخاصة، وتحوّلها catch واحدة كلها إلى السطر الأحمر نفسه. وتبني صفحة النماذج على هذا مع عدة حقول وأخطاء بجانب كل منها.
الأسئلة الشائعة
كيف تعمل try catch في PHP؟
تنفّذ PHP الشيفرة داخل try { }. وإذا رمى شيء فيها، تتخطى PHP بقية كتلة try وتنفّذ أول كتلة catch يطابق نوعها، مع الكائن المرمي في المتغير: catch (Exception $e) { echo $e->getMessage(); }. وإذا لم يُرمَ شيء، تُتخطى كتل catch.
كيف ألتقط كل الاستثناءات والأخطاء في PHP؟
التقط Throwable: catch (Throwable $e). لا تلتقط catch (Exception $e) إلا الاستثناءات، لا أخطاء المحرك مثل TypeError أو ValueError أو DivisionByZeroError، التي ترث Error. وكلٌّ من Exception وError ينفّذ Throwable.
كيف ألتقط عدة استثناءات في كتلة catch واحدة في PHP؟
افصل الأنواع بخط عمودي: catch (InvalidArgumentException | RangeException $e). ويمكنك أيضًا كتابة عدة كتل catch واحدة تلو الأخرى؛ تستخدم PHP أول واحدة تطابق، لذا ضع الأنواع الأكثر تحديدًا أولًا.
هل تعمل finally إذا كان في try جملة return؟
نعم. تعمل finally بعد try وcatch سواء أعادت الشيفرة قيمة، أو رمت، أو انتهت طبيعيًا، بما في ذلك حين تحتوي try على return. وإذا أعادت finally نفسها قيمة، تستبدل تلك القيمة القيمة القادمة من try، لذا تجنّب return داخل finally.
هل تلتقط try catch التحذيرات في PHP؟
لا. التحذيرات والإشعارات (مثل قراءة مفتاح مصفوفة مفقود) ليست استثناءات، لذا لا تراها catch أبدًا. افحص الشرط قبل الاستدعاء، أو استخدم وضع الاستثناءات في الدالة حيث يوجد (json_decode(..., flags: JSON_THROW_ON_ERROR))، أو حوّل التحذيرات بـ set_error_handler() وErrorException.