
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ế chữ ký hàm an toàn trong PHP với Union/Intersection Types và Named Arguments, thực hành viết Closure, Arrow Function và kiểm soát phạm vi biến cục bộ.
Khi đọc mã nguồn của các dự án PHP cũ, bạn rất dễ bắt gặp những lời gọi hàm kiểu như createReport('monthly', true, false, null, true). Người đọc không thể biết chữ true thứ nhất và chữ false thứ hai bật tắt tính năng gì nếu không mở lại định nghĩa hàm, và chỉ cần truyền lệch một vị trí là hành vi nghiệp vụ sai lệch hoàn toàn. Một tình huống phổ biến khác xảy ra khi viết hàm ẩn danh (Closure) truyền vào bộ lọc mảng: lập trình viên khai báo biến $taxRate = 0.1; ngay dòng trên, nhưng bên trong function ($price) { return $price * $taxRate; } lại báo lỗi Undefined variable $taxRate vì quy tắc phạm vi biến cục bộ của PHP khác với JavaScript hay Python.
Bài học thứ tám trong lộ trình PHP mở đầu Chặng 2 (Hàm hiện đại, Xử lý chuỗi UTF-8 & Mảng) và kế thừa nền tảng kiểm tra kiểu từ PHP EP 7. Dựa trên tài liệu chính thức về Hàm do người dùng định nghĩa, Đối số định danh (Named Arguments), Hàm ẩn danh (Anonymous functions) và Hàm mũi tên (Arrow Functions) trên PHP 8.4, chúng ta sẽ làm chủ cách thiết kế chữ ký hàm chặt chẽ với Union/Intersection Types, truyền đối số theo tên, và phân biệt cơ chế bắt biến giữa function () use (...) với fn() =>.
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. Giới hạn kiểm chứng: CLI.
Trong PHP hiện đại, chữ ký của một hàm (Function Signature) đóng vai trò là bản hợp đồng tự kiểm chứng giữa người viết hàm và người gọi hàm. Bảng đối chiếu dưới đây tổng hợp các cơ chế cốt lõi khi định nghĩa và gọi hàm trên PHP 8.4:
| Cơ chế / Cú pháp | Đầu vào & Đầu ra chính | Hành vi thực tế trên PHP 8.4 | Điều kiện / Không nên giả định |
|---|---|---|---|
| Union & Nullable Types (`int|float`, `?string`) | Đầu vào: Nhiều kiểu phân tách bởi dấu `|` hoặc `?Type` (tương đương `Type|null`). | Cho phép tham số hoặc giá trị trả về thuộc một trong các kiểu đã liệt kê một cách hợp lệ dưới `strict_types=1`. | Kiểu `void` (không trả về giá trị) và `never` (luôn ném lỗi hoặc `exit`) chỉ được dùng ở vị trí kiểu trả về, không dùng trong Union Type. |
| Named Arguments (`name: $val`) | Đầu vào: Tên tham số (không có dấu `$`) kèm dấu hai chấm. | Truyền đối số dựa theo tên tham số định nghĩa trong hàm, cho phép bỏ qua các tham số có giá trị mặc định đứng ở giữa. | Tên biến tham số trong hàm trở thành một phần của API; nếu truyền trùng cả theo vị trí lẫn theo tên sẽ ném lỗi `Error`. |
| Closure (`function () use ($x)`) | Đầu vào: Khối lệnh `{ ... }` và danh sách biến `use`. Đầu ra: Đối tượng lớp `Closure`. | Mặc định không nhìn thấy biến ở phạm vi hàm bao ngoài; phải chỉ định qua `use ($x)` (bản sao) hoặc `use (&$x)` (tham chiếu). | Nếu bắt biến theo bản sao `use ($x)`, giá trị `$x` được chốt tại thời điểm **khởi tạo** Closure, không phải lúc gọi. |
| Arrow Function (`fn($a) => $a * $x`) | Đầu vào: Tham số và một biểu thức đơn sau `=>`. Đầu ra: Đối tượng lớp `Closure`. | Tự động bắt các biến từ phạm vi bao ngoài theo **bản sao giá trị (by-value)** mà không cần viết từ khóa `use`. | Không thể thay đổi biến ở phạm vi ngoài từ bên trong `fn() =>` và không viết được khối nhiều câu lệnh `{}`. |
Khác với nhiều ngôn ngữ kịch bản cho phép hàm con tự động đọc mọi biến toàn cục hoặc biến của hàm cha, PHP thiết kế phạm vi biến hàm cô lập (isolated function scope): một hàm thông thường chỉ nhìn thấy các tham số được truyền vào chính nó và các biến khởi tạo bên trong nó. Thiết kế này giúp người đọc nhìn vào chữ ký hàm là biết ngay toàn bộ dữ liệu đầu vào mà không sợ hàm âm thầm phụ thuộc vào một biến toàn cục ở tệp khác.
Khi làm việc với hàm ẩn danh (được biểu diễn dưới dạng thực thể của lớp Closure dựng sẵn), PHP cung cấp hai cú pháp phục vụ hai nhu cầu khác nhau:
function (...) use (...) { ... }: Phù hợp khi thân hàm ẩn danh có nhiều bước xử lý hoặc когда bạn cần cập nhật ngược lại một biến đếm ở phạm vi bên ngoài thông qua tham chiếu use (&$counter). Thêm từ khóa static phía trước (static function) khi hàm ẩn danh không dùng tới con trỏ đối tượng $this sẽ giúp tiết kiệm bộ nhớ vì Zend Engine không cần gắn kết ngữ cảnh đối tượng vào Closure.fn(...) => biểu_thức: Ra đời để viết các hàm gọi lại (callback) ngắn gọn cho array_find(), array_map(), array_filter(). Hàm mũi tên tự động đọc biến từ phạm vi ngoài theo bản sao giá trị, vừa gọn gàng vừa đảm bảo không vô tình làm biến đổi trạng thái bên ngoài.
Sơ đồ Hình 1 minh họa 4 bước thiết kế hàm theo chuẩn PHP 8.4: xác định kiểu tham số (kể cả Union Types), đặt các tham số mặc định về cuối để hỗ trợ Named Arguments, khóa chặt kiểu trả về và kiểm soát biến ngoại vi khi dùng Closure.
Hãy mở terminal trong container Lab, tạo thư mục tạm và khởi tạo tệp functions_modern.php với hàm format_metric() dùng Union Type int|float và gọi qua Named Arguments:
lab_dir="$(mktemp -d /tmp/php-ep08-XXXXXX)"
cd "$lab_dir"
cat << 'EOF' > functions_modern.php
<?php
declare(strict_types=1);
function format_metric(
string $name,
int|float $value,
string $unit = 'ms',
bool $uppercase = false
): string {
$label = $uppercase ? strtoupper($name) : $name;
return sprintf('%s=%s%s', $label, (string) $value, $unit);
}
$resNamed = format_metric(name: 'latency', value: 12.5, uppercase: true);
EOF
Lời gọi format_metric(name: 'latency', value: 12.5, uppercase: true) bỏ qua tham số thứ ba $unit = 'ms' ở giữa để chỉ định thẳng uppercase: true, đồng thời truyền số thực 12.5 hợp lệ nhờ Union Type int|float.
Tiếp theo, chúng ta nối thêm hàm mũi tên fn() tự động bắt biến $multiplier và Closure tăng biến đếm qua use (&$counter) vào functions_modern.php rồi chạy kịch bản:
cat << 'EOF' >> functions_modern.php
$multiplier = 3;
$scaleFn = static fn(int $n): int => $n * $multiplier;
$counter = 0;
$bumpClosure = static function () use (&$counter): int {
$counter++;
return $counter;
};
$bumpClosure();
$bumpClosure();
printf("Named=%s | Arrow=%d | ClosureCount=%d\n", $resNamed, $scaleFn(7), $counter);
EOF
php functions_modern.php
Kết quả xuất ra: Named=LATENCY=12.5ms | Arrow=21 | ClosureCount=2. Hàm mũi tên $scaleFn(7) nhân 7 * 3 = 21 mà không cần viết use ($multiplier), trong khi $bumpClosure() cập nhật trực tiếp biến $counter bên ngoài lên 2 nhờ tham chiếu &$counter.
Điều gì xảy ra nếu người gọi hàm vừa truyền đối số theo vị trí đầu tiên, vừa truyền lại chính tham số đó theo tên a: 20? Hãy chạy lệnh kiểm thử nhánh lỗi dưới đây:
set +e
php -r '
function demo(int $a, int $b = 1): int { return $a + $b; }
demo(10, a: 20);
'
echo "Mã thoát khi trùng Named Argument: $?"
set -e
Trình thông dịch lập tức dừng chương trình với mã thoát 255 và ném ra lỗi:
Fatal error: Uncaught Error: Named parameter $a overwrites previous argument in Command line code:3.
Tương tự, nếu bạn truyền một tên tham số không tồn tại trong định nghĩa hàm (hoặc sau khi tác giả thư viện đổi tên biến tham số từ $a sang $first), PHP sẽ ném lỗi Unknown named parameter.

