ב-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)ברמה החיצונית ביותר (handler של בקשה, מריץ משימות) כדי לרשום ללוג כל כישלון, כולל השגיאות של 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() שומרת את הפרט הנמוך בשביל הלוג.
חריגות שלא נתפסו ו-handler גלובלי
חריגה שאף catch לא מטפל בה מסיימת את הסקריפט עם שגיאה קטלנית שכוללת את המחלקה, ההודעה, הקובץ והשורה, ו-stack trace:
<?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() רושמת פונקציה שמקבלת כל חריגה שאף אחד לא תפס, מקום אחד לרשום אותה ללוג ולהדפיס דף מנומס:
ה-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 רץ אם יש return בתוך try?
כן. finally רץ אחרי try ו-catch בין אם הקוד החזיר ערך, זרק או הסתיים כרגיל, כולל כש-try מכיל return. אם finally עצמו מחזיר ערך, הערך הזה מחליף את זה מ-try, אז הימנעו מ-return בתוך finally.
האם try catch תופס אזהרות ב-PHP?
לא. אזהרות והודעות (כמו קריאה של מפתח מערך חסר) אינן חריגות, ולכן catch אף פעם לא רואה אותן. בדקו את התנאי לפני הקריאה, השתמשו במצב החריגות של הפונקציה כשיש לה כזה (json_decode(..., flags: JSON_THROW_ON_ERROR)), או המירו אזהרות עם set_error_handler() ו-ErrorException.