Menu

חריגות ב-PHP: throw ומחלקות חריגה מותאמות

זורקים חריגה ב-PHP עם throw new Exception('message');, והקורא קורא אותה עם $e->getMessage() בבלוק catch. למדו את מחלקות החריגה המובנות, כתיבת חריגות מותאמות, getCode(), getLine() ו-getPrevious(), throw כביטוי, והשגיאות ש-PHP זורקת בעצמה כמו ValueError ו-TypeError.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

זורקים חריגה ב-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() עליה מחזירה את הסיבה המקורית, כך ששום פרט לא הולך לאיבוד.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל