ป้ายกำกับ: Notices

PHP: Internal Errors as ExceptionsPHP: Internal Errors as Exceptions

การจัดการ Error ใน PHP ยุคใหม่ (ตั้งแต่ PHP 7 เป็นต้นมา และสมบูรณ์แบบมากใน PHP 8) ได้เปลี่ยนผ่านจากระบบเดิมที่เป็น Error Reporting (พวก Notice, Warning, Fatal Error) มาเป็นระบบ Object-Oriented Exceptions เกือบทั้งหมดแล้ว

บทความนี้จะพาคุณไปเจาะลึกว่าเกิดอะไรขึ้นเมื่อ Internal Errors กลายเป็น Exceptions และเราจะเขียนโค้ดดักจับมันอย่างมืออาชีพได้อย่างไรครับ


🚀 จาก Error ดั้งเดิม สู่ Throwable Engine

ใน PHP 5 หรือเวอร์ชันที่เก่ากว่านั้น เมื่อโค้ดเกิดความผิดพลาดรุนแรง เช่น เรียกใช้ฟังก์ชันที่ไม่มีอยู่จริง (Fatal Error) หรือหน่วยความจำเต็ม ระบบจะหยุดทำงานทันที (Die) โดยที่เราไม่สามารถใช้บล็อก try-catch ไปดักจับมันได้

แต่ใน PHP 7+ มีการปรับปรุงโครงสร้างภายในใหม่ โดยให้ Internal Errors ส่วนใหญ่ถูกโยนออกมาเป็น Exceptions ผ่าน Interface ที่ชื่อว่า Throwable

โครงสร้างลำดับชั้น (Hierarchy) ของ Throwable

interface Throwable
    ├── Exception (สำหรับ User-level exceptions ทั่วไป)
    └── Error (สำหรับ Internal PHP errors)
        ├── ArithmeticError
        ├── DivisionByZeroError
        ├── AssertionError
        ├── CompileError
        ├── TypeError
        └── ArgumentCountError

💡 ข้อควรจำ: Error ไม่ได้สืบทอดมาจากคลาส Exception แต่ทั้งคู่สืบทอดมาจาก Throwable เหมือนกัน ดังนั้นถ้าคุณใช้ catch (Exception $e) คุณจะไม่สามารถดักจับ Internal Errors ได้


🛠️ วิธีดักจับ Internal Errors ที่ถูกต้อง

เมื่อ Internal Errors กลายเป็น Exceptions แล้ว วิธีการรับมือที่ถูกต้องและปลอดภัยที่สุดคือการดักจับด้วย Throwable หรือระบุคลาส Error ให้ตรงจุด

❌ แบบที่ผิด (จับไม่โดน)

โค้ดด้านล่างนี้จะยังคงทำให้เกิด Fatal Error และโปรแกรมหยุดทำงาน เพราะ TypeError ไม่ได้เป็นลูกของ Exception

try {
    // ส่ง Argument ผิดประเภท (Internal TypeError)
    strlen(["this is an array"]); 
} catch (Exception $e) {
    echo "จับคู่ข้อผิดพลาด: " . $e->getMessage();
}

แบบที่ถูกต้อง (จับได้อยู่หมัด)

เปลี่ยนไปใช้ Throwable เพื่อให้ครอบคลุมทั้งระบบ หรือใช้คลาส Error โดยตรง

try {
    strlen(["this is an array"]); 
} catch (Throwable $e) {
    echo "⚠️ จับ Internal Error ได้แล้ว: " . $e->getMessage();
    // ผลลัพธ์: ⚠️ จับ Internal Error ได้แล้ว: strlen(): Argument #1 ($string) must be of type string, array given
}

🔍 เจาะลึก Internal Errors ที่พบบ่อย

นี่คือตัวอย่างของ Internal Errors ที่อัปเกรดมาเป็น Exceptions และเราสามารถจัดการมันได้แล้วในปัจจุบัน

TypeError

เกิดขึ้นเมื่อส่ง Argument ผิดประเภท หรือฟังก์ชัน Return ค่าไม่ตรงกับที่กำหนดไว้

try {
    function add(int $a, int $b) { return $a + $b; }
    add("ห้า", 5);
} catch (TypeError $e) {
    echo "Type mismatch: " . $e->getMessage();
}

DivisionByZeroError

เกิดขึ้นเมื่อมีความพยายามหารตัวเลขด้วยศูนย์ (เฉพาะการใช้โอเปอเรเตอร์ % หรือฟังก์ชันบางตัว ส่วนการหารด้วย / ใน PHP 8 จะได้ผลลัพธ์เป็น INF แต่จะโยน Error นี้ในบางกรณี)

try {
    $result = 10 % 0;
} catch (DivisionByZeroError $e) {
    echo "ล้มเหลว: ห้ามหารด้วยศูนย์!";
}

ArgumentCountError

เกิดขึ้นเมื่อส่ง Argument ให้ฟังก์ชันน้อยกว่าที่มันต้องการ

try {
    function myProfile($name, $age) { }
    myProfile("Tam"); // ส่งไม่ครบ
} catch (ArgumentCountError $e) {
    echo "ข้อมูลไม่ครบ: " . $e->getMessage();
}

🎯 ประโยชน์ของการเปลี่ยนแปลงนี้

  1. Application ไม่ตายกลางคัน (Graceful Degradation): หากเกิด Error ในระบบหลังบ้าน เราสามารถดักจับแล้วแสดงหน้า Error Page สวย ๆ ให้ผู้ใช้ดู แทนที่จะปล่อยหน้าจอขาว (White Screen of Death)
  2. ทำ Logging ได้ง่ายขึ้น: สามารถเขียนโค้ดส่ง Error บันทึกเข้าคลังข้อมูล (เช่น Log files, Sentry) ได้ทันทีผ่านบล็อก catch
  3. เขียน Clean Code ได้ง่ายขึ้น: โครงสร้าง try-catch-finally ทำให้แยกส่วนของ Business Logic ออกจาก Error Handling ได้อย่างชัดเจน

📢 ข้อควรระวัง: Warnings และ Notices ยังคงอยู่!

แม้ว่า Fatal Errors จะกลายเป็น Exceptions แล้ว แต่ความผิดพลาดระดับ Warning และ Notice (เช่น การใช้ตัวแปรที่ไม่ได้ประกาศ หรือ Array undefined index) ยังคงไม่พ่นออกมาเป็น Exception นอกเสียจากว่าคุณจะใช้ PHP 8 ซึ่งบางตัวถูกยกฐานะขึ้นมาแล้ว

หากต้องการเปลี่ยน Warning/Notice ทั้งหมดให้กลายเป็น Exceptions คุณต้องใช้ฟังก์ชัน set_error_handler ควบคู่ไปด้วย

// เปลี่ยนทุก Error ดั้งเดิมให้กลายเป็น ErrorException
set_error_handler(function($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        return;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
});

// ตอนนี้แม้แต่เรื่องเล็กๆ ก็ใช้ try-catch ครอบได้แล้ว
try {
    echo $undefinedVariable; // Notice ดั้งเดิม จะถูกโยนเป็น Exception ทันที
} catch (ErrorException $e) {
    echo "จับ Notice ได้: " . $e->getMessage();
}

อ่านเพิ่มเติม