Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
PHP

PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch

PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch

Hiểu hệ thống phân cấp Throwable (Error và Exception) trong PHP, thực hành bắt lỗi bằng try/catch/finally, gắn nhãn #[Deprecated] và trả mã thoát CLI chính xác.

catch Exception không bắt được mọi Throwable

Trong các hệ thống PHP cũ, lập trình viên thường bọc đoạn mã giao tiếp bên ngoài bằng khối try { ... } catch (Exception $e) { ... } với suy nghĩ rằng mọi sự cố đều sẽ lọt vào nhánh catch. Tuy nhiên, từ khi PHP hợp nhất kiến trúc lỗi dưới giao diện gốc Throwable, các lỗi nội tại của máy ảo như truyền sai kiểu dữ liệu (TypeError), truyền đối số ngoài miền hợp lệ (ValueError), chia nguyên cho không (DivisionByZeroError) hoặc thiếu nhánh khớp trong match (UnhandledMatchError) đều kế thừa từ lớp Error chứ không kế thừa từ Exception. Nếu một hàm đang giữ khóa tệp mà gặp TypeError và không có khối finally, chương trình sẽ thoát đột ngột mà không giải phóng tài nguyên.

Bài học mở đầu Chặng 4 này giúp bạn nắm vững cây phân cấp Throwable (Error và Exception), bảo toàn tài nguyên với try / catch / finally, bọc ngoại lệ giữ dấu vết gốc ($previous), và thuộc tính #[\Deprecated] mới trên PHP 8.4. Bài viết nối tiếp PHP EP 14: Namespace, PSR-4 và Composer trong PHP và chuẩn bị cho PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP. Tài liệu tham chiếu gồm PHP Manual: Exceptions, Throwable Hierarchy và PHP RFC: #[\Deprecated] Attribute.

Hồ sơ kiểm chứng & Môi trường Lab (Verification Profile):

  • Ngày kiểm chứng: 2026-10-26.
  • Môi trường thực thi: Container cô lập php-course-qa:8.4 (--network none, --read-only, --tmpfs /tmp:rw,exec,nosuid,size=64m, user phplab UID 1001).
  • Phiên bản runtime: PHP 8.4.27 (cli) trên Alpine Linux 3.21 (aarch64), Zend Engine v4.4.27.
  • Giới hạn kiểm chứng: Kiểm chứng cây phân cấp Throwable, thứ tự thực thi try/catch/finally và thuộc tính #[\Deprecated] của PHP 8.4 trong SAPI CLI; chưa đi vào thu thập lỗi qua mạng.

Phân biệt Error, Exception, finally và Deprecated

Bảng dưới đây tổng hợp hợp đồng kỹ thuật của các thành phần xử lý lỗi trong PHP 8.4:

Thành phần Vị trí / Cú pháp Hành vi thực tế trong PHP 8.4 Điều kiện / Không nên giả định
Giao diện gốc Throwable Gốc của cả hai nhánh Error và Exception Mọi đối tượng ném bằng throw đều thực thi Throwable (có getMessage(), getTrace(), getPrevious()). Lớp người dùng không thể implements Throwable trực tiếp mà phải extends Exception.
Nhánh Error TypeError, ValueError, ParseError, ArithmeticError Do Zend Engine ném ra khi vi phạm kiểu dữ liệu, cú pháp hoặc tham số cốt lõi. Phản ánh lỗi lập trình (bug) cần sửa trong mã nguồn; không nên bắt ở tầng nghiệp vụ để bỏ qua.
Nhánh Exception RuntimeException, LogicException, InvalidArgumentException Dùng cho ngoại lệ nghiệp vụ, sự cố kết nối mạng, hệ thống tệp hoặc dữ liệu đầu vào sai. Có thể bắt nhiều kiểu bằng catch (InvalidArgumentException | DomainException $e).
Khối finally try { ... } finally { ... } Luôn thực thi dù khối try thành công, có lệnh return sớm hay ném ngoại lệ chưa bắt. Tuyệt đối không đặt lệnh return trong finally vì sẽ nuốt mất ngoại lệ đang ném ra từ try!
Thuộc tính #[\Deprecated] (PHP 8.4) #[\Deprecated(message: 'Use v2()', since: '8.4')] Phát thông báo E_USER_DEPRECATED chuẩn hóa ngay khi hàm, phương thức hoặc hằng số lớp được gọi. Áp dụng cho hàm, phương thức, hằng số lớp và case Enum trong PHP 8.4.

Chuỗi nguyên nhân giúp truy vết ngoại lệ

