
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.

So sánh cơ chế rẽ nhánh if/else, switch và biểu thức match trong PHP, thực hành xử lý nhánh mặc định, bắt lỗi UnhandledMatchError và tránh rơi nhánh ngoài ý.
Khi viết logic phân loại trạng thái đơn hàng hoặc mã phản hồi HTTP trong PHP, cấu trúc switch/case truyền thống tiềm ẩn hai rủi ro rất dễ lọt qua khâu đọc mã nguồn: thứ nhất, nếu lập trình viên vô tình quên viết từ khóa break; ở cuối một nhánh case, trình thông dịch sẽ tự động chạy tràn xuống câu lệnh của nhánh case kế tiếp (hiện tượng fall-through); thứ hai, switch sử dụng phép so sánh lỏng == để đối chiếu giá trị, khiến chuỗi "200" hay thậm chí "200_ok" trong các phiên bản cũ bị khớp nhầm với số nguyên 200. Để khắc phục triệt để những nhược điểm này, từ PHP 8.0 đến PHP 8.4, ngôn ngữ PHP cung cấp thêm biểu thức match.
Bài học thứ sáu trong lộ trình PHP kế thừa trực tiếp nguyên lý so sánh nghiêm ngặt === đã học ở PHP EP 5. Dựa trên tài liệu chính thức về Cấu trúc if/elseif/else, Câu lệnh switch và Biểu thức match trên PHP 8.4, chúng ta sẽ phân tích khi nào nên dùng if/else, sự khác biệt cơ chế giữa switch và match, cách gộp nhiều điều kiện trong một nhánh match và cách xử lý ngoại lệ UnhandledMatchError.
Hồ sơ kiểm chứng & Môi trường Lab: Ngày kiểm chứng: Các kịch bản thực hành trong bài được kiểm chứng ngày 26/10/2026 bên trong container Docker cô lập mạng (--network none), tệp gốc chỉ đọc (--read-only) kèm vùng tạm /tmp, chạy dưới tài khoản thường phplab (UID 1001) trên Alpine Linux (aarch64) với PHP 8.4.27 (cli) và Zend Engine v4.4.27. Bài viết tập trung vào cấu trúc rẽ nhánh trong bộ nhớ; việc kết hợp match với kiểu liệt kê Enum sẽ được mở rộng chi tiết ở EP 13. Giới hạn kiểm chứng: CLI cục bộ, chưa kiểm chứng production.
Mặc dù cả ba cấu trúc đều dùng để điều hướng luồng thực thi, nhưng trong ngữ pháp của PHP, if và switch là các câu lệnh điều khiển (statements) không tự trả về giá trị, còn match là một biểu thức (expression) luôn trả về một kết quả cụ thể để bạn gán trực tiếp vào biến hoặc return ngay từ hàm.
| Tiêu chí kỹ thuật | `if / elseif / else` | Câu lệnh `switch` | Biểu thức `match` (PHP 8.4) | Điều kiện / Không nên giả định |
|---|---|---|---|---|
| Cơ chế đối chiếu | Đánh giá biểu thức `bool` bất kỳ (`>`, `<`, `&&`, `===`). | So sánh lỏng (`==`) giá trị đầu vào với từng `case`. | So sánh nghiêm ngặt (`===`) cả kiểu dữ liệu lẫn giá trị. | Trong `match`, chuỗi `'200'` sẽ không bao giờ khớp với nhánh số nguyên `200`. |
| Hành vi chuyển nhánh | Chỉ chạy khối đầu tiên có điều kiện đúng. | Rơi xuống nhánh tiếp theo nếu thiếu `break;` hoặc `return;`. | Tự động kết thúc ngay sau khi tính xong nhánh khớp. | Không thể dùng hiệu ứng rơi nhánh (fall-through) giữa hai biểu thức khác nhau trong `match`. |
| Giá trị trả về | Không phải biểu thức; phải gán biến bên trong từng khối. | Không trả về giá trị trực tiếp. | Trả về giá trị của nhánh khớp để gán thẳng vào biến. | Mỗi vế phải của dấu `=>` trong `match` chỉ được chứa một biểu thức đơn, không chứa khối nhiều câu lệnh. |
| Khi không khớp nhánh nào | Bỏ qua im lặng nếu không viết khối `else`. | Bỏ qua im lặng nếu không viết nhánh `default:`. | Ném lỗi `UnhandledMatchError` nếu thiếu nhánh `default`. | Luôn đặt nhánh `default => ...` trong `match` khi đầu vào đến từ người dùng hoặc hệ thống ngoài. |
Điểm khác biệt quan trọng nhất của biểu thức match trên PHP 8.4 là tính vét cạn (exhaustiveness). Trong câu lệnh switch, nếu biến $status mang giá trị 503 mà bạn chỉ viết case 200: và case 404: (không có default:), chương trình sẽ lặng lẽ đi qua khối switch mà không gán giá trị gì, dẫn tới biến kết quả bị null hoặc chưa khởi tạo ở các dòng code phía dưới. Ngược lại, vì match là một biểu thức bắt buộc phải sinh ra giá trị trả về, nếu không có nhánh nào khớp với 503 và cũng không có nhánh default, Zend Engine sẽ lập tức ném ra đối tượng lỗi UnhandledMatchError (kế thừa từ lớp Error). Cơ chế này giúp phát hiện ngay các trạng thái bị bỏ quên ngay trong quá trình kiểm thử.
Đối với câu lệnh if/else, khi cần kiểm tra nhiều điều kiện tiền đề (như kiểm tra dữ liệu đầu vào rỗng, kiểm tra quyền truy cập), thay vì lồng nhiều tầng if (...) { if (...) { ... } } gây khó đọc, bạn nên áp dụng mẫu Guard Clauses (Điều kiện bảo vệ thoát sớm): kiểm tra các nhánh lỗi ở ngay đầu hàm và return hoặc throw ngay lập tức, để phần thân chính của hàm luôn nằm ở mức thụt lề phẳng nhất.

