
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.

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.
Khi dự án mới có vài trăm dòng mã, lập trình viên thường kiểm tra tính năng bằng cách mở trình duyệt, nhập thử biểu mẫu, rồi chèn var_dump($x); die(); vào giữa hàm để xem giá trị biến. Tuy nhiên, khi ứng dụng tăng lên 20 lớp dịch vụ và cần sửa đổi một quy tắc tính toán ở tầng lõi, việc bấm tay lại toàn bộ màn hình là bất khả thi. Hệ quả là sửa xong lỗi ở phân hệ A lại vô tình làm hỏng phân hệ B (lỗi hồi quy - regression), hoặc quên xóa lệnh var_dump() trước khi đẩy mã lên Production. Mặt khác, nếu trộn lẫn mã HTML, đọc $_POST và tính toán nghiệp vụ vào chung một tệp, bạn không thể chạy kiểm thử tự động từ dòng lệnh CLI.
Bài học áp chót của khóa học hướng dẫn bạn tổ chức cấu trúc thư mục dự án PHP 8.4 theo kiến trúc phân lớp (tách biệt public/, src/ và tests/), áp dụng mô hình kiểm thử tự động Arrange - Act - Assert (AAA) kiểm chứng cả nhánh thành công lẫn nhánh ném ngoại lệ, và kỹ năng đọc ngược Stack Trace để định vị lỗi chính xác. Bài viết kế thừa kiến thức từ PHP EP 14: Namespace, PSR-4 và Composer trong PHP cùng PHP EP 20: Transaction PDO, khóa ngoại và Repository trong PHP, chuẩn bị cho bài tổng hợp tại PHP EP 22: Xây dựng ứng dụng Ticket bằng PHP và PDO. Tài liệu tham chiếu gồm PHP Manual: Assertions, Error Reporting và PHPUnit Documentation.
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) và Composer version 2.10.3 trên Alpine Linux 3.21 (aarch64).zend.assertions=1 và phân tích Stack Trace trong container cô lập; không tuyên bố kiểm chứng cấu hình trình gỡ lỗi bước Xdebug qua mạng TCP.Bảng dưới đây tổng hợp hợp đồng kỹ thuật của từng lớp thư mục và công cụ kiểm chứng trong dự án PHP 8.4:
| Thành phần / Công cụ | Đường dẫn / Cú pháp chuẩn | Vai trò & Hành vi thực tế trong PHP 8.4 | Điều kiện / Không nên giả định |
|---|---|---|---|
Thư mục public/ |
public/index.php (DocumentRoot duy nhất của Web Server) |
Chứa điểm vào HTTP duy nhất và tài sản tĩnh; nạp ../vendor/autoload.php rồi chuyển điều khiển vào src/. |
Tuyệt đối không đặt tệp .env, composer.json, src/ hay tệp .sqlite bên trong thư mục public/! |
Thư mục src/ |
Ánh xạ PSR-4 "App\\": "src/" |
Chứa các lớp Thực thể (Domain), Kho lưu trữ (Repository) và Dịch vụ (Service) độc lập giao diện. |
Các lớp trong src/ không được đọc trực tiếp $_GET, $_POST để có thể kiểm thử dễ dàng từ CLI. |
Thư mục tests/ |
Ánh xạ "Tests\\": "tests/" trong autoload-dev |
Chứa kịch bản kiểm thử đơn vị (Unit Test) và kiểm thử tích hợp (Integration Test với sqlite::memory:). |
Mỗi bài kiểm thử phải tự tạo dữ liệu mẫu riêng (isolated state), không phụ thuộc vào thứ tự chạy. |
Kiểm tra khẳng định (assert) |
php -d zend.assertions=1 -d assert.exception=1 |
Khi bật zend.assertions=1, biểu thức assert($actual === $expected, 'Msg') ném ngay AssertionError nếu sai. |
Trên Production, zend.assertions thường đặt là -1 (bỏ qua lúc biên dịch), nên không bao giờ dùng assert() để kiểm tra dữ liệu người dùng! |
| Phân tích Stack Trace | $e->getFile(), $e->getLine(), $e->getTrace() |
Liệt kê chuỗi lời gọi hàm từ điểm phát sinh lỗi (`#0`) lùi dần về điểm vào chương trình (`{main}`). | Khi gỡ lỗi, luôn đọc dòng thông báo lỗi và dòng #0 thuộc thư mục src/ trước tiên. |
Bí quyết để viết mã nguồn dễ kiểm thử nằm ở nguyên tắc Tiêm phụ thuộc (Dependency Injection) và tách biệt tính toán khỏi giao vận: thay vì để lớp dịch vụ tự khởi tạo kết nối cơ sở dữ liệu cứng bên trong hàm, bạn truyền đối tượng PDO (hoặc Repository) qua hàm tạo __construct(). Khi chạy kiểm thử tự động trên CI/CD, bạn chỉ cần truyền new PDO('sqlite::memory:') chạy hoàn toàn trong RAM.
Mỗi ca kiểm thử tự động luôn tuân theo cấu trúc 3 bước Arrange - Act - Assert (AAA):
SlaCalculator) và chuẩn bị dữ liệu đầu vào xác định.$hours = $calc->deadlineHours(1)) và thu nhận kết quả.assert($hours === 2)), đồng thời kiểm tra cả nhánh lỗi bằng try / catch để xác nhận phương thức ném đúng loại ngoại lệ như cam kết.
Chúng ta sẽ khởi tạo cấu trúc thư mục src/ và tests/, viết lớp nghiệp vụ App\SlaCalculator tính thời hạn xử lý sự cố theo mức độ nghiêm trọng.
Bước 1: Tạo vùng làm việc tạm và cấu trúc thư mục src/, tests/
Mục đích: Khởi tạo không gian dự án chuẩn trong /tmp.
WORKDIR="$(mktemp -d /tmp/php-ep21-XXXXXX)"
cd "$WORKDIR"
mkdir -p "$WORKDIR/src" "$WORKDIR/tests"
pwd
Bước 2: Tạo composer.json và lớp nghiệp vụ src/SlaCalculator.php
Mục đích: Khai báo autoloading PSR-4 và viết lớp tính cam kết thời gian phản hồi (SLA) sử dụng biểu thức match kết hợp throw của PHP 8.4.
cat <<'EOF' > "$WORKDIR/composer.json"
{
"name": "thanhnh/php-ep21-lab",
"autoload": { "psr-4": { "App\\": "src/" } }
}
EOF
cat <<'EOF' > "$WORKDIR/src/SlaCalculator.php"
<?php
declare(strict_types=1);
namespace App;
use InvalidArgumentException;
final class SlaCalculator
{
public function deadlineHours(int $severity): int
{
return match ($severity) {
1 => 2,
2 => 8,
3 => 24,
default => throw new InvalidArgumentException("Unsupported severity: {$severity}"),
};
}
}
EOF
composer dump-autoload --working-dir="$WORKDIR" --quiet
Sau khi có lớp nghiệp vụ trong src/, chúng ta viết kịch bản kiểm thử trong tests/run_tests.php tách biệt hoàn toàn để kiểm chứng tự động mọi nhánh logic.
Bước 3: Tạo và thực thi kịch bản kiểm thử tự động tests/run_tests.php
Mục đích: Kiểm chứng tự động 2 trường hợp hợp lệ (severity = 1, severity = 3) và 1 trường hợp ngoại lệ (severity = 99).
cat <<'EOF' > "$WORKDIR/tests/run_tests.php"
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\SlaCalculator;
$calc = new SlaCalculator();
assert($calc->deadlineHours(1) === 2, 'Severity 1 must be 2h');
assert($calc->deadlineHours(3) === 24, 'Severity 3 must be 24h');
$caught = false;
try {
$calc->deadlineHours(99);
} catch (InvalidArgumentException) {
$caught = true;
}
assert($caught === true, 'Invalid severity must throw');
echo "TestsPassed=3 | CaughtInvalid=true\n";
EOF
php -d zend.assertions=1 -d assert.exception=1 "$WORKDIR/tests/run_tests.php"
Kết quả quan sát được:
TestsPassed=3 | CaughtInvalid=true.catch (InvalidArgumentException) kiểm tra chính xác rằng truyền mức độ 99 vào deadlineHours() chắc chắn bị chặn bởi InvalidArgumentException.Cảnh báo an toàn: Khi chạy kiểm thử bằng assert(), luôn truyền cờ -d zend.assertions=1 -d assert.exception=1 trên CLI để đảm bảo mọi biểu thức assert() đều được kích hoạt.
Khi một bài kiểm thử thất bại (AssertionError) hoặc ứng dụng ném ngoại lệ chưa bắt, PHP xuất ra một Stack Trace (dấu vết ngăn xếp). Hãy thử cố tình làm sai một kỳ vọng để đọc cấu trúc của Stack Trace và kiểm chứng mã thoát CLI.
Bước 4: Kích hoạt lỗi AssertionError có kiểm soát và phân tích Stack Trace
Mục đích: Quan sát cách PHP 8.4 báo cáo chính xác tệp, số dòng, thông điệp khẳng định và trả về mã thoát 255 (giúp CI/CD chặn bản dựng lỗi).
set +e
php -d zend.assertions=1 -d assert.exception=1 -r '
require "'"$WORKDIR"'/vendor/autoload.php";
$calc = new App\SlaCalculator();
assert($calc->deadlineHours(2) === 999, "Expected 8h for severity 2");
'
RC=$?
set -e
echo "RC=$RC"
Kết quả quan sát được:
RC=255 kèm thông báo: Fatal error: Uncaught AssertionError: Expected 8h for severity 2 in Command line code:4.#0, #1 tiếp theo liệt kê ngược lại chuỗi hàm đã gọi dẫn tới điểm lỗi đó.
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"
Trong các dự án doanh nghiệp, quy trình kiểm định tự động gồm 3 tầng:
composer require --dev phpunit/phpunit để dùng $this->assertSame(), $this->expectException() và đo độ bao phủ mã (Code Coverage).stderr thay cho var_dump(): Dùng error_log(json_encode($context, JSON_UNESCAPED_UNICODE)) để in ra stderr mà không làm hỏng luồng JSON/HTML trả về trên stdout.Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:
public/ được cấu hình làm DocumentRoot của Web Server còn src/, vendor/, tests/ phải nằm bên ngoài public/? -> Để ngăn người dùng bên ngoài truy cập trực tiếp qua URL vào các tệp cấu hình, mã nguồn lớp hoặc tệp cơ sở dữ liệu nội bộ.assert($amount > 0) để kiểm tra dữ liệu đầu vào của người dùng trên Production? -> Vì trên Production zend.assertions thường đặt bằng -1 khiến câu lệnh assert() bị trình biên dịch bỏ qua hoàn toàn! Phải dùng if (...) { throw new InvalidArgumentException(...); }.Repository dùng PDO nhanh chóng mà không làm bẩn tệp DB thật? -> Truyền kết nối new PDO('sqlite::memory:') vào hàm tạo của Repository trong bài kiểm thử.Hoàn thành bài học này, bạn đã có đầy đủ tư duy tổ chức dự án và kiểm thử tự động. Hãy bước vào bài học cuối cùng PHP EP 22: Xây dựng ứng dụng Ticket bằng PHP và PDO để kết nối toàn bộ kiến thức thành một ứng dụng hoàn chỉnh.
Ôn bài trước: PHP EP 20: Transaction PDO, khóa ngoại và Repository trong PHP. Học tiếp: PHP EP 22: Xây dựng ứng dụng Ticket bằng PHP và PDO.

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.

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.

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.