
PHP EP 20: Transaction PDO, khóa ngoại và Repository trong PHP
Quản lý giao dịch cơ sở dữ liệu bằng beginTransaction, commit và rollBack trong PHP PDO, thực hành bật ràng buộc khóa ngoại và đóng gói truy vấn vào Repository.

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.
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):
php-course-qa:8.4 (--network none, --read-only, --tmpfs /tmp:rw,exec,nosuid,size=64m, user phplab UID 1001).PHP 8.4.27 (cli) trên Alpine Linux 3.21 (aarch64), Zend Engine v4.4.27.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.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. |
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.

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:
Flow1=try->ok:20->finally | Flow2=try->catch:Divisor cannot be zero->finally.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.
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:
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.
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:
Deprecated: Function oldApi() is deprecated since 8.4, Use newApi() in Command line code on line 4.
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"
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:
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.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.Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:
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.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.$e vào tham số thứ ba ($previous): throw new DomainException('Msg', 0, $e);.#[\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.

Quản lý giao dịch cơ sở dữ liệu bằng beginTransaction, commit và rollBack trong PHP PDO, thực hành bật ràng buộc khóa ngoại và đóng gói truy vấn vào Repository.

Thiết kế cấu trúc thư mục dự án PHP tách biệt nghiệp vụ và giao diện, thực hành viết kịch bản kiểm thử tự động xác minh nhánh lỗi và đọc Stack Trace để gỡ lỗi.

Tổng hợp kiến thức toàn khóa qua dự án ứng dụng quản lý Ticket bằng PHP và PDO: kết hợp Property Hooks, Enum, Transaction, bảo mật biểu mẫu HTTP và bộ kiểm thử.

Phân tích vòng đời HTTP Request/Response trong PHP qua $_GET, $_POST, $_SERVER, thực hành kiểm tra dữ liệu bằng filter_var và mã hóa đầu ra chống tấn công XSS.