Sơ đồ Hình 1 giúp bạn chọn đúng công cụ rẽ nhánh: dùng if với Guard Clauses khi kiểm tra khoảng giá trị hoặc điều kiện phức hợp, và dùng match khi cần ánh xạ 1-1 từ một giá trị đầu vào sang một kết quả đầu ra.
Hãy mở terminal trong container Lab, tạo thư mục tạm và khởi tạo tệp branch_compare.php với hàm switch_status() sử dụng câu lệnh switch truyền thống:
lab_dir="$(mktemp -d /tmp/php-ep06-XXXXXX)"
cd "$lab_dir"
cat << 'EOF' > branch_compare.php
<?php
declare(strict_types=1);
function switch_status(int|string $code): string
{
switch ($code) {
case 200:
return 'OK_SWITCH';
default:
return 'OTHER_SWITCH';
}
}
EOF
Trong hàm switch_status(), tham số $code khai báo kiểu hợp int|string để cho phép nhận cả mã trạng thái dạng số nguyên lẫn dạng chuỗi ký tự.
Tiếp theo, chúng ta bổ sung hàm match_status() sử dụng biểu thức match của PHP 8.4 vào branch_compare.php và chạy đối chiếu kết quả:
cat << 'EOF' >> branch_compare.php
function match_status(int|string $code): string
{
return match ($code) {
200 => 'OK_MATCH',
400, 404 => 'CLIENT_ERR_MATCH',
default => 'OTHER_MATCH',
};
}
printf(
"switch('200')=%s | match('200')=%s | match(404)=%s\n",
switch_status('200'),
match_status('200'),
match_status(404)
);
EOF
php branch_compare.php
Kết quả thực thi xuất ra: switch('200')=OK_SWITCH | match('200')=OTHER_MATCH | match(404)=CLIENT_ERR_MATCH. Kết quả này chứng minh hai điểm kỹ thuật rõ rệt:
'200', hàm switch_status('200') dùng phép so sánh lỏng '200' == 200 nên vẫn nhảy vào nhánh case 200 và trả về OK_SWITCH. Ngược lại, hàm match_status('200') dùng phép so sánh chặt '200' === 200 (sai khác kiểu string và int), nên bỏ qua nhánh 200 và rơi vào nhánh default (OTHER_MATCH).400, 404 => 'CLIENT_ERR_MATCH' trong match cho phép gộp nhiều giá trị đầu vào phân tách bằng dấu phẩy để cùng trả về một biểu thức mà không cần viết lặp lại nhiều dòng case như switch.Bây giờ chúng ta thực hiện kiểm thử nhánh lỗi (Negative Testing) bằng cách truyền mã trạng thái 503 vào một biểu thức match không khai báo nhánh default:
set +e
php -r '
$status = 503;
$label = match ($status) {
200 => "OK",
404 => "Not Found",
};
'
echo "Mã thoát khi thiếu nhánh match: $?"
set -e
Trình thông dịch dừng ngay lập tức với mã thoát 255 và in ra thông báo lỗi:
Fatal error: Uncaught UnhandledMatchError: Unhandled match case 503 in Command line code:3.
Thông báo chỉ rõ giá trị 503 chưa được xử lý tại dòng 3. Trong thực tế, nếu muốn kiểm soát lỗi này êm thấm hoặc chuyển thành thông báo nghiệp vụ riêng, bạn luôn phải đặt nhánh default => ... ở cuối khối match (hoặc bọc trong khối try/catch (UnhandledMatchError $e) sẽ học ở EP 15).

