1. דף הבית
  2. קורסים אונליין
  3. קורס PHP אונליין
  4. הממשק Throwable וטיפול ב-Exceptions ב-PHP

הממשק Throwable וטיפול ב-Exceptions ב-PHP

בפרק זה, נכיר את הממשק Throwable, נלמד לטפל בשגיאות מטיפוס Error ומטיפוס Exception ונסקור את המבנה try-catch-finally. הפרק עשיר בדוגמאות מהעולם האמיתי.
Exceptions ב-PHP

הממשק Throwable

שפת PHP כוללת דרך מובנית להתמודדות עם שגיאות שעלולות להתרחש במהלך הפעלת הקוד. במקום להפסיק את ביצוע הקוד עם הודעת שגיאה, אנו יכולים לטפל בצורה נאותה בבעיות בלתי צפויות ולהמשיך בקוד ללא עצירתו.

הטיפול בשגיאות נעשה באמצעות סידרה של מחלקות (Classes) אשר בראשן עומד הממשק (Interface) שנקרא Throwable. על ממשק זה מבוססות המחלקות Exception ו-Error. נוסף על 2 מחלקות אלו, קיימות מחלקות רבות נוספות אשר יורשות מחלקות אלו. לדוגמה: המחלקה LogicException יורשת את המחלקה Exception. במילים אחרות, כל מקום בקוד שמטפל באובייקט מטיפס Exception, ידע לטפל גם באובייקט מטיפוס LogicException.

מבלי להכנס לעומק בנושא תכנות מונחה עצמים, נסביר באופן כללי כי ממשק (Interface) מגדיר התנהגות כללית של מחלקות (Classes). כלומר, אם אנו מגדירים מחלקות שונות שיש להן התנהגות דומה, ניתן להגדיר את מבנה המחלקות באמצעות ממשק (Interface) משותף. במקרה שלנו, המחלקות Execption ו-Error בעלות התנהגות דומה: שתיהן מייצגות שגיאות ושתיהן כוללות מאפיינים זהים (כגון תאור השגיאה). לכן, הוגדר הממשק Throwable ששתי מחלקות אלו מבוססות עליו.

המחלקה Exception נועדה בעיקר לשגיאות שהמשתמש מגדיר ואילו המחלקה Error נועדה בעיקר לשגיאות פנימיות של PHP, כגון שגיאות של המרת טיפוסים, בעיות חישוב וכיו"ב.

 

ניתן להציג את ההיררכיה של Throwable באופן הבא:

Throwable
│
├── Exception
│   ├── LogicException
│   ├── RuntimeException
│   ├── DomainException
│   ├── InvalidArgumentException
│   ├── LengthException
│   ├── OutOfRangeException
│   ├── OutOfBoundsException
│   ├── OverflowException
│   ├── UnderflowException
│   ├── UnexpectedValueException
│
└── Error
    ├── ArithmeticError
    ├── AssertionError
    ├── DivisionByZeroError
    ├── ParseError
    ├── TypeError

ניתן לראות כי שם כל מחלקה שיורשת את המחלקה Exception מסתיים במילה Exception, ושם כל מחלקה שיורשת את המחלקה Error מסתיים במילה Error. הדבר נקבע מטעמי נוחיות וקריאות הקוד.

זריקה ותפיסה של שגיאות באמצעות מבנה try-catch

ניתן להשתמש במבנה try-catch לתפיסת שגיאות. זריקת שגיאה בצורה מפורשת נעשית באמצעות הפקודה throw.

תחביר:

<?php
try {
    // Code that may throw an exception
} catch (Exception $e) {
    // Handle the exception
} finally {
    // (Optional) Code that runs no matter what
}
?>

את קטע הקוד שעלול לזרוק שגיאה אנו מכניסים לבלוק ה-try. אם במהלך ביצוע הקוד נזרקת שגיאה, אנו מועברים לביצוע הקוד בבלוק ה-catch. אם לא נזרקת שגיאה, בלוק ה-catch אינו מופעל כלל.

בבלוק ה-catch נוכל לתפוס Throwable או Exception או Error או כל מחלקה אחרת שיורשת אחת מ-2 המחלקות האחרונות. אם בלוק ה-catch מוגדר לתפוס Exception, הוא יתפוס לא רק אובייקטים מטיפוס Exception, אלא גם אובייקטים מטיפוס כלשהו אחר שיורשים את המחלקה Exception. באופן דומה, אם בלוק ה-catch מוגדר לתפוס Error, הוא יתפוס גם אובייקטים מטיפוסים שיורשים את המחלקה Error. אם בלוק ה-catch מוגדר לתפוס Throwable, הוא יתפוס את כל סוגי השגיאות באשר הן.

