Menu

Ngoại lệ trong PHP: throw và lớp exception tùy chỉnh

Bạn ném một ngoại lệ trong PHP bằng throw new Exception('message');, và nơi gọi đọc nó bằng $e->getMessage() trong một khối catch. Tìm hiểu các lớp exception có sẵn, cách viết exception tùy chỉnh, getCode(), getLine() và getPrevious(), throw dưới dạng biểu thức, và các lỗi do chính PHP ném ra như ValueError và TypeError.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Bạn ném một ngoại lệ (exception) trong PHP bằng throw new Exception('message');. Việc thực thi dừng ở dòng đó và nhảy đến khối catch gần nhất nhận kiểu của ngoại lệ, nơi $e->getMessage() trả về thông báo. Hãy dùng một lớp cụ thể, như InvalidArgumentException, để nơi gọi phân biệt được các vấn đề.

Ngoại lệ là một đối tượng. throw đẩy nó ngược lên call stack đến nơi gọi đầu tiên có catch phù hợp, và nơi gọi đó quyết định phải làm gì; hàm phát hiện ra vấn đề không cần biết điều đó. Cách bắt ngoại lệ, finally và bắt nhiều kiểu cùng lúc có ở trang try catch.

Đối tượng ngoại lệ chứa gì

Mọi ngoại lệ đều mang theo một thông báo, một mã số nguyên, tệp và dòng nơi nó được tạo ra, cùng một stack trace. Hàm khởi tạo nhận ($message, $code, $previous), tất cả đều không bắt buộc:

