
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.

Thực hành thao tác Filesystem và luồng I/O trong PHP: tạo thư mục tạm an toàn, khóa tệp bằng flock, kiểm tra đường dẫn realpath và giải mã JSON bắt ngoại lệ.
Khi xây dựng tính năng lưu tệp cấu hình hoặc trao đổi dữ liệu JSON, hai lỗi phổ biến thường xuất hiện trên môi trường đa tiến trình: thứ nhất là hai request cùng ghi vào một tệp tại cùng thời điểm mà không có cơ chế khóa (flock), khiến nội dung tệp bị cắt cụt hoặc trộn lẫn làm hỏng cấu trúc; thứ hai là gọi json_decode($raw, true) ở chế độ mặc định mà không biết rằng hàm này chỉ âm thầm trả về null khi gặp chuỗi JSON sai cú pháp, dẫn tới việc ứng dụng hiểu nhầm dữ liệu đang rỗng.
Bài học này hướng dẫn bạn thao tác với hệ thống tệp (Filesystem) và luồng vào/ra (Streams I/O) an toàn trong PHP 8.4: kiểm chứng đường dẫn bằng realpath() chống tấn công vượt thư mục (Path Traversal), sử dụng fopen('cb+') kết hợp khóa độc quyền flock(LOCK_EX), và bật cờ JSON_THROW_ON_ERROR khi xử lý JSON. Bài viết kế thừa kiến thức từ PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch và chuẩn bị cho PHP EP 17: Xử lý form HTTP và mã hóa đầu ra trong PHP. Tài liệu tham chiếu gồm PHP Manual: Filesystem Functions, Streams và JSON Functions.
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.flock và xử lý JSON trên hệ thống tệp POSIX (tmpfs); không tuyên bố kiểm chứng khóa tệp qua mạng NFS/SMB.Bảng dưới đây tổng hợp hợp đồng kỹ thuật của các hàm thao tác I/O và JSON cốt lõi:
| Nhóm thao tác | Hàm / 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 |
|---|---|---|---|
| Kiểm tra đường dẫn thực | realpath($path) + str_starts_with() |
Phân giải toàn bộ ký hiệu ../, ./ và symlink thành đường dẫn tuyệt đối chính tắc; trả về false nếu đích chưa tồn tại. |
Phải kiểm tra đường dẫn trả về từ realpath() có bắt đầu bằng thư mục gốc cho phép hay không trước khi mở tệp. |
| Mở và khóa tệp an toàn | fopen($p, 'cb+') + flock($fp, LOCK_EX) |
Chế độ 'c+' tạo tệp nếu chưa có mà không xóa trắng tệp ngay lúc mở, chờ lấy khóa LOCK_EX xong mới gọi ftruncate($fp, 0). |
Nếu mở bằng fopen($p, 'w'), tệp sẽ bị cắt về 0 byte ngay lập tức trước cả khi kịp gọi lệnh chờ khóa flock()! |
| Mã hóa JSON | json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) |
Chuyển mảng/đối tượng sang chuỗi JSON, giữ nguyên chữ tiếng Việt UTF-8 và ném JsonException nếu dữ liệu lỗi. |
Thiếu JSON_THROW_ON_ERROR, hàm chỉ trả về false khi gặp lỗi truyền resource hoặc chuỗi UTF-8 hỏng. |
| Giải mã JSON | json_decode($json, true, 512, JSON_THROW_ON_ERROR) |
Phân tích chuỗi JSON thành mảng kết hợp (associative = true) và ném JsonException nếu sai cú pháp. |
Ở chế độ mặc định không có cờ, json_decode('{bad') trả về null giống hệt chuỗi hợp lệ 'null'. |
Một bẫy kỹ thuật rất sâu trong lập trình hệ thống với PHP nằm ở chế độ mở tệp của fopen() (PHP Manual: fopen). Nếu mở tệp bằng fopen($file, 'w') rồi mới gọi flock($fp, LOCK_EX), chế độ 'w' sẽ ra lệnh cho hệ điều hành cắt độ dài tệp về 0 byte ngay tại thời điểm mở tệp, tức là trước khi lệnh flock() kịp chạy! Nếu tiến trình khác đang giữ khóa và ghi dở, lệnh fopen('w') sẽ xóa sạch dữ liệu ngay lập tức.
Quy trình chuẩn mực để cập nhật tệp có khóa độc quyền gồm 4 bước:
'cb+' (tạo tệp nếu chưa có, mở đọc/ghi, không cắt tệp về 0 byte lúc mở).flock($fp, LOCK_EX).ftruncate($fp, 0) và rewind($fp) để xóa nội dung cũ, ghi nội dung mới bằng fwrite($fp, $data) và fflush($fp).flock($fp, LOCK_UN) và đóng fclose($fp).Với JSON, cờ JSON_THROW_ON_ERROR biến mọi lỗi cú pháp hoặc chuỗi UTF-8 hỏng thành ngoại lệ JsonException, giúp bắt gọn trong khối try / catch.