Hình 2 đối chiếu trực quan cơ chế bắt biến của Closure truyền thống và Arrow Function. Kết thúc bài Lab, 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 08."
Khi xây dựng các hàm tiện ích tổng quát trên PHP 8.4, bạn sẽ thường xuyên gặp hai cú pháp sử dụng dấu ba chấm (...) có ý nghĩa khác nhau tùy ngữ cảnh:
int ...$numbers): Đặt trong danh sách tham số khi định nghĩa hàm, cho phép hàm nhận số lượng đối số tùy ý và gom toàn bộ vào mảng $numbers trong đó mỗi phần tử đều được kiểm tra đúng kiểu int.format_metric(...$params)): Đặt tại nơi gọi hàm, giúp bung một mảng tuần tự hoặc mảng kết hợp (khớp theo khóa Named Arguments) thành các đối số truyền vào hàm.$fn = strlen(...);): Khi viết dấu ba chấm duy nhất bên trong ngoặc tròn sau tên hàm hoặc phương thức ($this->handle(...)), PHP không gọi hàm đó mà tạo ra một đối tượng Closure trỏ tới hàm đó, thay thế hoàn toàn cách truyền tên hàm bằng chuỗi 'strlen' thiếu an toàn kiểu trước đây.Hãy tự trả lời 4 câu hỏi tình huống dưới đây để củng cố kiến thức EP 8:
$fn = fn(int $x) => $x + $offset;, nếu sau khi tạo $fn ta đổi giá trị $offset ở bên ngoài, kết quả của $fn có đổi theo không?
Đáp án: Không. Hàm mũi tên bắt biến $offset theo bản sao giá trị (by-value) ngay tại thời điểm khởi tạo đối tượng Closure.function test(int $a = 1, int $b) trên PHP 8.4 không?
Đáp án: Không nên vì sẽ phát sinh cảnh báo Deprecated. Từ PHP 8.0 đến PHP 8.4, việc đặt tham số có giá trị mặc định trước tham số bắt buộc bị cảnh báo Deprecated: Optional parameter $a declared before required parameter $b.: void và : never là gì?
Đáp án: Khác nhau ở việc hàm có trả quyền điều khiển về nơi gọi hay không. Hàm : void vẫn chạy hết thân hàm rồi quay về nơi gọi (không kèm giá trị), còn hàm : never cam kết không bao giờ quay về nơi gọi (bắt buộc phải kết thúc bằng throw, exit() hoặc vòng lặp vô tận).$unit thành $metricUnit sẽ làm lỗi các lời gọi viết unit: 'ms'.Tiêu chí nghiệm thu: Hoàn thành EP 8 khi thiết kế được chữ ký hàm có Union Types, gọi hàm bằng Named Arguments đúng quy chuẩn và phân biệt rõ Closure với Arrow Function. Sang PHP EP 9, chúng ta sẽ đi sâu vào bản chất mảng byte của chuỗi PHP và cách xử lý tiếng Việt UTF-8 an toàn bằng mbstring.
Ôn bài trước: PHP EP 7: Vòng lặp, tham chiếu và Generator trong PHP. Học tiếp: PHP EP 9: Xử lý chuỗi UTF-8 bằng mbstring 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.