
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.

Phân tích thứ tự ưu tiên toán tử trong PHP, thực hành đối chiếu so sánh lỏng == với so sánh chặt ===, kết hợp Null Coalescing ?? và tránh lỗi ép kiểu ngầm.
Trong quá trình rà soát lỗi logic của các ứng dụng PHP, có hai lỗi kinh điển xuất hiện lặp đi lặp lại ngay cả ở những dự án đã chạy lâu năm: lỗi thứ nhất là viết if (strpos($slug, "php") == true) để kiểm tra xem chuỗi $slug có bắt đầu bằng chữ "php" hay không, nhưng khi "php" nằm ở ngay vị trí đầu tiên (chỉ mục 0), biểu thức lại trả về false khiến chương trình bỏ sót dữ liệu. Lỗi thứ hai xảy ra khi lập trình viên dùng toán tử rút gọn $page = $_GET['page'] ?: 1; để gán trang mặc định, dẫn tới việc người dùng truyền ?page=0 lại bị ép nhảy sang trang 1. Cả hai sự cố đều xuất phát từ việc dùng nhầm toán tử so sánh lỏng và đánh đồng giá trị "0" / 0 với trạng thái rỗng.
Bài học thứ năm trong lộ trình PHP kế thừa trực tiếp kiến thức về các kiểu dữ liệu vô hướng và cơ chế ép kiểu ở PHP EP 4. Dựa trên tài liệu chính thức về Độ ưu tiên toán tử (Operator Precedence) và Toán tử so sánh (Comparison Operators) trên PHP 8.4, chúng ta sẽ phân tích sự khác biệt bản chất giữa == và ===, cách hoạt động của toán tử Spaceship <=>, sự khác nhau sống còn giữa Elvis ?: và Null Coalescing ??, cùng toán tử chuỗi an toàn Nullsafe ?->.
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 nghiệm trong bài được chạy 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, 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 toán tử biểu thức và so sánh trên dữ liệu trong bộ nhớ; chưa kết hợp xử lý truy vấn HTTP thực tế (sẽ học ở EP 17). Giới hạn kiểm chứng: CLI.
Từ phiên bản PHP 8.0 đến PHP 8.4, hành vi của phép so sánh lỏng == giữa số và chuỗi không phải số đã được sửa lại hợp lý hơn (ví dụ 0 == "abc" đã trả về false thay vì true như PHP 7), nhưng những bẫy ép kiểu với chuỗi số "0", false và null vẫn tồn tại nguyên vẹn. Bảng hợp đồng kỹ thuật dưới đây chỉ rõ hành vi và ranh giới an toàn của từng toán tử:
| Toán tử | Cú pháp & Đầu ra | Hành vi thực tế trên PHP 8.4 | Điều kiện / Không nên giả định |
|---|---|---|---|
| So sánh chặt (`===`, `!==`) | `$a === $b` trả về `bool`. | Chỉ trả về `true` khi `$a` và `$b` có **cùng kiểu dữ liệu** và **cùng giá trị**. | Đối với hai biến đối tượng (object), `===` kiểm tra xem cả hai có cùng trỏ tới một thực thể duy nhất trong bộ nhớ hay không. |
| So sánh lỏng (`==`, `!=`) | `$a == $b` trả về `bool`. | Thực hiện ép kiểu tự động trước khi so sánh: `0 == "0"`, `0 == false`, `"" == null` đều là `true`. | Không dùng `==` để kiểm tra kết quả của hàm có thể trả về cả số `0` lẫn `false` (như `strpos`, `array_search`). |
| Null Coalescing (`??`, `??=`) | `$a ?? $default` trả về giá trị đầu tiên khác `null`. | Kiểm tra ngầm giống `isset($a)`: nếu `$a` chưa khai báo hoặc bằng `null` thì lấy `$default`, không báo `Warning`. | Giữ nguyên các giá trị `0`, `"0"`, `""` và `false` ở vế trái vì chúng không phải là `null`. |
| Toán tử Elvis (`?:`) | `$a ?: $default` trả về `$a` nếu `bool($a)` là `true`. | Ép kiểu vế trái sang `bool`: nếu là falsy (`0`, `"0"`, `""`, `false`, `null`, `[]`) sẽ lấy `$default`. | Phát sinh `Warning: Undefined variable/array key` nếu vế trái chưa tồn tại, và làm mất giá trị `"0"` hợp lệ. |
Khi kết hợp nhiều điều kiện bằng toán tử logic, PHP cung cấp hai bộ ký hiệu có độ ưu tiên khác hẳn nhau: nhóm độ ưu tiên cao (&&, ||) và nhóm độ ưu tiên rất thấp bằng chữ (and, or). Đặc biệt, hai từ khóa and và or có độ ưu tiên thấp hơn cả dấu gán =. Điều đó có nghĩa nếu bạn viết $ok = $a and $b;, PHP sẽ thực hiện phép gán ($ok = $a) trước rồi mới tính and $b, làm biến $ok nhận kết quả sai lệch. Vì vậy, quy tắc bắt buộc trong mã nguồn chuẩn là luôn dùng && và ||, kết hợp dấu ngoặc đơn () khi biểu thức có từ hai toán tử trở lên.
Cả && và || đều hoạt động theo cơ chế đánh giá ngắn mạch (short-circuit evaluation): với $dk1 && $dk2, nếu $dk1 đã là false, PHP dừng ngay mà không gọi biểu thức $dk2. Tương tự, toán tử Nullsafe ($user?->getProfile()?->getEmail()) áp dụng nguyên lý ngắn mạch cho chuỗi lời gọi phương thức đối tượng: ngay khi bất kỳ mắt xích nào ở bên trái trả về null, toàn bộ biểu thức dừng lại và trả về null an toàn thay vì ném lỗi nghiêm trọng Call to a member function on null. Còn khi cần viết hàm sắp xếp mảng với usort(), toán tử Spaceship ($a <=> $b) giúp trả về ngay -1 (nhỏ hơn), 0 (bằng nhau) hoặc 1 (lớn hơn) chỉ trong một biểu thức gọn gàng.

Sơ đồ Hình 1 minh họa thứ tự kiểm tra biểu thức an toàn: đặt dấu ngoặc tường minh, dùng ?? cho các tham số có thể vắng mặt, dùng === để đối chiếu giá trị và tận dụng tính ngắn mạch của && / ||.
Hãy mở terminal trong container Lab, tạo thư mục tạm và khởi tạo phần đầu kịch bản operators_check.php để kiểm chứng sự khác biệt khi xử lý chuỗi "0":
lab_dir="$(mktemp -d /tmp/php-ep05-XXXXXX)"
cd "$lab_dir"
cat << 'EOF' > operators_check.php
<?php
declare(strict_types=1);
$rawPage = "0";
$looseZero = ($rawPage == false);
$strictZero = ($rawPage === false);
EOF
Ở đoạn mã trên, biến $rawPage mang giá trị chuỗi "0" (tình huống rất hay gặp khi người dùng truyền tham số ?page=0 hoặc ?offset=0 trên URL).
Tiếp theo, chúng ta nối phần kiểm tra ?:, ?? và strpos() vào operators_check.php rồi thực thi kịch bản:
cat << 'EOF' >> operators_check.php
$elvisPort = $rawPage ?: 8080;
$nullCoalescePort = $rawPage ?? 8080;
$pos = strpos("php-ep-05", "php");
$foundCorrectly = ($pos !== false);
printf(
"loose=%s | strict=%s | elvis=%s | coalesce=%s | strpos_ok=%s\n",
$looseZero ? 'true' : 'false',
$strictZero ? 'true' : 'false',
(string) $elvisPort,
(string) $nullCoalescePort,
$foundCorrectly ? 'true' : 'false'
);
EOF
php operators_check.php
Kết quả thực thi xuất ra dòng: loose=true | strict=false | elvis=8080 | coalesce=0 | strpos_ok=true. Hãy phân tích từng cặp giá trị quan sát được:
$rawPage = "0", phép so sánh lỏng $rawPage == false trả về true, trong khi $rawPage === false trả về false chính xác vì kiểu string khác kiểu bool.$rawPage ?: 8080, chuỗi "0" bị coi là false nên bị ghi đè mất thành 8080; ngược lại, toán tử $rawPage ?? 8080 giữ nguyên giá trị "0" vì $rawPage khác null.strpos("php-ep-05", "php"), từ khóa nằm ở đầu chuỗi nên hàm trả về số nguyên 0; nhờ kiểm tra $pos !== false (hoặc dùng hàm str_starts_with() trên PHP 8+), chúng ta nhận được kết quả đúng strpos_ok=true.Một ưu điểm cực kỳ quan trọng của toán tử ?? so với == hoặc ?: là khả năng đọc trực tiếp khóa mảng chưa tồn tại mà không phát sinh cảnh báo Undefined array key. Hãy chạy lệnh kiểm thử nhánh lỗi dưới đây khi cố tình so sánh trực tiếp một khóa mảng bị thiếu bằng ==:
set +e
php -r '$a = []; echo ($a["missing"] == "x") ? "yes\n" : "no\n";'
set -e
Trình thông dịch lập tức in ra cảnh báo trên luồng lỗi: Warning: Undefined array key "missing" in Command line code on line 1, trước khi in chữ no. Nếu thay bằng (($a["missing"] ?? "") === "x"), chương trình sẽ chạy sạch sẽ tuyệt đối không một dòng cảnh báo nào.

Hình 2 đặt ba cơ chế ==, === và ?? cạnh nhau để bạn nắm rõ ranh giới sử dụng. Sau khi hoàn tất bài kiểm chứng, hãy xóa sạch 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 05."
Trong mã nguồn thực tế trên PHP 8.4, bạn nên kết hợp ba kỹ thuật hiện đại sau để giảm thiểu lỗi toán tử:
??=): Thay vì viết $config['timeout'] = $config['timeout'] ?? 30;, bạn có thể viết gọn $config['timeout'] ??= 30;. Câu lệnh này chỉ gán 30 nếu khóa 'timeout' chưa tồn tại hoặc đang mang giá trị null.strpos() bằng str_contains(), str_starts_with(), str_ends_with(): Nếu mục đích của bạn chỉ là kiểm tra sự tồn tại của chuỗi con chứ không cần lấy vị trí chỉ mục số, hãy dùng 3 hàm này vì chúng luôn trả về kiểu bool (true hoặc false), triệt tiêu hoàn toàn nguy cơ nhầm lẫn giữa chỉ mục 0 và false.?? với toán tử ba ngôi ? :: Do hai toán tử này có độ ưu tiên gần nhau, khi viết lồng nhau bắt buộc phải bọc ngoặc đơn ($a ?? $b) ? $x : $y để tránh lỗi cú pháp Unparenthesized trên PHP 8+.Hãy tự kiểm tra lại kiến thức EP 5 qua 4 câu hỏi tình huống dưới đây:
0 == "abc" trên PHP 8.4 trả về true hay false?
Đáp án: Trả về false. Từ PHP 8.0 trở đi, khi so sánh số với chuỗi không phải là chuỗi số, PHP chuyển số thành chuỗi ("0" == "abc") nên kết quả là false (khác với hành vi cũ của PHP 7). Tuy nhiên 0 == "0" vẫn là true, nên vẫn phải ưu tiên dùng ===.$_GET có phần tử $_GET['limit'] = "0", biểu thức $_GET['limit'] ?: 20 trả về kết quả gì?
Đáp án: Trả về 20. Toán tử ?: ép kiểu "0" sang bool(false) nên bỏ qua giá trị "0" và lấy giá trị mặc định 20. Muốn giữ lại "0", phải dùng $_GET['limit'] ?? 20.and và or thay cho && và || trong câu lệnh gán?
Đáp án: Vì and và or có độ ưu tiên thấp hơn toán tử gán =. Viết $res = $x and $y sẽ gán $res = $x trước, dẫn tới sai lệch kết quả logic.$order?->customer?->name có chặn được cảnh báo nếu biến $order ở đầu chuỗi chưa từng được khai báo không?
Đáp án: Không. Toán tử ?-> chỉ xử lý an toàn khi biến $order đã tồn tại và mang giá trị null; nếu biến $order chưa từng được định nghĩa vẫn sẽ phát sinh Warning: Undefined variable $order.Tiêu chí nghiệm thu: Hoàn thành EP 5 khi phân biệt chính xác == với ===, ?: với ??, và hiểu rõ nguyên lý ngắn mạch. Ở bài tiếp theo (PHP EP 6), chúng ta sẽ vận dụng các phép so sánh này vào cấu trúc điều khiển rẽ nhánh if/else, switch và biểu thức match.
Ôn bài trước: PHP EP 4: Biến, hằng số và ép kiểu trong PHP. Học tiếp: PHP EP 6: Rẽ nhánh với if, switch và match.

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.