1. Ngoại lệ trong PHP là gì?

Một ngoại lệ (exceptions) là một tín hiệu chỉ ra số loại sự kiện hoặc lỗi đặc biệt xảy ra. Nói một cách dễ hiểu hơn thì ngoại lệ là một đối tượng mô tả lỗi hoặc hành vi không mong muốn của tập lệnh PHP. Các ngoại lệ có thể được gây ra do nhiều lý do. Ví dụ như kết nối hoặc truy vấn CSDL không thành công, tệp truy vấn không tồn tại, hoặc là lỗi phép chia cho số 0 hoặc bất kỳ một sự kiện nào đó không hợp lệ và không được thực thi. Các ngoại lệ được ném bởi nhiều hàm và lớp PHP. Các hàm và lớp do người dùng định nghĩa cũng có thể đưa ra các ngoại lệ. Chính vì vậy mà ta có thể dùng ngoại lệ là một cách tốt để dừng một hàm khi nó gặp phải dữ liệu mà nó không thể sử dụng.

Các lỗi ngoại lệ tiềm ẩn được bao bọc bên trong khối thử nếu gặp phải ngoại lệ, sẽ được ném để bắt hoặc cuối cùng là khối. PHP thường xử lý ngoại lệ trong một khối bắt riêng biệt cho từng loại ngoại lệ khác nhau.

Trái ngược với việc xử lý lỗi trong PHP, xử lý ngoại lệ là phương pháp hướng đối tượng để xử lý lỗi, cung cấp hình thức báo cáo lỗi linh hoạt và có kiểm soát hơn. PHP đã cung cấp cho ta một cơ chế xử lý ngoại lệ mạnh mẽ cho phép ta xử lý ngoại lệ một cách đơn giản.

2. Các phương thức xử lý ngoại lệ trong PHP

Khi ngoại lệ được kích hoạt thì sẽ xảy ra một số việc sau:

  • Trạng thái code hiện tại được lưu
  • Việc thực thi code sẽ chuyển sang một hàm xử lý ngoại lệ được xác định trước(tùy chỉnh)
  • Tùy thuộc vào tình huống, trình xử lý có thể tiếp tục thực hiện từ trạng thái mà đã lưu, chấm dứt thực thi tập lệnh hoặc tiếp tục tập lệnh từ một vị trí khác trong tập lệnh.

Và ta sẽ có một số phương thức xử lý ngoại lệ trong PHP như sau:

  • Sử dụng try, throw và catch.
  • Xử lý nhiều ngoại lệ.
  • Tạo lớp ngoại lệ tùy chỉnh.
  • Ném lại một ngoại lệ.
  • Thiết lập trình xử lý ngoại lệ cao cấp.

Lưu ý

Ngoại lệ chỉ được nên sử dụng với các điều kiện lỗi và không được sử dụng để nhảy đến một vị trí khác trong tập lệnh tại một thời điểm cụ thể.

3. Lớp lỗi trong PHP

Lớp lỗi là lớp cơ sở cho tất cả các lỗi PHP nội bộ. Một số lỗi sẽ tạo ra một lớp lỗi con cụ thể như lỗi phân tích cú pháp, lỗi loại,…

3.1. Lỗi cú pháp/ phân tích cú pháp

Lỗi cú pháp/ phân tích cú pháp trong mã trong khi biên dịch thì lỗi cú pháp được đưa ra. Nếu có mã lỗi thì trình phân tích cú pháp của PHP không thể diễn giải mã và nó ngừng hoạt động.

Ví dụ:

<?php
    $x = "Exception";
    y = "Handling";
    echo $x . ' ' . y;
?>

Lỗi hiển thị: syntax error, unexpected ‘=’ in line 3

3.2. Lỗi loại

Khi kiểu dữ liệu không khớp xảy ra trong PHP trong khi thực hiện một hoạt động, lỗi loại sẽ được đưa ra. Có ba trường hợp xảy ra loại lỗi này:

  • Số lượng đối số được truyền vào một hàm tích hợp không hợp lệ.
  • Giá trị trả về từ một hàm không khớp với kiểu trả về của hàm đã khai báo.
  • Kiểu đối số được truyền cho một hàm không khớp với kiểu tham số đã khai báo.

Ví dụ:

<?php
function add(int $x, int $y)
{
    return $x + $y;
}
try {
    $value = add('Type', 10);
}
catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}
?>

Lỗi hiển thị ra là:

add(): Argument #1 ($x) must be of type int, string given, called in C:\xampp\htdocs\index.php on line 7

3.3. Lỗi số học

Sẽ xảy ra khi thực hiện một phép toán, chuyển bit theo số âm hoặc gọi hàm intdiv() lỗi số học được ném ra.

Ví dụ: với toán tử bộ phận

<?php
try {
    intdiv(PHP_INT_MIN, -1);
}
catch (ArithmeticError $e) {
    echo $e->getMessage();
}
?>

Lỗi hiển thị:

Division of PHP_INT_MIN by -1 is not an integer.

Ví dụ 2: với toán tử bộ phận trả về INF:

<?php
try {
    $x = 4;
    $y = 0;
    $result = $x / $y;
}
catch (DivisionByZeroError $e) {
    echo $e->getMessage();
}
?>

Lỗi hiển thị:

PHP Warning: Division by zero in /home/ADYlPB/prog.php on line 5

3.4. Lỗi xác nhận

Khi một lệnh gọi khẳng định không thành công hoặc giả sử khi điều kiện bên trong lệnh gọi khẳng định không đáp ứng, lỗi Xác nhận sẽ được đưa ra. Mô tả chuỗi phát ra thông báo E_DEPRECATED từ phiên bản PHP 7.2. Lỗi xác nhận được ném bởi khẳng định sẽ chỉ được gửi đến khối bắt nếu khẳng định exception = on được bật trong php.ini.

Ví dụ:

<?php
try {
    $x  = 1;
    $y  = 2;
    $result = assert($x === $y);
    if (!$result) {
        throw new DivisionByZeroError('Assertion error');
    }
}
catch (AssertionError $e) {
    echo $e->getMessage();
}
?>

Lỗi hiển thị:

Assertion error

3.5. Lỗi giá trị

Khi loại đối số là đúng và giá trị của đối số đó không chính xác, một lỗi giá trị sẽ được đưa ra. Các loại lỗi này xảy ra khi:

  • Chuyển một giá trị âm khi hàm mong đợi một giá trị dương.
  • Truyền một chuỗi hoặc mảng trống khi hàm yêu cầu một chuỗi / mảng không trống.

Ví dụ:

<?php
    $x = strpos("u", "austin", 24);
    var_dump($x);
?>

Lỗi hiển thị:

Fatal error: Uncaught ValueError: strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack) in C:\xampp\htdocs\index.php:2 Stack trace: #0 C:\xampp\htdocs\index.php(2): strpos(‘u’, ‘austin’, 24) #1 {main} thrown in C:\xampp\htdocs\index.php on line 2

Ví dụ 2:

<?php
    $x = array_rand(array(), 0);
    var_dump($x);
?>

Lỗi hiển thị:

Fatal error: Uncaught ValueError: array_rand(): Argument #1 ($array) cannot be empty in C:\xampp\htdocs\index.php:2 Stack trace: #0 C:\xampp\htdocs\index.php(2): array_rand(Array, 0) #1 {main} thrown in C:\xampp\htdocs\index.php on line 2

4. Sử dụng try, throw, catch

Mình sẽ giải thích một cách đơn giản cho các bạn như sau:

  • Try – Một hàm sử dụng ngoại lệ phải ở trong khối “try”. Nếu ngoại lệ không xảy ra, code sẽ tiếp tục như bình thường. Tuy nhiên, nếu ngoại lệ xảy ra, ngoại lệ bị “throw”.
  • Throw – Đây là cách bạn kích hoạt ngoại lệ. Mỗi “throw” phải có ít nhất một “try”.
  • Catch – Một khối “catch” bắt một ngoại lệ và tạo một đối tượng chứa thông tin ngoại lệ.

4.1. Ném một ngoại lệ

Câu lệnh throw cho phép một hàm hoặc phương thức do người dùng định nghĩa ném ra một ngoại lệ. Khi một ngoại lệ được ném ra, mã theo sau nó sẽ không được thực thi. Nếu một ngoại lệ không được bắt, một lỗi nghiêm trọng sẽ xảy ra với thông báo “Uncaught Exception”.

Ví dụ: thử ném một ngoại lệ mà không bắt nó

<?php
//tạo hàm với một ngoại lệ
function checkNum($number) {
  if($number>1) {
    throw new Exception("Giá trị phải nhỏ hơn hoặc bằng 1.");
  }
  return true;
} 
//kích hoạt ngoại lệ
checkNum(2);
?>

Kết quả là:

Fatal error: Uncaught Exception: Giá trị phải nhỏ hơn hoặc bằng 1. in C:\xampp\htdocs\php\vi-du-ngoai-le-1.php:5 Stack trace: #0 C:\xampp\htdocs\php\vi-du-ngoai-le-1.php(11): checkNum(2) #1 {main} thrown in C:\xampp\htdocs\php\vi-du-ngoai-le-1.php on line 5

4.2. Bắt một ngoại lệ

Để tránh lỗi từ ví dụ trên, chúng ta cần phải sử dụng đối tượng Exception thích hợp với khối catch để xử lý một ngoại lệ. Chúng ta có thể sử dụng câu lệnh try…catch để bắt các ngoại lệ và tiếp tục quá trình.

Ví dụ:

<?php
//tạo hàm và throw một ngoại lệ
function checkNum($number) {
  if($number > 1) {
    throw new Exception("Giá trị phải nhỏ hơn hoặc bằng 1.");
  }
  return true;
} 
//kích hoạt ngoại lệ trong khối "try"
try {
  checkNum(2);
  // nếu ngoại lệ được ném ra thì lệnh sau không được thực thi
  echo 'Number nhỏ hơn hoặc bằng 1.';
}
//catch exception
catch(Exception $e) {
  echo 'Message: ' .$e->getMessage();
}
?>

Kết quả: Message: Giá trị phải nhỏ hơn hoặc bằng 1.

  • Hàm checkNum() được tạo. Nó sẽ kiểm tra nếu một số lớn hơn 1. Nếu có, một ngoại lệ được ném.
  • Hàm checkNum() được gọi trong khối “try”.
  • Ngoại lệ trong hàm checkNum() được ném.
  • Khối “catch” lấy ra ngoại lệ và tạo một đối tượng ($e) chứa thông tin ngoại lệ.
  • Thông báo lỗi từ ngoại lệ được lặp lại bằng cách gọi $e->getMessage() từ đối tượng ngoại lệ.

4.3. Ném lại một ngoại lệ

Đôi khi, khi một ngoại lệ được ném, bạn có thể muốn xử lý nó khác với cách tiêu chuẩn. Có thể ném một ngoại lệ lần thứ hai trong một khối “catch”. Tập lệnh nên ẩn các lỗi hệ thống khỏi người dùng. Lỗi hệ thống có thể quan trọng đối với người lập trình nhưng không quan trọng với người dùng. Để giúp người dùng dễ dàng hơn, bạn có thể ném lại ngoại lệ bằng thông điệp thân thiện với người dùng.

Ví dụ: ném lại một ngoại lệ

<?php
class CustomException3 extends Exception {
  public function errorMessage() {
    //error message
    $errorMsg = $this->getMessage().' không phải là một địa chỉ E-Mail hợp lệ.';
    return $errorMsg;
  }
}
$email = "someone@example.com";
try {
  try {
    //kiểm tra tồn tại chuỗi "example" trong địa chỉ email
    if(strpos($email, "example") !== FALSE) {
      //throw exception nếu email không hợp lệ
      throw new Exception($email);
    }
  }
  catch(Exception $e) {
    //ném lại một ngoại lệ
    throw new CustomException3($email);
  }
}
catch (CustomException3 $e) {
  //hiển thị message
  echo $e->errorMessage();
}
?>

Kết quả: someone@example.com is not a valid E-Mail address.

Ví dụ trên đã kiểm tra xem địa chỉ email có chứa chuỗi “example” trong đó hay không, nếu có, ngoại lệ sẽ được ném lại:

  • Lớp CustomException3() được tạo ra như là một phần mở rộng của lớp Exception. Bằng cách này, nó kế thừa tất cả các phương thức và thuộc tính từ lớp ngoại lệ cũ.
  • Hàm errorMessage() được tạo. Hàm này trả về một thông báo lỗi nếu địa chỉ e-mail không hợp lệ.
  • Biến $email được đặt thành một chuỗi là địa chỉ e-mail hợp lệ, nhưng chứa chuỗi “example”.
  • Khối “try” chứa một khối “thử” khác để làm cho nó có thể ném lại ngoại lệ.
  • Ngoại lệ được kích hoạt vì e-mail chứa chuỗi “example”.
  • Khối “catch” bắt ngoại lệ và trả lại “CustomException3”.
  • “CustomException3” bị bắt và hiển thị thông báo lỗi.

4.4. Câu lệnh kết thúc

Câu lệnh try…catch…finally có thể được sử dụng để bắt các ngoại lệ. Mã trong finally khối sẽ luôn chạy bất kể có bắt được ngoại lệ hay không. Nếu finally có, khối catch là tùy chọn.

Ví dụ: hiển thị thông báo khi một ngoại lệ được đưa ra và sau đó cho biết rằng quá trình đã kết thúc:

<?php
function divide($dividend, $divisor) {
  if($divisor == 0) {
    throw new Exception("Chia cho số không");
  }
  return $dividend / $divisor;
}
try {
  echo divide(5, 0);
} catch(Exception $e) {
  echo "Không thể phân chia ";
} finally {
  echo "Quá trình hoàn tất";
}
?>

Ví dụ 2: xuất một chuỗi ngay cả khi không bắt được ngoại lệ:

<?php
function divide($dividend, $divisor) {
  if($divisor == 0) {
    throw new Exception("Chia cho số không");
  }
  return $dividend / $divisor;
}
try {
  echo divide(5, 0);
} finally {
  echo "Quá trình hoàn tất";
}
?>