
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 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.
Khi chuyển từ kịch bản CLI sang ứng dụng Web HTTP, toàn bộ dữ liệu đi vào chương trình qua các biến siêu toàn cục ($_GET, $_POST, $_COOKIE, $_SERVER) đều nằm dưới quyền kiểm soát của máy khách. Kẻ tấn công có thể truyền một chuỗi chữ vào trường số lượng, truyền mảng ?id[]=1 vào tham số kỳ vọng chuỗi đơn, giả mạo header HTTP_X_FORWARDED_FOR, hoặc chèn đoạn mã <script>alert("xss")</script> vào ô ghi chú biểu mẫu. Nếu ứng dụng nối thẳng chuỗi đó vào trang HTML trả về, trình duyệt của người dùng khác sẽ tự động thực thi mã độc (Cross-Site Scripting - XSS).
Bài học này giúp bạn nắm vững vòng đời xử lý yêu cầu HTTP trong PHP 8.4, phân biệt các biến siêu toàn cục, xây dựng lớp xác thực đầu vào (Input Validation) bằng filter_var(), và áp dụng mã hóa đầu ra (Output Escaping) bằng htmlspecialchars() để chặn đứng lỗ hổng XSS. Bài viết nối tiếp PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP và chuẩn bị cho PHP EP 18: Cookie, Session, CSRF và băm mật khẩu trong PHP. Tài liệu tham chiếu gồm PHP Manual: Superglobals, Data Filtering và htmlspecialchars.
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, phần mở rộng filter biên dịch sẵn.filter_var() và htmlspecialchars() với dữ liệu biểu mẫu mô phỏng trong container cô lập; không tuyên bố kiểm chứng chính sách Content-Security-Policy (CSP) hay WAF.Bảng dưới đây tổng hợp hợp đồng kỹ thuật của các công cụ xử lý HTTP trong PHP 8.4:
| Thành phần | Cú pháp / Cờ hiệu chuẩn | Hành vi thực tế trong PHP 8.4 | Điều kiện / Không nên giả định |
|---|---|---|---|
$_GET và $_POST |
Đọc kèm toán tử ?? và kiểm tra is_string() |
Chứa dữ liệu từ query string URL hoặc thân biểu mẫu; mọi giá trị lá đều là string. |
Nếu client gửi ?page[]=1, giá trị $_GET['page'] sẽ là array chứ không phải string (gọi trim() sẽ gây TypeError!). |
php://input (Raw Body) |
file_get_contents('php://input') |
Đọc thân request nguyên bản khi client gửi dữ liệu dạng application/json (vì $_POST rỗng với JSON). |
Luôn giải mã bằng json_decode(..., true, 512, JSON_THROW_ON_ERROR). |
$_SERVER |
$_SERVER['REQUEST_METHOD'], $_SERVER['REMOTE_ADDR'] |
Chứa thông tin môi trường SAPI và các header HTTP (khóa có tiền tố HTTP_*). |
Mọi khóa bắt đầu bằng HTTP_* đều do client gửi lên và có thể bị giả mạo. |
Xác thực đầu vào (filter_var) |
filter_var($v, FILTER_VALIDATE_INT, ['options' => ...]) |
Trả về giá trị đã chuyển đổi đúng kiểu (int, string...) nếu hợp lệ, hoặc trả về false nếu thất bại. |
Phải dùng so sánh chặt === false để kiểm tra lỗi (vì số nguyên hợp lệ 0 nếu dùng !$val sẽ bị coi nhầm là lỗi). |
| Mã hóa đầu ra HTML | htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') |
Biến các ký tự & < > " ' thành thực thể HTML an toàn (`<`, `"`...) và thay byte UTF-8 lỗi. |
Chỉ bảo vệ khi đặt vào nội dung thẻ hoặc thuộc tính có dấu nháy; không tự bảo vệ nếu chèn vào URL javascript:. |
Một sai lầm kiến trúc phổ biến là gọi htmlspecialchars($_POST['title']) ngay trước khi lưu vào cơ sở dữ liệu. Cách làm này làm biến dạng dữ liệu gốc trong DB (chữ A & B bị lưu thành A & B, gây sai lệch độ dài chuỗi khi tìm kiếm hoặc khi xuất ra JSON API/Mobile App).
Chuẩn bảo mật ứng dụng web hiện đại tách bạch hai lớp phòng thủ tại hai biên:
is_string()), cắt khoảng trắng bằng mb_trim(), và xác thực định dạng/miền giá trị bằng filter_var() (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT kèm min_range/max_range). Nếu dữ liệu sai quy tắc, từ chối ngay với mã HTTP 422. Nếu hợp lệ, giữ nguyên văn bản gốc sạch cho tầng nghiệp vụ.htmlspecialchars($val, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). Cờ ENT_QUOTES mã hóa cả dấu nháy kép " lẫn dấu nháy đơn ', ngăn kẻ tấn công thoát khỏi thuộc tính HTML value='...'.
Chúng ta sẽ viết hàm xử lý dữ liệu biểu mẫu tiếp nhận email, mức độ ưu tiên (1..5) và ghi chú có chứa thẻ <script> độc hại, xác thực bằng filter_var() và mã hóa đầu ra bằng htmlspecialchars().
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 riêng trong /tmp.
WORKDIR="$(mktemp -d /tmp/php-ep17-XXXXXX)"
cd "$WORKDIR"
pwd
Bước 2: Tạo kịch bản http_filter_xss.php xác thực đầu vào và thoát mã XSS
Mục đích: Kiểm tra email bằng FILTER_VALIDATE_EMAIL, kiểm tra số nguyên 1..5 bằng FILTER_VALIDATE_INT và vô hiệu hóa thẻ <script> bằng htmlspecialchars().
cat <<'EOF' > "$WORKDIR/http_filter_xss.php"
<?php
declare(strict_types=1);
function sanitizeForm(array $post): array
{
$email = filter_var(trim((string) ($post['email'] ?? '')), FILTER_VALIDATE_EMAIL);
$priority = filter_var($post['priority'] ?? null, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 5],
]);
if ($email === false || $priority === false) {
throw new InvalidArgumentException('Invalid form input');
}
$safeNote = htmlspecialchars((string) ($post['note'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return ['email' => $email, 'priority' => $priority, 'safeNote' => $safeNote];
}
$res = sanitizeForm([
'email' => ' support@thanhnh.id.vn ',
'priority' => '3',
'note' => '<script>alert("xss")</script>',
]);
echo "Email={$res['email']} | Prio={$res['priority']} | Escaped={$res['safeNote']}\n";
EOF
php "$WORKDIR/http_filter_xss.php"
Kết quả quan sát được:
Email=support@thanhnh.id.vn | Prio=3 | Escaped=<script>alert("xss")</script>.'3' được chuyển thành số nguyên int(3), và đoạn mã <script> bị biến thành văn bản hiển thị vô hại.Cảnh báo an toàn: Hằng số FILTER_SANITIZE_STRING cũ đã bị loại bỏ từ PHP 8.1+. Hãy luôn dùng FILTER_VALIDATE_* ở đầu vào và htmlspecialchars() ở đầu ra HTML.
Khi ứng dụng bật strict_types=1, nếu kẻ tấn công đổi URL từ ?q=php thành ?q[]=php, $_GET['q'] sẽ trở thành array và làm hàm mb_trim(string $s) ném TypeError.
Bước 3: Tạo kịch bản scalar_param_guard.php đọc tham số chuỗi đơn an toàn
Mục đích: Viết hàm readStringParam() chỉ chấp nhận chuỗi đơn và từ chối ngay nếu client gửi mảng.
cat <<'EOF' > "$WORKDIR/scalar_param_guard.php"
<?php
declare(strict_types=1);
function readStringParam(array $source, string $key, string $default = ''): string
{
$val = $source[$key] ?? $default;
if (!is_string($val)) {
throw new InvalidArgumentException("Parameter '{$key}' must be a scalar string.");
}
return mb_trim($val);
}
echo "Normal=" . readStringParam(['q' => ' php 8.4 '], 'q') . "\n";
EOF
php "$WORKDIR/scalar_param_guard.php"
Kết quả quan sát được:
Normal=php 8.4. Nếu client gửi mảng, hàm chủ động ném InvalidArgumentException để trả về HTTP 400 thay vì lỗi hệ thống 500.Cảnh báo an toàn: Khi đặt URL của người dùng vào <a href="...">, ngoài htmlspecialchars(), bạn phải kiểm tra URL bắt đầu bằng https:// hoặc http:// để chặn giao thức javascript:alert(1).
Hãy kiểm chứng cách hàm sanitizeForm() chặn đứng dữ liệu sai định dạng email và mức ưu tiên vượt ngưỡng 1..5.
Bước 4: Truyền dữ liệu biểu mẫu sai quy tắc vào sanitizeForm() và kiểm tra mã thoát
Mục đích: Xác nhận filter_var() trả về false và kích hoạt InvalidArgumentException.
set +e
php -r '
require "'"$WORKDIR"'/http_filter_xss.php";
sanitizeForm(["email" => "bad-email", "priority" => "9"]);
'
RC=$?
set -e
echo "RC=$RC"
Kết quả quan sát được:
RC=255 do ngoại lệ InvalidArgumentException: Invalid form input được ném ra ngay tại bước kiểm tra đầu vào.
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 triển khai ứng dụng xử lý biểu mẫu trên Production, cần bổ sung hai lớp bảo vệ:
$_FILES) an toàn: Không tin tưởng $_FILES['f']['type']. Hãy kiểm tra UPLOAD_ERR_OK, dùng finfo_file() đọc chữ ký MIME thực tế, đổi tên tệp thành chuỗi ngẫu nhiên (bin2hex(random_bytes(16))) và lưu ngoài thư mục chạy mã PHP.X-Content-Type-Options: nosniff, X-Frame-Options: DENY và Content-Security-Policy.Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:
htmlspecialchars() ngay trước khi lưu vào cơ sở dữ liệu? -> Vì nó làm biến dạng dữ liệu gốc (sai lệch độ dài chuỗi và làm hỏng kết quả khi xuất ra JSON API). Chỉ gọi htmlspecialchars() ngay tại biên hiển thị ra HTML.filter_var($input, FILTER_VALIDATE_INT) cho số nguyên có thể bằng 0, vì sao viết if (!filter_var(...)) lại sai? -> Vì số 0 hợp lệ khi ép kiểu boolean bằng ! sẽ bị coi nhầm là false (lỗi). Phải dùng === false.ENT_QUOTES trong htmlspecialchars() bảo vệ thêm kịch bản nào? -> Mã hóa cả dấu nháy đơn ' bên cạnh dấu nháy kép ", ngăn thoát khỏi thuộc tính <input value='...'>.$_SERVER['HTTP_X_FORWARDED_FOR'] có đáng tin cậy tuyệt đối không? -> Không. Mọi khóa HTTP_* đều do client gửi lên và có thể bị giả mạo nếu không qua Reverse Proxy tin cậy lọc lại.Hoàn thành bài học này, bạn đã biết cách tiếp nhận dữ liệu HTTP và chống XSS chuẩn mực. Hãy tiếp tục sang PHP EP 18: Cookie, Session, CSRF và băm mật khẩu trong PHP để bảo mật phiên đăng nhập và mật khẩu.
Ôn bài trước: PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP. Học tiếp: PHP EP 18: Cookie, Session, CSRF và băm mật khẩu 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ử.

Thiết lập Cookie và Session an toàn trong PHP với HttpOnly, SameSite, thực hành cấp mới session ID, kiểm tra CSRF token và băm mật khẩu bằng password_hash.