Trong kiến trúc Zend Engine (PHP Manual: Throwable), câu lệnh throw là một biểu thức ($val = $input ?? throw new InvalidArgumentException(...)). Khi ngoại lệ được ném ra, máy ảo cuộn ngược ngăn xếp lời gọi hàm (stack unwinding) để tìm khối catch khớp kiểu gần nhất. Trên đường cuộn ngược đó, mọi khối finally đi qua đều được thực thi chắc chắn 100% để đóng tệp hoặc giải phóng khóa.

Khi bắt một ngoại lệ kỹ thuật ở tầng hạ tầng để chuyển đổi thành ngoại lệ nghiệp vụ, hãy luôn truyền ngoại lệ gốc vào tham số thứ ba $previous: throw new ConfigLoadException('Msg', 500, $e). Nhờ đó, kỹ sư vận hành vẫn truy vết được nguyên nhân gốc rễ qua $e->getPrevious().

Trong PHP 8.4, đặc tả PHP RFC: #[\Deprecated] Attribute cho phép gắn #[\Deprecated(message: 'Use executeV2() instead', since: '8.4')] lên hàm, phương thức và hằng số để chính Zend Engine tự động phát cảnh báo Deprecated chuẩn hóa lúc chạy mà không cần gọi trigger_error() thủ công.

Chu trình phát sinh, đánh chặn ngoại lệ và dọn dẹp tài nguyên trong khối finally.
Hình 1. Chu trình phát sinh, đánh chặn ngoại lệ và dọn dẹp tài nguyên trong khối finally. Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

Kiểm tra try/catch/finally và Deprecated

Chúng ta sẽ tạo kịch bản kiểm tra thứ tự thực thi của try, catch và finally trong cả hai trường hợp thành công và ném ngoại lệ, đồng thời kiểm chứng #[\Deprecated] của PHP 8.4.

Bước 1: Tạo vùng làm việc tạm an toàn

Mục đích: Khởi tạo thư mục thực hành tách biệt trong /tmp.

WORKDIR="$(mktemp -d /tmp/php-ep15-XXXXXX)"
cd "$WORKDIR"
pwd

Bước 2: Tạo kịch bản error_flow.php theo dõi dấu vết try -> catch -> finally

Mục đích: Chứng minh khối finally luôn chạy ở bước cuối cùng bất kể phép chia intdiv() thành công hay bị chặn bởi InvalidArgumentException.

cat <<'EOF' > "$WORKDIR/error_flow.php"
<?php
declare(strict_types=1);

class ServiceRunner
{
    #[\Deprecated(message: 'Use executeV2() instead', since: '8.4')]
    public function executeLegacy(): string
    {
        return 'legacy';
    }

    public function run(int $divisor): string
    {
        $trace = [];
        try {
            $trace[] = 'try';
            if ($divisor === 0) {
                throw new InvalidArgumentException('Divisor cannot be zero');
            }
            $trace[] = 'ok:' . intdiv(100, $divisor);
        } catch (InvalidArgumentException $e) {
            $trace[] = 'catch:' . $e->getMessage();
        } finally {
            $trace[] = 'finally';
        }
        return implode('->', $trace);
    }
}

$runner = new ServiceRunner();
echo "Flow1=" . $runner->run(5) . " | Flow2=" . $runner->run(0) . "\n";
EOF
php "$WORKDIR/error_flow.php"

Kết quả quan sát được:

  • Đầu ra hiển thị: Flow1=try->ok:20->finally | Flow2=try->catch:Divisor cannot be zero->finally.
  • Ở cả hai nhánh thực thi, khối finally luôn là điểm chốt cuối cùng.

Cảnh báo an toàn: Tuyệt đối không viết catch (\Throwable $e) {} bỏ trống làm nuốt mất ngoại lệ mà không ghi nhật ký lỗi.

Tạo ngoại lệ miền và giữ nguyên nhân gốc

Việc tạo lớp ngoại lệ riêng cho từng phân hệ giúp tầng giao vận dễ dàng phân loại lỗi nào trả về HTTP 400/422, lỗi nào là sự cố hệ thống 500.

Bước 3: Tạo kịch bản chained_exception.php bọc ngoại lệ kèm $previous

Mục đích: Bắt lỗi tầng thấp và ném lại ngoại lệ miền đính kèm ngoại lệ gốc $e.

cat <<'EOF' > "$WORKDIR/chained_exception.php"
<?php
declare(strict_types=1);

final class ConfigLoadException extends RuntimeException {}