getLine() là dòng chứa throw new, không phải dòng của catch. Trace liệt kê các lời gọi dẫn đến đó (#0 /home/index.php(8): findOrder()), thường là thứ đầu tiên cần đọc khi một lỗi xuất hiện trong log. Ép ngoại lệ thành chuỗi (echo $e; hoặc (string) $e) sẽ in ra tất cả theo định dạng chuẩn của PHP.

Các lớp exception có sẵn

PHP có sẵn một họ lớp exception trong Standard PHP Library (SPL). Ném đúng lớp gọi tên vấn đề giúp code dễ đọc hơn và cho phép nơi gọi bắt thật chính xác. Tất cả các lớp này đều kế thừa Exception:

LớpNém khi
InvalidArgumentExceptionmột đối số có dạng sai: tên rỗng, tùy chọn không xác định
DomainExceptionmột giá trị nằm ngoài tập hợp hợp lý: giá âm
OutOfRangeExceptioncode yêu cầu một chỉ số không bao giờ có thể tồn tại (bug ở nơi gọi)
OutOfBoundsExceptionthiếu một khóa hoặc chỉ số trong dữ liệu chỉ biết được lúc chạy
RangeExceptionmột kết quả tính ra nằm ngoài phạm vi hợp lệ trong lúc chạy
LengthExceptionthứ gì đó quá dài hoặc quá ngắn
RuntimeExceptionmột vấn đề chỉ phát hiện được lúc chạy: ổ đĩa đầy, hết thời gian chờ
UnexpectedValueExceptionmột hàm trả về hoặc nhận một giá trị có kiểu bạn không ngờ tới
LogicExceptionbản thân code bị sai, là bug chứ không phải dữ liệu đầu vào xấu
JsonExceptionđược json_encode()/json_decode() ném ra khi dùng JSON_THROW_ON_ERROR

InvalidArgumentException, DomainException, LengthException và OutOfRangeException kế thừa LogicException; OutOfBoundsException, RangeException và UnexpectedValueException kế thừa RuntimeException. Bạn có thể tự kiểm tra các lớp cha của bất kỳ lớp nào:

TypeError có lớp cha là Error, không phải Exception: nó thuộc họ thứ hai, ở bên dưới.

Các lỗi do chính PHP ném ra

Từ PHP 7, và càng nhiều hơn từ PHP 8, các hàm và toán tử của chính PHP ném ra lớp con của Error cho những vấn đề trước đây chỉ là cảnh báo. Bạn bắt chúng theo cùng một cách, nhưng catch (Exception $e) sẽ không khớp với chúng:

DivisionByZeroError, TypeError, ValueError, ArgumentCountError, UnhandledMatchError và Error thuần đều kế thừa Error. Mỗi lớp thường đánh dấu một bug trong code gọi hơn là dữ liệu xấu, đó là lý do chúng nằm ngoài cây Exception. Chính declare(strict_types=1) khiến str_repeat(5, 2) thành TypeError; nếu không có nó, PHP sẽ chuyển 5 thành "5".

Viết lớp exception tùy chỉnh

Một lớp con chỉ một dòng thường là đủ: chính tên lớp đã mang ý nghĩa, và nơi gọi có thể bắt đúng kiểu đó. Khi nơi bắt cần dữ liệu (đơn hàng nào, số tiền bao nhiêu), hãy thêm thuộc tính và chuyển thông báo lên bằng parent::__construct():

Hãy chọn lớp cha cẩn thận: kế thừa RuntimeException (thay vì Exception thuần) nghĩa là code nào bắt RuntimeException cũng xử lý luôn ngoại lệ của bạn. Một hệ thống phân cấp nhỏ, mỗi thư viện hoặc module có một ngoại lệ gốc và các lớp con cụ thể bên dưới, cho phép nơi gọi chọn giữa "bắt mọi thứ từ phần thanh toán" và "chỉ bắt đúng trường hợp này".

Nối chuỗi ngoại lệ với previous

Khi bạn bắt một ngoại lệ cấp thấp rồi ném một ngoại lệ cấp cao hơn, hãy truyền ngoại lệ gốc làm đối số thứ ba. Không có gì bị mất: getPrevious() lần ngược về nguyên nhân gốc.

Người dùng thấy "customers.csv could not be imported"; còn log, khi lần theo chuỗi, nhận thêm "row 2 has 2 columns". Nếu một ngoại lệ đã nối chuỗi không bao giờ được bắt, lỗi nghiêm trọng của PHP sẽ in cả chuỗi: nguyên nhân gốc trước, rồi đến từng lớp bọc dưới một dòng Next.

throw dưới dạng biểu thức

Từ PHP 8.0, throw là một biểu thức, nên nó có thể đặt ở bất kỳ đâu cần một giá trị: sau ??, trong toán tử ba ngôi, trong arrow function. Nhờ đó "lấy giá trị này hoặc báo lỗi" chỉ còn một dòng:

Khi nào không nên ném ngoại lệ

Ngoại lệ dành cho những tình huống mà hàm hiện tại không xử lý được và luồng chạy bình thường không lường trước. Một lần tìm kiếm không thấy gì, một trường không bắt buộc bị bỏ trống, hay một người dùng chưa đăng nhập đều là kết quả bình thường: hãy trả về null, false hoặc một mảng rỗng để nơi gọi tự kiểm tra, và giữ ngoại lệ cho những trường hợp mà chạy tiếp sẽ là sai.

Thêm hai thói quen giúp code xử lý ngoại lệ dễ đọc: đừng bao giờ bắt một ngoại lệ chỉ để bỏ qua nó (một catch (Exception $e) {} rỗng sẽ che giấu bug thật tiếp theo), và hãy ném các lớp cụ thể thay vì Exception trơn, để nơi gọi không bao giờ bị buộc phải bắt mọi thứ chỉ để xử lý một trường hợp.

Câu hỏi thường gặp

Làm sao ném một ngoại lệ trong PHP?

Tạo một đối tượng exception rồi ném nó: throw new InvalidArgumentException('Quantity must be positive');. Việc thực thi dừng ở dòng đó và nhảy đến khối catch phù hợp gần nhất; nếu không có, script kết thúc với một lỗi nghiêm trọng "Uncaught".

Làm sao tạo exception tùy chỉnh trong PHP?

Kế thừa Exception hoặc một lớp con của nó: class PaymentFailedException extends RuntimeException {}. Chỉ một dòng đó là đủ để ném và bắt nó theo kiểu riêng. Thêm thuộc tính và hàm khởi tạo khi nơi bắt cần thêm dữ liệu, và gọi parent::__construct($message, $code, $previous).

getMessage và getCode khác nhau thế nào?

getMessage() trả về đoạn chữ được truyền làm đối số đầu tiên của hàm khởi tạo, dành cho người đọc và log. getCode() trả về số nguyên được truyền làm đối số thứ hai (mặc định là 0), hữu ích cho việc kiểm tra bằng máy, chẳng hạn ánh xạ lỗi sang mã trạng thái HTTP.

Nên ném exception nào trong PHP?

Dùng một lớp SPL có sẵn khớp với vấn đề: InvalidArgumentException cho đối số sai, DomainException cho giá trị nằm ngoài phạm vi cho phép, RuntimeException cho những lỗi chỉ thấy được lúc chạy (ổ đĩa đầy, hết thời gian chờ), LogicException cho lỗi lập trình. Với những lỗi mà nơi gọi cần phân biệt, hãy tạo lớp con của riêng bạn.

Nối chuỗi ngoại lệ (exception chaining) trong PHP là gì?

Là việc truyền ngoại lệ gốc làm đối số thứ ba của hàm khởi tạo khi bạn ném một ngoại lệ mới: throw new ImportException('Import failed', 0, $e);. Ngoại lệ mới bổ sung ngữ cảnh, và $e->getPrevious() trên nó trả về nguyên nhân gốc, nên không mất chi tiết nào.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu