7. الاستثناءات والأخطاء
عندما تواجه إحدى طرق الفئة خطأً لا يمكن استرداده (ملف غير موجود، قاعدة بيانات غير متصلة، اتصال الشبكة معطل)، فإنها لا تعرض خطأً على وحدة التحكم (ملف، قاعدة بيانات) بل تطلق استثناءً. جميع الاستثناءات تمتد من الفئة [\Exception]. بالإضافة إلى الاستثناءات، يصدر التشغيل الداخلي لـ PHP أيضًا أخطاءً تكون فئتها الأساسية هي الفئة [\Error]. وتقوم كلتا الفئتين بتنفيذ واجهة PHP [\Throwable].
7.1. هيكل شجرة البرامج النصية

7.2. الواجهة [\Throwable]
الواجهة [\Throwable] هي كما يلي:

دور أساليب الواجهة هو كما يلي:

7.3. الاستثناءات المحددة مسبقًا في PHP 7
تحدد PHP 7 عدة فئات من الاستثناءات:

- في [1]، الاستثناءات المحددة مسبقًا في PHP؛
- في [2]، الاستثناءات الموجودة في مكتبة SPL (مكتبة PHP القياسية) الخاصة بـ PHP 7. تعد مكتبة SPL مجموعة من الفئات والواجهات المصممة لحل المشكلات التي يواجهها المطورون بشكل متكرر.
7.4. الأخطاء المحددة مسبقًا في PHP 7
تحدد PHP 7 عدة فئات من الأخطاء:

فئة [\Error] هي الفئة الأم لجميع الأخطاء المحددة مسبقًا في PHP. تسمح فئة [ErrorException] بتغليف مثيل من فئة [\Error] داخل مثيل من فئة [\Exception]. وهذا يتيح توحيد إدارة الأخطاء من خلال معالجة الاستثناءات فقط.
7.5. المثال 1
يُظهر المثال الأول [exceptions-01.php] كلاً من أخطاء PHP واستثناءً:
<?php
// عرض جميع الأخطاء
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// الرمز --------
$var=[];
// مفتاح غير معروف
print $var["abcd"];
// القسمة على الصفر
$var=7/0;
var_dump($var);
// جدول ذو حدود ثابتة
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// مؤشر خارج النطاق
$array[5]=8;
تعليقات
- السطر 4: يُطلب من PHP الإبلاغ عن جميع الأخطاء. المعلمة الثانية هي مستوى الأخطاء المطلوب:


- السطر 5: يُطلب عرض الأخطاء على وحدة التحكم؛
- السطر 9: يتم الوصول إلى عنصر غير موجود في المصفوفة [$var]؛
- السطر 11: يتم إجراء قسمة على صفر؛
- السطر 14: يتم إنشاء مثيل للفئة [SplFixedArray]. تسمح هذه الفئة بإنشاء مصفوفة ذات حدود ثابتة ومؤشرات صحيحة؛
- السطر 18: يتم الوصول إلى عنصر غير موجود في المصفوفة؛
النتائج
تعليقات
- السطر 1 من النتائج: يؤدي الوصول إلى مفتاح غير موجود في مصفوفة إلى حدوث خطأ PHP من المستوى [E_NOTICE]. ولا يؤدي ذلك إلى إيقاف تنفيذ البرنامج النصي؛
- السطر 3 من النتائج: يؤدي قسمة عدد على الصفر إلى حدوث خطأ PHP من المستوى [E_WARNING]. ولا يؤدي ذلك إلى إيقاف تنفيذ البرنامج النصي؛
- الأسطر 6-9 من النتائج: يؤدي الوصول إلى مؤشر غير موجود في مصفوفة [SplFixedArray] إلى حدوث استثناء من النوع [RuntimeException] ويؤدي إلى توقف تنفيذ البرنامج النصي؛
7.6. معالجة الاستثناءات
يوضح البرنامج النصي [exceptions-02.php] كيفية إدارة الاستثناءات:
<?php
// عرض جميع الأخطاء
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// تحيط الكود بـ try / catch
try {
$var = [];
// مفتاح غير معروف
print $var["abcd"];
// القسمة على الصفر
$var = 7 / 0;
var_dump($var);
// مصفوفة ذات حدود ثابتة
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// مؤشر خارج النطاق
$array[5] = 8;
// التحقق
print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
// \Throwable هي الواجهة التي تنفذها معظم الأخطاء والاستثناءات
// يتم عرض الاستثناء
print "erreur, message : " . $ex->getMessage() . ", type : " . get_class($ex) . "\n";
}
تعليقات
- هذا البرنامج النصي هو نفسه الذي تم عرضه في الفقرة السابقة. إلا أنه تم الآن إحاطة الكود الموجود في الأسطر 8-19، والذي قد يتسبب في حدوث أخطاء، ببنية try / catch: إذا تسبب الكود الموجود في الأسطر 8-21 في حدوث (إطلاق) استثناء أو خطأ، فسيتم معالجته بواسطة جملة catch الموجودة في الأسطر 22-26؛
- السطر 22: المعلمة في جملة [catch] هي نوع الاستثناء أو الخطأ الذي نريد معالجته. عند تعيين النوع [\Throwable]، وهو واجهة، فإننا نشير إلى أننا نريد معالجة أي مثيل لفئة تنفذ واجهة [\Throwable]. ونظرًا لأن جميع فئات الأخطاء والاستثناءات تُنفِّذ هذه الواجهة، فإن الجملة [catch] تُعالج هنا أي خطأ/استثناء مُغلف في فئة؛
- السطر 19: الأمر الذي يثير الخطأ ويؤدي إلى ظهور الاستثناء. بمجرد حدوث استثناء، يتم التحويل إلى الجملة [catch]. وبالتالي، لن يتم تنفيذ الكود الموجود بعد السطر 19؛
النتائج
تعليقات على النتائج
- السطران 1 و 3: نجد أخطاء المستوى [E_NOTICE] و [E_WARNING]. هذه الأخطاء ليست استثناءات، وبالتالي لا تديرها الجملة [catch]؛
- السطر 5: تظهر رسالة الخطأ المكتوبة في البند [catch]. وبالتالي، فقد حدث استثناء مشتق من [\Exception] أو خطأ مشتق من [\Error]. ونلاحظ هنا أن الأمر يتعلق بالفئة [\RuntimeException]؛
7.7. معلمات البند [catch]
دعونا نلقي نظرة على البرنامج النصي [exceptions-03.php] التالي:
<?php
// يتم عرض جميع الأخطاء
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// مصفوفة ذات حدود ثابتة
$array = new \SplFixedArray(5);
try {
// مؤشر خارج النطاق
$array[5] = 8;
} catch (\Throwable $ex) {
// عرض رسالة خطأ
print "Erreur 1 : " . $ex->getMessage() . "\n";
}
try {
// مؤشر خارج النطاق
$array[5] = 8;
} catch (\Exception $ex) {
// عرض رسالة خطأ
print "Erreur 2 : " . $ex->getMessage() . "\n";
}
try {
// مؤشر خارج النطاق
$array[5] = 8;
} catch (\RuntimeException $ex) {
// عرض رسالة خطأ
print "Erreur 3 : " . $ex->getMessage() . "\n";
}
try {
// القسمة على 0
intdiv(5, 0);
} catch (\Throwable $ex) {
// عرض رسالة خطأ
print "Erreur 4 : " . $ex->getMessage() . "\n";
}
try {
// القسمة على 0
intdiv(5, 0);
} catch (\DivisionByzeroError $ex) {
// عرض رسالة خطأ
print "Erreur 5 : " . $ex->getMessage() . "\n";
}
try {
// القسمة على 0
intdiv(5, 0);
} catch (\Error $ex) {
// عرض رسالة خطأ
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
try {
// القسمة على 0
intdiv(5, 0);
} catch (\Exception $ex) {
// عرض رسالة خطأ
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
تعليقات
- الأسطر 8-31: 3 طرق مختلفة للتعامل مع الاستثناء الناتج عن استخدام مؤشر غير صحيح مع الفئة [\SplFixedArray]. لقد رأينا أن هذا الخطأ يولد استثناءً من النوع [RuntimeException]؛
- السطر 12: يعالج خطأً من النوع [\Throwable]. وهذا صحيح لأن النوع [RuntimeException] مشتق من النوع [\Exception] الذي ينفذ الواجهة [\Throwable]؛
- السطر 20: يعالج خطأً من النوع [\Exception]. وهذا صحيح لأن النوع [RuntimeException] مشتق من النوع [\Exception]؛
- السطر 28: يعالج خطأً من النوع [\RuntimeException]. هذه هي الطريقة المفضلة لأنها تمثل النوع الدقيق للاستثناء الذي تم إنشاؤه؛
- الأسطر 32-62: 4 طرق مختلفة لمعالجة الاستثناء الذي تولده الدالة [intdiv] عند تمرير مقسوم عليه يساوي 0 إليها. تقوم الدالة [ intdiv ( int $dividend , int $divisor ) : int] بإجراء القسمة الصحيحة $dividend / $divisor. وعندما يكون القاسم صفرًا، يتم إطلاق الاستثناء [\DivisionByzeroError]؛
- السطر 35: يتم اعتراض أي خطأ ينفذ واجهة [\Throwable]. وهذا صحيح؛
- السطر 43: يتم اعتراض النوع الدقيق للخطأ: هذه هي الطريقة المفضلة؛
- السطر 51: تم اكتشاف النوع [\Error]. وهو صالح لأن الفئة [DivisionByzeroError] تمتد من الفئة [Error]؛
- السطر 59: تم اعتراض النوع [\Exception]. هذا غير صالح لأن الفئة [DivisionByzeroError] لا علاقة لها بالفئة [\Exception]؛
النتائج
7.8. البند [finally]
يمكن أن تحتوي بنية try / catch على عنصر ثالث وتصبح بنية try / catch / finally. يتم تنفيذ كود الجملة [finally] في الحالتين التاليتين:
- لا تطلق الجملة [try] استثناءً. يتم عندئذٍ تنفيذها بالكامل ثم ينتقل تنفيذ الكود إلى الجملة [finally] التي يتم تنفيذها بالكامل؛
- تُطلق الجملة [try] استثناءً. وعندئذٍ يتم تنفيذها حتى الوصول إلى التعليمات التي تطلق الاستثناء. ثم ينتقل تنفيذ الكود إلى الجملة [catch] التي يتم تنفيذها بالكامل. ثم ينتقل تنفيذ الكود إلى الجملة [finally] التي يتم تنفيذها بالكامل؛
وأخيرًا، يتم تنفيذ كود الجملة [finally] دائمًا. هذا السيناريو مفيد في الحالة التالية:
- في الجملة [try]، حصل الكود على موارد (ملفات، قواعد بيانات، اتصالات شبكة، قوائم انتظار). وعادةً ما تستهلك هذه الموارد قدرًا كبيرًا من الذاكرة. لذا يجب إرجاعها (يُشار إليها غالبًا بـ«إغلاقها») في أقرب وقت ممكن؛
- إذا تم الحصول على الموارد في السيناريو [try]، فسيتم إرجاعها في السيناريو [finally]. وهذا يضمن لنا أنه في جميع الحالات (سواء حدث خطأ أم لا)، يتم إرجاع الموارد التي تم الحصول عليها إلى النظام؛
يوضح لنا البرنامج النصي التالي [exemples/exceptions/exceptions-04.php] كيفية عمل الجملة [finally] في حالات مختلفة:
<?php
// أو ينشئ مثيل استثناء
$e = new \Exception("Erreur…");
var_dump($e);
// الاختبار الأول
try {
print "Premier test\n";
throw $e;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// الاختبار الثاني
try {
print "Second test\n";
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// الاختبار الثالث
try {
print "Troisième test\n";
return;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
تعليقات على الكود
- السطر 4: $e هي مثيل للفئة المحددة مسبقًا [\Exception]. سنقوم بتشغيلها في أماكن مختلفة؛
- الأسطر 8-15: يتم إلقاء الاستثناء $e في [try] (السطر 10)؛
- السطر 11: يتم اعتراض الاستثناء [\Exception] وتُكتب رسالة الخطأ الخاصة به على وحدة التحكم؛
- الأسطر 13-15: تكتب الجملة [finally] رسالة. وبناءً على ما سبق، ينبغي أن تُكتب هذه الرسالة دائمًا، سواء حدث خطأ في [try] أم لا؛
- الأسطر 18-24: لا يوجد خطأ في [try]. وينبغي هنا أيضًا الانتقال إلى [finally]؛
- الأسطر 27-34: توجد تعليمات [return] في try ولا يوجد خطأ. يمكننا أن نتساءل إذن عما إذا كنا سننتقل إلى الجملة [finally]. يظهر التنفيذ أن الإجابة هي نعم؛
النتائج
لننظر إلى حالة أخرى [exceptions-05.php]:
<?php
// الاختبار الرابع
try {
print "Quatrième test\n";
exit;
} finally {
print "Terminé\n";
}
تعليقات
- السطر 6: الأمر [exit] يوقف تنفيذ البرنامج النصي فورًا: لا يتم تنفيذ الجملة [finally]؛
- الأسطر 4-9: مثال على try / catch / finally بدون الجملة [catch]. هذا ممكن؛
النتائج
7.9. إنشاء فئات الاستثناءات الخاصة
في أي مشروع ذي حجم معقول، من المفيد التمييز بين الأخطاء المختلفة عن طريق تجميعها في فئات استثناءات مختلفة. في البرنامج النصي السابق، رأينا أن أي استثناء يمكن اعتراضه بواسطة جملة [catch (\Throwable]. يُنصح بذلك إذا لم تكن لدينا أي فكرة عن الاستثناء الذي تم اعتراضه وكانت معالجته هي نفسها لجميع الأخطاء. قد يكون هذا هو الحال أحيانًا، لكننا غالبًا ما نحتاج إلى تكييف المعالجة وفقًا لنوع الخطأ المحدد. لذا، يجب التمييز بين الأخطاء المختلفة.
لنلقِ نظرة على البرنامج النصي [exceptions-06.php] التالي:
<?php
// نقوم بتعريف مجموعة الاستثناءات الخاصة بنا
class Exception1 extends \RuntimeException {
}
class Exception2 extends \RuntimeException {
}
// أو نستخدم استثناءاتنا
$e1 = new Exception1("Erreur1…");
var_dump($e1);
$e2 = new Exception2("Erreur2…");
var_dump($e2);
// الاختبار الأول
print ("premier test\n");
try {
// نقوم بإطلاق استثناء من النوع Exception1
throw $e1;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// الاختبار الثاني
print ("second test\n");
try {
// نقوم بإطلاق استثناء من النوع Exception2
throw $e2;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// الاختبار الثالث
print ("troisième test\n");
try {
// يتم إلقاء استثناء من النوع Exception1
throw $e1;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
// الاختبار الرابع
print ("quatrième test\n");
try {
// يتم إلقاء استثناء من النوع Exception2
throw $e2;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
تعليقات
- الأسطر 4-10: يتم تعريف فئتين [Exception1] و [Exception2] وكلاهما مشتقتان من الفئة المحددة مسبقًا [\RuntimeException]. نص هاتين الفئتين فارغ. بعبارة أخرى، لا يتم استخدامهما إلا من أجل أنواعهما: ولأنهما من أنواع مختلفة، فسيكون بإمكاننا التمييز بين هذين الاستثناءين في البنود [catch]؛
- الأسطر 13-16: نحدد متغيرين $e1 و $e2 اللذين لهما على التوالي الأنواع [Exception1] و [Exception2]؛
- الأسطر 20-29: لدينا بنية try / catch / catch. وهذا يسمح بإدارة أنواع مختلفة من الاستثناءات باستخدام بنود مختلفة [catch]؛
- السطر 23: يعترض الاستثناءات من النوع [Exception1]؛
- السطر 26: يعترض الاستثناءات من النوع [Exception2]؛
- السطر 49: يعترض الاستثناءات من النوع [Exception1] أو (|) [Exception2]؛
النتائج
تعليقات على النتائج
- الأسطر 1-17: «محتوى» الاستثناء:
- الأسطر 2-3: رسالة الخطأ؛
- الأسطر 6-7: رمز الخطأ؛
- الأسطر 8-9: اسم الملف الذي حدثت فيه الاستثناء؛
- السطور 10-11: السطر الذي حدثت فيه الاستثناء؛
- السطران 15-16: الاستثناء السابق. يمكن أن يحتوي استثناء ما على استثناء آخر، وبذلك يشكل مكدسًا من الاستثناءات. سيسمح السمة [previous] باستغلال هذا المكدس؛
7.10. إعادة إطلاق استثناء
يمكن إطلاق استثناء عدة مرات كما يوضح البرنامج النصي [exceptions-07.php] التالي:
<?php
try {
try {
// يتم إلقاء استثناء
throw new \Exception("test");
} catch (\Exception $ex) {
// إعادة إطلاق الاستثناء الذي تم اعتراضه
throw $ex;
} finally {
// سننتقل إلى finally
print "finally 1\n";
}
} catch (\Exception $ex2) {
// يتم استرداد الاستثناء الأولي بنجاح
print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
// سننتقل بالفعل إلى جملة finally
print "finally 2\n";
}
تعليقات
- السطر 6: يتم إطلاق استثناء؛
- السطر 7: يتم اعتراضها؛
- السطر 9: يتم إعادة إطلاقها. ثم تنتقل إلى بنية try / catch / finally في المستوى الأعلى؛
- السطر 14: يتم اعتراضها مرة أخرى؛
- الأسطر 10-12: يُظهر التنفيذ أنه حتى بعد [throw] في السطر 9، يتم الانتقال بالفعل إلى جملة [finally] في بنية try / catch / finally؛
النتائج
7.11. استغلال مكدس الاستثناءات
يمكن أن تحتوي الاستثناءات على استثناءات أخرى، والتي بدورها قد تحتوي على استثناءات أخرى، مما يؤدي في النهاية إلى تكوين مكدس من الاستثناءات. فيما يلي مثال على ذلك:
تعليقات
- الأسطر 4-14: تُعرّف ثلاث فئات من الاستثناءات المشتقة من الاستثناء المُعرَّف مسبقًا [RuntimeException]؛
- السطر 17: يتم تغليف مثيل للفئة [Exception3] في مثيل للفئة [Exception2] التي يتم تغليفها بدورها في مثيل للفئة [Exception1]. المنشئ المستخدم هنا هو منشئ الفئة [Exception]:
![]()
يسمح المعلمة الثالثة للمنشئ بتغليف استثناء. وقد يكون ذلك مفيدًا في السيناريو التالي:
- نُعرِّف طريقة M يمكنها توليد استثناء من النوع [Exception1]، وهذا النوع فقط لأسباب تتعلق بالتوافق، على سبيل المثال مع واجهة؛
- أو قد تحدث أنواع أخرى من الاستثناءات في الطريقة M. ولإرسال خطأ إلى الكود الذي يستدعي الطريقة M، سنقوم بتغليف هذه الاستثناءات في النوع [Exception1] الذي سنقوم بإطلاقه. وهذا يتيح عدم فقدان المعلومات الواردة في الاستثناء المغلف والتي كانت السبب الأصلي للخطأ؛
- توضح الأسطر 20-27 كيفية إدارة مكدس الاستثناءات الداخلية لاستثناء ما؛
النتائج
object(Exception1)#1 (7) {
["message":protected]=>
string(11) "Erreur 1…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(1)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception2)#2 (7) {
["message":protected]=>
string(11) "Erreur 2…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(2)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception3)#3 (7) {
["message":protected]=>
string(11) "Erreur 3…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(0)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
NULL
}
}
}
Erreur 1…
Erreur 2…
Erreur 3…