function loadPort(array $env): int
{
    try {
        $raw = $env['APP_PORT'] ?? throw new OutOfBoundsException('Missing APP_PORT');
        return (int) $raw;
    } catch (OutOfBoundsException $e) {
        throw new ConfigLoadException('Failed to initialize server configuration', 500, $e);
    }
}

try {
    loadPort([]);
} catch (ConfigLoadException $ex) {
    $prev = $ex->getPrevious();
    echo "Main={$ex->getMessage()} | RootCause=" . ($prev ? $prev::class . ':' . $prev->getMessage() : 'none') . "\n";
}
EOF
php "$WORKDIR/chained_exception.php"

Kết quả quan sát được:

  • Màn hình in ra: Main=Failed to initialize server configuration | RootCause=OutOfBoundsException:Missing APP_PORT. Lời gọi $ex->getPrevious() giữ trọn vẹn nguyên nhân gốc.

Cảnh báo an toàn: Trên Production, tuyệt đối không in $e->getTraceAsString() ra phản hồi HTTP vì Stack Trace làm lộ đường dẫn tệp máy chủ và tham số nhạy cảm.

Kích hoạt cảnh báo Deprecated và dọn dẹp

Hãy kiểm chứng cách PHP 8.4 phát thông báo cảnh báo chuẩn khi gọi vào hàm gắn nhãn #[\Deprecated].

Bước 4: Gọi hàm gắn #[\Deprecated] và kiểm tra thông điệp cảnh báo

Mục đích: Xác nhận PHP 8.4 tự động phát thông báo Deprecated chứa cả phiên bản since và hướng dẫn message.

php -r '
#[\Deprecated(message: "Use newApi()", since: "8.4")]
function oldApi(): void {}
oldApi();
'

Kết quả quan sát được:

  • Trình thông dịch in ra: Deprecated: Function oldApi() is deprecated since 8.4, Use newApi() in Command line code on line 4.
Cây phân cấp giao diện Throwable tách biệt lỗi nội tại (Error) và ngoại lệ (Exception).
Hình 2. Cây phân cấp giao diện Throwable tách biệt lỗi nội tại (Error) và ngoại lệ (Exception). Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

Bước 5: Dọn dẹp vùng làm việc tạm

Mục đích: Xóa thư mục thực hành trong /tmp.

rm -rf "$WORKDIR"

Tách thông báo lỗi và nhật ký trên production

Khi vận hành ứng dụng PHP 8.4 trên Production, cần thiết lập hai rào chắn:

  1. Tắt hiển thị lỗi ra màn hình (display_errors = Off): Bắt buộc đặt display_errors = Off và bật log_errors = On kèm error_log = /dev/stderr trong container để đẩy nhật ký lỗi về hệ thống giám sát tập trung.
  2. Đăng ký set_exception_handler() ở điểm vào ứng dụng: Bắt mọi Throwable chưa xử lý ở tầng ngoài cùng, ghi log định dạng JSON (chuẩn PSR-3) và trả mã thoát khác 0 (exit(1)) trong CLI.

Tự kiểm tra và giới hạn

Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:

  • Câu hỏi 1: Vì sao khối catch (Exception $e) không bắt được lỗi TypeError? -> Vì TypeError kế thừa từ lớp Error, không kế thừa từ Exception. Cả hai đều là nhánh con độc lập dưới Throwable.
  • Câu hỏi 2: Điều gì xảy ra nếu đặt lệnh return 'fallback'; bên trong khối finally khi khối try đang ném ngoại lệ? -> Ngoại lệ đang ném từ try sẽ bị nuốt mất hoàn toàn và hàm trả về 'fallback'. Tuyệt đối không đặt return trong finally.
  • Câu hỏi 3: Làm thế nào để ném lại ngoại lệ nghiệp vụ mới mà không làm mất Stack Trace gốc? -> Truyền ngoại lệ cũ $e vào tham số thứ ba ($previous): throw new DomainException('Msg', 0, $e);.
  • Câu hỏi 4: Thuộc tính #[\Deprecated] mới trong PHP 8.4 vượt trội hơn @deprecated trong PHPDoc ở điểm nào? -> Nó được chính máy ảo Zend Engine thực thi lúc chạy (phát cảnh báo E_USER_DEPRECATED tự động) và đọc được qua Reflection API.

Bạn đã nắm vững kiến trúc xử lý lỗi trên PHP 8.4. Hãy tiếp tục sang PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP để áp dụng vào thao tác đọc/ghi tệp và giải mã JSON.

Ôn bài trước: PHP EP 14: Namespace, PSR-4 và Composer trong PHP. Học tiếp: PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP.

Bài liên quan trong bộ PHP

Tìm hiểu thêm