Chúng ta sẽ viết kịch bản kiểm tra đường dẫn bằng realpath(), mở tệp với fopen('cb+') + flock(LOCK_EX), ghi dữ liệu tiếng Việt bằng json_encode() và đọc lại bằng json_decode() kèm JSON_THROW_ON_ERROR.
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-ep16-XXXXXX)"
cd "$WORKDIR"
pwd
Bước 2: Tạo kịch bản storage_io.php ghi và đọc JSON với khóa flock
Mục đích: Thực hiện chu trình khóa tệp, cắt tệp, ghi chuỗi JSON tiếng Việt và giải mã lại an toàn.
cat <<'EOF' > "$WORKDIR/storage_io.php"
<?php
declare(strict_types=1);
$baseDir = realpath(__DIR__);
$file = $baseDir . '/config.json';
$fp = fopen($file, 'cb+');
if ($fp === false || !flock($fp, LOCK_EX)) {
throw new RuntimeException('Cannot lock file');
}
$payload = ['app' => 'TicketHub', 'utf8' => 'Đạt chuẩn', 'active' => true];
$json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, $json);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
$decoded = json_decode((string) file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
echo "App={$decoded['app']} | UTF8={$decoded['utf8']} | Active=" . ($decoded['active'] ? 'true' : 'false') . "\n";
EOF
php "$WORKDIR/storage_io.php"
Kết quả quan sát được:
App=TicketHub | UTF8=Đạt chuẩn | Active=true.JSON_UNESCAPED_UNICODE, chuỗi 'Đạt chuẩn' được lưu nguyên bản UTF-8 trong tệp config.json.Cảnh báo an toàn: Không truyền trực tiếp tên tệp do người dùng nhập vào fopen(). Hãy lọc qua basename($userInput) hoặc kiểm tra str_starts_with(realpath($target), $allowedDir . DIRECTORY_SEPARATOR) để chống Path Traversal.
Khi phân tích tệp nhật ký (log) hàng trăm megabyte, gọi file_get_contents() sẽ nạp toàn bộ tệp vào RAM và gây lỗi Allowed memory size exhausted. Giải pháp chuẩn là đọc cuốn chiếu từng dòng bằng SplFileObject.
Bước 3: Tạo kịch bản stream_lines.php đọc tệp theo luồng từng dòng
Mục đích: Dùng SplFileObject kết hợp cờ DROP_NEW_LINE | SKIP_EMPTY | READ_AHEAD để duyệt từng dòng với mức tiêu thụ RAM cố định.
cat <<'EOF' > "$WORKDIR/stream_lines.php"
<?php
declare(strict_types=1);
$logPath = __DIR__ . '/access.log';
file_put_contents($logPath, "200 /health\n\n500 /api/orders\n200 /docs\n", LOCK_EX);
$fileObj = new SplFileObject($logPath, 'rb');
$fileObj->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);
$errors = 0;
foreach ($fileObj as $line) {
if (is_string($line) && str_starts_with($line, '500 ')) {
$errors++;
}
}
echo "ErrorLines={$errors}\n";
EOF
php "$WORKDIR/stream_lines.php"
Kết quả quan sát được:
ErrorLines=1. Tại mỗi bước lặp, PHP chỉ giữ đúng 1 dòng văn bản trong RAM và bỏ qua dòng trống.Cảnh báo an toàn: Khi bật SplFileObject::SKIP_EMPTY, bắt buộc phải bật kèm SplFileObject::READ_AHEAD để vòng lặp foreach bỏ qua đúng các dòng trống.
Chúng ta sẽ kiểm chứng cách cờ JSON_THROW_ON_ERROR chặn đứng một chuỗi JSON bị cắt cụt cú pháp ngay tại biên giải mã.
Bước 4: Gọi json_decode với chuỗi hỏng kèm JSON_THROW_ON_ERROR và kiểm tra mã thoát
Mục đích: Xác nhận PHP 8.4 ném ngoại lệ JsonException tường minh thay vì trả về null.
set +e
php -r 'json_decode("{bad_json", true, 512, JSON_THROW_ON_ERROR);'
RC=$?
set -e
echo "RC=$RC"
Kết quả quan sát được:
RC=255 kèm thông báo: Fatal error: Uncaught JsonException: Syntax error in Command line code:1.
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"
Trên môi trường Production tải cao, hãy áp dụng hai nguyên tắc:
rename): Ghi toàn bộ nội dung mới ra tệp tạm cùng thư mục ($tmp = $file . '.tmp.' . getmypid()), đóng tệp rồi gọi rename($tmp, $file). Lệnh rename() trên cùng phân vùng POSIX là thao tác nguyên tử nên tiến trình đọc không bao giờ nhìn thấy tệp đang ghi dở.Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:
fopen($path, 'w') rồi mới gọi flock($fp, LOCK_EX) lại làm mất dữ liệu khi chạy song song? -> Vì chế độ 'w' cắt tệp về 0 byte ngay lúc mở, trước khi lệnh flock() kịp chạy. Phải dùng 'cb+', lấy khóa xong mới gọi ftruncate($fp, 0).JSON_THROW_ON_ERROR, hàm json_decode('{invalid', true) sẽ trả về gì? -> Âm thầm trả về null, dễ bị nhầm với giá trị JSON hợp lệ "null".../../etc/passwd) khi mở tệp? -> Dùng basename() hoặc phân giải bằng realpath() rồi kiểm tra str_starts_with($realPath, $allowedDir . DIRECTORY_SEPARATOR).JSON_UNESCAPED_UNICODE trong json_encode() có tác dụng gì? -> Giữ nguyên ký tự tiếng Việt UTF-8 trong chuỗi JSON đầu ra thay vì mã hóa thành \uXXXX.Hoàn thành bài học này, bạn đã nắm vững kỹ thuật thao tác Filesystem và JSON an toàn. Hãy tiếp tục sang PHP EP 17: Xử lý form HTTP và mã hóa đầu ra trong PHP để bước vào lập trình ứng dụng Web HTTP.
Ôn bài trước: PHP EP 15: Xử lý ngoại lệ PHP với Throwable và try/catch. Học tiếp: PHP EP 17: Xử lý form HTTP và mã hóa đầu ra 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.