בלוק ה-finally הוא בלוק רשות וניתן להשמיטו לגמרי. אם הוא קיים, הקוד בתוכו מבוצע בכל מקרה בסיום, בין אם נזרקה שגיאה או לא. מטרת בלוק ה-finally לבצע "סגירות" של דברים שנפתחו בבלוק ה-try, כגון, סגירת קבצים, סגירת משאבים או ניתוק ממסד הנתונים.

 

דוגמה:

<?php
function divide($a, $b) {
    if ($b == 0) {
        throw new Exception("Division by zero is not allowed!");
    }
    return $a / $b;
}

try {
    echo divide(10, 0);
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>

בדוגמה זו, הגדרנו את הפונקציה divide שמקבלת 2 פרמטרים ומחזירה את ערך החלוקה של הפרמטר הראשון בפרמטר השני. אם הפרמטר השני b הוא אפס, אנו זורקים Exception. קטע הקוד try-catch מבצע קריאה לפונקציה divide עם הארגומנטים 10 ו-0. מכיוון שהארגומנט השני המועבר הוא 0, הפונקציה divide תזרוק Exception ובלוק ה-catch יופעל. בבלוק זה, אנו מדפיסים את הודעת השגיאה של ה-Exception שנזרק. למעשה, גם אם בלוק ה-try יזרוק אובייקט שיורש את המחלקה Exception ולא אובייקט מטיפוס Exception, גם אז בלוק ה-catch יתפוס אובייקט זה.

נשים לב שבדוגמה זו הגדרנו מפורשות זריקה של Exception בפונקציה divide, באמצעות הפקודה throw. לעיתים, נפעיל בבלוק ה-try פונקציות פנימיות של PHP או פונקציות של מודולים אחרים שזורקות Exception כברירת מחדל, כך שבקוד שלנו לא נראה בצורה נגלית לעין את הקוד שזורק את ה-Exception, אלא רק את הקוד שתופס אותו.

 

דוגמה:

<?php
try {
    echo "Opening file...\n";
    throw new Exception("Something went wrong!");
} catch (Exception $e) {
    echo "Caught exception: " . $e->getMessage() . "\n";
} finally {
    echo "Closing file...\n";
}

// Output:
// Opening file...
// Caught exception: Something went wrong!
// Closing file...
?>

בדוגמה זו אנו מדגימים שימוש ב-finally. בבלוק ה-try אנו פותחים קובץ (בכאילו...) ומבצעים עליו פעולות שונות. הפעולה נכשלת וזורקת Exception, ואנו מועברים לבלוק ה-catch לטיפול ב-Exception. בסיום התהליך, אנו מבצעים את בלוק ה-finally ושם מבצעים יציאה מסודרת שכוללת את סגירת הקובץ. באופן כללי, כאמור, בלוק ה-finally מבוצע בכל מקרה, גם אם נזרקת וגם אם לא נזרקת שגיאה בבלוק ה-try.

 

דוגמה:

<?php
function setAge($age) {
    if ($age < 0) {
        throw new InvalidArgumentException("Age cannot be negative.");
    }
    return "Age is set to $age.";
}

try {
    echo setAge(-5);
} catch (InvalidArgumentException $e) {
    echo "Caught Exception: " . $e->getMessage();
}
?>

בדוגמה זו, הגדרנו את הפונקציה setAge שמקבלת את הפרמטר age. אם ערך הפרמטר קטן מ-0, נזרק אובייקט מטיפוס InvalidArgumentException שהוא אובייקט ממחלקה שיורשת את Exception (ניתן גם להבין זאת בקלות לפי שם המחלקה InvalidArgumentException שמסתיים במילה Exception). אנו קוראים לפונקציה setAge עם הערך 5- (שהוא כמובן ערך קטן מ-0), ולכן, נזרק אובייקט מטיפוס InvalidArgumentException שנתפס בבלוק ה-catch. נשים לב שאם תהיה שגיאה כלשהי אחרת בפונקציה setAge שאינה מטיפוס InvalidArgumentException היא לא תיתפס בבלוק ה-catch.

 

דוגמה:

<?php
function add(int $a, int $b) {
    return $a + $b;
}

try {
    echo add(10, "hello"); // Invalid argument type
} catch (TypeError $e) {
    echo "Caught TypeError: " . $e->getMessage();
}
?>

בדוגמה זו, הגדרנו את הפונקציה add שסוכמת 2 מספרים מטיפוס int. אנו קוראים לפונקציה עם 2 ארגומנטים, כאשר השני בהם הוא מטיפוס string ולא int, ולכן, נזרק אובייקט מטיפוס TypeError, אשר מטופל בבלוק ה-catch.

 

הסיבות לשימוש בבלוק try-catch-finally:

  • מניעת סיום פתאומי של הקוד.
  • יכולת איתור באגים ורישום שגיאות טוב יותר.
  • הפיכת הטיפול בשגיאות למודולרי ומובנה יותר.
  • הפרדה לוגית בין הקוד הביצועי לבין הקוד המטפל בשגיאות.

זריקה חוזרת של שגיאה שנתפסה

כאשר אנו תופסים שגיאה (מטיפוס Exception או Error) בבלוק ה-catch, אנו יכולים לזרוק אותה או כל כל שגיאה אחרת מחדש.

דוגמה:

<?php
function process() {
    try {
        throw new Exception("Processing error!");
    } catch (Exception $e) {
        echo "Logging error: " . $e->getMessage() . "\n";
        throw $e; // Re-throwing the exception
    }
}

try {
    process();
} catch (Exception $e) {
    echo "Caught exception in main block: " . $e->getMessage();
}
?>

בדוגמה זו, הפונקציה process מבצעת עיבוד כלשהו (בכאילו...) בבלוק ה-try שבתוכה. הביצוע נכשל ונזרק Exception אשר נתפס בבלוק ה-catch. כאן אנו מדפיסים את הודעות השגיאה של ה-Exception וזורקים אותו שוב (הוא נזרק החוצה מהפונקציה). אנו קוראים לפונקציה process מתוך בלוק try, ואז גם מודפסת השגיאה שנזרקת בבלוק ה-try של הפונקציה, וגם נזרק ה-Exception בשנית החוצה מהפונקציה ונתפס בבלוק ה-catch החיצוני.

הגדרת handler גלובלי לטיפול בשגיאות

כפי שראינו, כאשר נזרקת שגיאה בקוד מתוך בלוק ה-try היא נתפס בבלוק ה-catch. אם קטע הקוד שזורק את השגיאה אינו עטוף ב-try, השגיאה אינה נתפסת וביצוע הקוד מסתיים עם Fatal error (שגיאה קריטית).

ניתן להגדיר Handler גלובלי לתפיסת שגיאות, באמצעות הפונקציה set_exception_handler לתפיסה של Exception, באמצעות הפונקציה set_error_handler לתפיסה של Error או באמצעות הפונקציה register_shutdown_function לתפיסה גם של Exception וגם של Error.

באופן כללי, Handler הוא סוג של "פונקציית טיפול" אשר לא קוראים לה בצורה מפורשת, אלא היא נקראת במקרה של אירוע (Event) כלשהו בקוד. האירוע יכול להיות הקשה על מקש ספציפי, הזזה של העכבר, גלילה של העמוד או כל דבר אחר. במקרה שלנו, האירוע הוא זריקת שגיאה שלא נתפסת בדרך אחרת.

דוגמה:

<?php
function handleGlobalException($e) {
    echo "Global Handler: Uncaught Exception - " . $e->getMessage();
}

set_exception_handler("handleGlobalException");

// This will trigger the global exception handler
throw new Exception("This error was not caught!");
?>

בדוגמה זו, הגדרנו את הפונקציה handleGlobalException אשר משמשת כ-Handler גלובלי לתפיסת שגיאות מטיפוס Exception. הפונקציה מקבלת את הפרמטר e (שהוא ה-Exception) ומדפיסה את הודעת השגיאה הרלוונטית. הפונקציה set_exception_handler מגדירה את הפונקציה handleGlobalException כ-Handler לטיפול בשגיאות מטיפוס Exception. בשורת הקוד האחרונה אנו מדמים זריקת Exception שאינו עטוף בבלוק try-catch, ולכן, הפונקציה handleGlobalException מופעלת ומטפלת ב-Exception.

 

דוגמה:

<?php
function myErrorHandler($errno, $errstr, $errfile, $errline) {
    echo "Caught an error: [$errno] $errstr in $errfile on line $errline\n";
    return true; // Prevents PHP from executing its default error handler
}

// Set error handler
set_error_handler("myErrorHandler");

// Trigger an error - This will be caught
echo $undefinedVariable; // Notice: Undefined variable

// Throw an exception - This will NOT be caught!
throw new Exception("This is an exception.");
?>

בדוגמה זו, הגדרנו את הפונקציה myErrorHandler אשר משמשת כ-Handler גלובלי לתפיסת שגיאות מטיפוס Error. הפונקציה מדפיסה את השגיאה שנתפסה ומחזירה את הערך true. החזרת הערך true נועדה למנוע ביצוע של Handler ברירת המחדל לטיפוס בשגיאות מטיפוס Error של PHP. הפונקציה set_error_handler מגדירה את הפונקציה myErrorHandler כ-Handler לטיפול בשגיאות מטיפוס Error. אנו מנסים להדפיס את המשתנה undefinedVariable שאינו מוגדר, ולכן, הפונקציה myErrorHandler מופעלת לטיפול בשגיאה. בשורה האחרונה בקוד אנו מדמים זריקת Exception שאינו עטוף בבלוק try-catch, ולכן, הקוד מסיים עם שגיאת Fatal Error. נשים לב שהפונקציה myErrorHandler תופסת רק שגיאות מטיפוס Error ולא שגיאות מטיפוס Exception, ולכן, השגיאה שנוצרת בשורת הקוד האחרונה אינה נתפסת.

 

דוגמה:

<?php
register_shutdown_function(function () {
    $error = error_get_last();
    if ($error !== null) {
        echo "Fatal Error Caught: " . $error['message'];
    }
});

// This will cause a fatal error
echo 10 / 0; // Division by zero error
?>

בדוגמה זו, קראנו לפונקציה register_shutdown_function עם פרמטר שהוא פונקציה אנונימית (פונקציה ללא שם), ובכך הגדרנו את הפונקציה האנונימית כ-Handler גלובלי שמטפל בשגיאות מטיפוס Error ומטיפוס Exception. השימוש בפונקציה אנונימית הוא רק לשם הדוגמה, כאשר יכולנו במקום זאת להגדיר פונקציה רגילה עם שם, ולהשתמש בה כפרמטר לפונקציה register_shutdown_function  (בדיוק כפי שעשינו ב-2 הדוגמאות הקודמות בקריאות ל-set_exception_handler ול-set_error_handler). אנו מנסים להדפיס את ערך החלוקה של 10 ב-0, ושגיאת החלוקה ב-0 נתפסת באמצעות הפונקציה האנונימית, שם היא מודפסת למסך. הפונקציה error_get_last שבה עשינו שימוש בתוך הפונקציה האנונימית, מחזירה את השגיאה האחרונה שקרתה בקוד.

שימוש במספר בלוקים של catch

ניתן לתפוס טיפוסי שגיאות שונים, באמצעות כמה בלוקים של catch.

דוגמה:

<?php
function divide($a, $b) {
    if ($b === 0) {
        throw new DivisionByZeroError("Cannot divide by zero.");
    }
    return $a / $b;
}

try {
    echo divide(10, 0);
} catch (DivisionByZeroError $e) {
    echo "Caught Error: " . $e->getMessage();
} catch (Exception $e) {
    echo "Caught Exception: " . $e->getMessage();
}
?>

בדוגמה זו, הגדרנו את הפונקציה divide אשר מקבלת 2 פרמטרים. במקרה שמועבר הערך 0 כפרמטר השני, הפונקציה זורקת אובייקט מטיפוס DivisionByZeroError. אנו קוראים לפונקציה add עם 2 פרמטרים בבלוק ה-try. במקרה שהפונקציה add זורקת אובייקט מטיפוס DivisionByZeroError, אנו מפעילים את בלוק ה-catch הראשון. במקרה שהפונקציה add זורקת אובייקט מטיפוס Exception (כולל כל אובייקט שיורש את המחלקה Exception), אנו מפעילים את בלוק ה-catch השני.

הוספת תגובה
אנו משתמשים בעוגיות על מנת לשפר את חווית המשתמש באתר. מדיניות הפרטיותאני מסכים