Hình 2 đặt câu lệnh switch và biểu thức match cạnh nhau để làm nổi bật sự khác biệt về cơ chế so sánh và xử lý nhánh thiếu. Kết thúc bài Lab, hãy xóa thư mục tạm:
cd /tmp
rm -rf "$lab_dir"
test ! -e "$lab_dir" && echo "Đã dọn sạch thư mục Lab EP 06."
Khi áp dụng cấu trúc rẽ nhánh vào dự án thực tế trên PHP 8.4, có ba kinh nghiệm thiết kế quan trọng bạn nên nắm vững:
match (true): Thông thường match ($x) so sánh giá trị của $x với từng nhánh. Tuy nhiên, bạn có thể viết match (true) rồi đặt các biểu thức so sánh trả về bool ở vế trái mỗi nhánh (ví dụ: $score >= 80 => 'Giỏi', $score >= 65 => 'Khá', default => 'Đạt'). Lúc này match (true) sẽ tìm nhánh đầu tiên có giá trị === true, vừa giữ được sự ngắn gọn của match vừa kiểm tra được khoảng giá trị.elseif theo chuẩn PSR-12: Ngữ pháp PHP cho phép viết cả elseif (viết liền) lẫn else if (cách rời), nhưng chuẩn định dạng PSR-12 quy định luôn viết liền elseif để thống nhất trên toàn bộ mã nguồn.switch: Nếu mỗi nhánh rẽ cần thực thi một chuỗi nhiều câu lệnh độc lập có tác dụng phụ (như vừa ghi log, vừa gửi thông báo, vừa cập nhật nhiều biến) mà không nhằm mục đích trả về một giá trị duy nhất, câu lệnh if/elseif hoặc switch (hoặc tách từng nhánh thành hàm riêng rồi gọi từ match) sẽ phù hợp hơn.Hãy tự kiểm tra lại kiến thức EP 6 qua 4 câu hỏi tình huống dưới đây:
match ($val), cần viết câu lệnh break; ở cuối mỗi nhánh không?
Đáp án: Không. Mỗi nhánh trong match tự động kết thúc ngay sau khi tính xong biểu thức của nhánh đó; bản thân cú pháp match cũng không cho phép đặt câu lệnh break; bên trong.1 (kiểu int) vào match (true) { 1 => 'Một', default => 'Khác' }, kết quả trả về là gì?
Đáp án: Trả về 'Khác'. Vì match sử dụng so sánh nghiêm ngặt ===, biểu thức đầu vào là bool(true) không khớp kiểu với nhánh int(1), nên rơi vào nhánh default.401 và 403 cùng trả về một chuỗi 'Cấm truy cập' trong match?
Đáp án: Liệt kê các giá trị cách nhau bằng dấu phẩy ở vế trái dấu =>. Cụ thể: 401, 403 => 'Cấm truy cập',.match không khớp với bất kỳ nhánh nào và trong match không có nhánh default?
Đáp án: PHP ném ra lỗi UnhandledMatchError. Nếu không được bắt bằng try/catch, lỗi này sẽ làm dừng tiến trình với mã thoát khác 0.Tiêu chí nghiệm thu: Hoàn thành EP 6 khi phân biệt rõ cơ chế == của switch với === của match, sử dụng được cú pháp gộp nhánh và chủ động phòng tránh UnhandledMatchError. Sang PHP EP 7, chúng ta sẽ kết thúc Chặng 1 với các cấu trúc vòng lặp và kỹ thuật tiết kiệm bộ nhớ bằng Generator (yield).
Ôn bài trước: PHP EP 5: Toán tử PHP và so sánh nghiêm ngặt. Học tiếp: PHP EP 7: Vòng lặp, tham chiếu và Generator 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.