זורקים חריגה ב-PHP עם throw new Exception('message');. הביצוע נעצר בשורה הזו וקופץ לבלוק ה-catch הקרוב ביותר שמקבל את הטיפוס של החריגה, ושם $e->getMessage() מחזירה את ההודעה. השתמשו במחלקה ספציפית, כמו InvalidArgumentException, כדי שהקוראים יוכלו להבחין בין בעיות.
חריגה היא אובייקט. throw מעבירה אותה במעלה מחסנית הקריאות לקורא הראשון עם catch מתאים, והקורא הזה מחליט מה לעשות; הפונקציה שמצאה את הבעיה לא צריכה לדעת. המכניקה של תפיסה, finally ותפיסה של כמה טיפוסים נמצאת בעמוד try catch.
מה אובייקט חריגה מכיל
כל חריגה נושאת הודעה, קוד שלם, את הקובץ והשורה שבהם היא נוצרה, ו-stack trace. הבנאי מקבל ($message, $code, $previous), כולם אופציונליים:
getLine() היא השורה של throw new, לא השורה של ה-catch. ה-trace מפרט את הקריאות שהובילו לשם (#0 /home/index.php(8): findOrder()), ובדרך כלל זה הדבר הראשון שקוראים כששגיאה מופיעה בלוג. המרה של החריגה למחרוזת (echo $e; או (string) $e) מדפיסה את כל זה בפורמט הסטנדרטי של PHP.
מחלקות חריגה מובנות
PHP מגיעה עם משפחה של מחלקות חריגה בספרייה הסטנדרטית שלה (SPL). זריקה של המחלקה שנותנת שם לבעיה הופכת את הקוד לקריא יותר ומאפשרת לקוראים לתפוס בצמצום. כולן מרחיבות את Exception:
| מחלקה | זרקו אותה כאשר |
|---|---|
InvalidArgumentException | לארגומנט יש צורה שגויה: שם ריק, אפשרות לא מוכרת |
DomainException | ערך נמצא מחוץ לקבוצה שהגיונית: מחיר שלילי |
OutOfRangeException | הקוד ביקש אינדקס שלעולם לא יכול להתקיים (באג אצל הקורא) |
OutOfBoundsException | מפתח או אינדקס חסרים בנתונים שידועים רק בזמן ריצה |
RangeException | תוצאה מחושבת נופלת מחוץ לטווח התקין בזמן ריצה |
LengthException | משהו ארוך מדי או קצר מדי |
RuntimeException | בעיה שאפשר לזהות רק בזמן ריצה: דיסק מלא, timeout |
UnexpectedValueException | פונקציה החזירה או קיבלה ערך מטיפוס שלא ציפיתם לו |
LogicException | הקוד עצמו שגוי, באג ולא קלט רע |
JsonException | נזרקת על ידי json_encode()/json_decode() עם JSON_THROW_ON_ERROR |
InvalidArgumentException, DomainException, LengthException ו-OutOfRangeException מרחיבות את LogicException; OutOfBoundsException, RangeException ו-UnexpectedValueException מרחיבות את RuntimeException. אפשר לבדוק בעצמכם את ההורים של כל מחלקה:
ל-TypeError האב הוא Error, לא Exception: היא שייכת למשפחה השנייה, בהמשך.
שגיאות ש-PHP זורקת בעצמה
מאז PHP 7, והרבה יותר מאז PHP 8, הפונקציות והאופרטורים של PHP עצמה זורקים מחלקות בת של Error לבעיות שפעם היו אזהרות. תופסים אותן באותה דרך, אבל catch (Exception $e) לא יתאים להן:
DivisionByZeroError, TypeError, ValueError, ArgumentCountError, UnhandledMatchError ו-Error הרגילה מרחיבות כולן את Error. כל אחת מהן מסמנת לעיתים קרובות יותר באג בקוד הקורא מאשר נתונים רעים, ולכן הן חיות מחוץ לעץ של Exception. declare(strict_types=1) הוא מה שהופך את str_repeat(5, 2) ל-TypeError; בלעדיו PHP הייתה ממירה את 5 ל-"5".
כתיבת מחלקת חריגה מותאמת
מחלקת בת של שורה אחת היא לעיתים קרובות כל מה שצריך: שם המחלקה עצמו נושא את המשמעות, וקוראים יכולים לתפוס בדיוק את הטיפוס הזה. כשמי שתופס צריך נתונים (איזו הזמנה, איזה סכום), הוסיפו מאפיינים והעבירו את ההודעה למעלה עם parent::__construct():
בחרו את האב בזהירות: הרחבה של RuntimeException (ולא של Exception רגילה) אומרת שקוד שתופס RuntimeException מטפל גם בשלכם. היררכיה קטנה, חריגת בסיס אחת לכל ספרייה או מודול עם מחלקות בת ספציפיות מתחתיה, מאפשרת לקוראים לבחור בין "לתפוס הכול מהתשלומים" לבין "לתפוס רק את המקרה הזה".
שרשור חריגות עם previous
כשתופסים חריגה ברמה נמוכה וזורקים חריגה ברמה גבוהה יותר, העבירו את המקורית כארגומנט השלישי. שום דבר לא הולך לאיבוד: getPrevious() חוזרת אחורה עד לסיבת השורש.
המשתמש רואה "customers.csv could not be imported"; הלוג, שעובר על השרשרת, מקבל גם "row 2 has 2 columns". אם חריגה משורשרת לא נתפסת אף פעם, השגיאה הקטלנית של PHP מדפיסה את כל השרשרת: קודם הסיבה המקורית, ואז כל עוטפת תחת שורת Next.
throw כביטוי
מאז PHP 8.0, throw היא ביטוי, ולכן היא יכולה לשבת בכל מקום שבו מצופה ערך: אחרי ??, בטרנרי, בפונקציית חץ. זה הופך את "תביא את זה או תיכשל" לשורה אחת:
מתי לא לזרוק
חריגות נועדו למצבים שהפונקציה הנוכחית לא יכולה לטפל בהם והזרימה הרגילה לא מצפה להם. חיפוש שלא מוצא כלום, שדה אופציונלי שריק, או משתמש שלא מחובר הם תוצאות רגילות: החזירו null, false או מערך ריק ותנו לקורא לבדוק, ושמרו את החריגות למקרים שבהם להמשיך יהיה שגוי.
שני הרגלים נוספים שומרים על קוד חריגות קריא: אף פעם אל תתפסו חריגה רק כדי להתעלם ממנה (catch (Exception $e) {} ריק מסתיר את הבאג האמיתי הבא), וזרקו מחלקות ספציפיות ולא Exception חשופה, כך שקורא לעולם לא ייאלץ לתפוס הכול כדי לטפל במקרה אחד.
שאלות נפוצות
איך זורקים חריגה ב-PHP?
צרו אובייקט חריגה וזרקו אותו: throw new InvalidArgumentException('Quantity must be positive');. הביצוע נעצר בשורה הזו וקופץ לבלוק ה-catch המתאים הקרוב ביותר; אם אין כזה, הסקריפט מסתיים עם שגיאה קטלנית "Uncaught".
איך יוצרים חריגה מותאמת ב-PHP?
הרחיבו את Exception או את אחת ממחלקות הבת שלה: class PaymentFailedException extends RuntimeException {}. השורה האחת הזו מספיקה כדי לזרוק ולתפוס אותה לפי הטיפוס שלה. הוסיפו מאפיינים ובנאי כשמי שתופס צריך נתונים נוספים, וקראו ל-parent::__construct($message, $code, $previous).
מה ההבדל בין getMessage ל-getCode?
getMessage() מחזירה את הטקסט שהועבר כארגומנט הראשון של הבנאי, שמיועד לבני אדם וללוגים. getCode() מחזירה את המספר השלם שהועבר כארגומנט השני (0 כברירת מחדל), שימושי לבדיקות של מכונה כמו מיפוי שגיאות לקודי סטטוס של HTTP.
איזו חריגה לזרוק ב-PHP?
השתמשו במחלקת SPL מובנית שמתאימה לבעיה: InvalidArgumentException לארגומנט לא תקין, DomainException לערך מחוץ למה שמותר, RuntimeException לכישלונות שנראים רק בזמן ריצה (דיסק מלא, timeout), LogicException לטעויות תכנות. לשגיאות שהקוראים שלכם צריכים להבחין ביניהן, צרו מחלקת בת משלכם.
מה זה שרשור חריגות ב-PHP?
העברה של החריגה המקורית כארגומנט השלישי של הבנאי כשזורקים חריגה חדשה: throw new ImportException('Import failed', 0, $e);. החריגה החדשה נותנת הקשר, ו-$e->getPrevious() עליה מחזירה את הסיבה המקורית, כך ששום פרט לא הולך לאיבוד.