
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 cấu trúc mảng kết hợp và danh sách trong PHP, thực hành lọc, ánh xạ cùng nhóm hàm mới array_find, array_any, array_all và tránh bẫy chỉ mục trả về 0.
Trong các ứng dụng xử lý danh sách đơn hàng hoặc hồ sơ máy chủ, lập trình viên thường phải trả lời bốn câu hỏi cơ bản: tìm phần tử đầu tiên thỏa điều kiện, tìm khóa của phần tử đó, kiểm tra xem có ít nhất một phần tử vượt ngưỡng không, và xác nhận toàn bộ danh sách có đạt chuẩn không. Trước PHP 8.4, do thiếu hàm tìm kiếm dừng sớm trong thư viện chuẩn, nhiều dự án phải viết lặp lại vòng lặp foreach khởi tạo $found = null kèm break, hoặc lạm dụng array_filter() rồi lấy phần tử đầu bằng reset(). Cách dùng array_filter() duyệt qua toàn bộ phần tử dù chỉ cần bản ghi đầu tiên gây lãng phí CPU và cấp phát mảng trung gian không cần thiết.
Bài học này hướng dẫn bạn làm chủ bốn hàm mảng mới của PHP 8.4 (array_find(), array_find_key(), array_any(), array_all()), kết hợp với array_map(), array_filter(), array_reduce() theo tư duy bất biến, và nhận diện ranh giới của array_is_list(). Bài viết nối tiếp PHP EP 9: Xử lý chuỗi UTF-8 bằng mbstring trong PHP và chuẩn bị cho OOP tại PHP EP 11: Class, Object và Constructor Promotion trong PHP. Tài liệu tham chiếu gồm PHP Manual: Array Functions và PHP RFC: array_find.
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.memory_limit (vốn cần Generator như đã học ở PHP EP 7).Mỗi hàm mảng có thứ tự tham số, hành vi bảo toàn khóa (key preservation) và điều kiện dừng khác nhau. Bảng dưới đây tổng hợp hợp đồng kỹ thuật để bạn tra cứu chính xác:
| Hàm xử lý mảng | Thứ tự tham số chính | Kiểu trả về & Cơ chế dừng | Hành vi với khóa mảng (Keys) | Điều kiện / Không nên giả định |
|---|---|---|---|---|
array_find($array, $cb) (PHP 8.4) |
($array, callable $cb) |
Trả về giá trị phần tử đầu tiên mà $cb($value, $key) trả về true; dừng sớm ngay lập tức. |
Truyền cả $value và $key vào callback; trả về null nếu không tìm thấy. |
Nếu mảng có chứa phần tử mang giá trị null, kết quả null không phân biệt được giữa "tìm thấy null" và "không tìm thấy" (hãy dùng array_find_key). |
array_find_key($array, $cb) (PHP 8.4) |
($array, callable $cb) |
Trả về khóa (int|string) đầu tiên thỏa điều kiện hoặc null nếu không có; dừng sớm. |
Xác định chính xác vị trí phần tử kể cả khi giá trị phần tử bằng null. |
Thay thế an toàn cho array_search() (vốn trả về false dễ nhầm với khóa 0 nếu so sánh lỏng). |
array_any($array, $cb) / array_all($array, $cb) (PHP 8.4) |
($array, callable $cb) |
Trả về bool; dừng ngay khi gặp phần tử đầu tiên quyết định kết quả. |
Nhận ($value, $key) vào callback kiểm tra điều kiện. |
Trên mảng rỗng []: array_any([], $cb) trả về false, nhưng array_all([], $cb) trả về true (vacuous truth). |
array_filter($array, $cb, $mode) |
($array, ?callable $cb, int $mode = 0) |
Duyệt hết mảng, trả về mảng mới chứa các phần tử thỏa điều kiện. | Giữ nguyên khóa gốc (tạo khoảng trống chỉ mục số và làm mất tính chất list). |
Nếu bỏ qua $cb, hàm sẽ xóa mọi phần tử "falsy" bao gồm cả chuỗi "0" và số 0! |
array_map($cb, $array, ...) |
(?callable $cb, array $array, ...) |
Duyệt hết mảng, trả về mảng mới đã biến đổi từng phần tử. | Giữ khóa gốc nếu chỉ truyền 1 mảng; đánh lại chỉ mục 0..n-1 nếu truyền từ 2 mảng trở lên. |
Thứ tự tham số ngược với array_filter ($callback đứng trước $array). |
Trong nhân Zend Engine, mọi mảng PHP đều được cài đặt bằng một cấu trúc Bảng băm có thứ tự (Ordered Hash Table). Khi mảng có khóa nguyên liên tục bắt đầu từ 0, 1, 2..., Zend Engine tối ưu hóa thành mảng đóng gói (packed array), và hàm array_is_list($arr) sẽ trả về true.
Theo PHP RFC: array_find, việc gọi array_filter($items, $fn) chỉ để lấy một phần tử buộc máy ảo cấp phát một HashTable mới và chạy callback trên 100% phần tử. Với array_find(), array_find_key(), array_any() và array_all() trên PHP 8.4, vòng lặp nội bộ dừng ngay tại phần tử đầu tiên quyết định kết quả mà không cấp phát mảng tạm. Cả bốn hàm thống nhất chữ ký callable(mixed $value, int|string $key): bool.
Khi biến đổi danh sách, áp dụng tư duy bất biến (immutability) qua chuỗi array_filter -> array_values -> array_map -> array_reduce giúp mảng gốc giữ nguyên 100%, loại bỏ lỗi tác dụng phụ do tham chiếu &$item.

Trong bài Lab này, chúng ta sẽ kiểm tra danh sách trạng thái máy chủ bằng array_find(), array_find_key(), array_any(), array_all() và array_is_list().
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-ep10-XXXXXX)"
cd "$WORKDIR"
pwd
Bước 2: Tạo kịch bản server_audit.php kiểm tra danh sách máy chủ với các hàm PHP 8.4
Mục đích: Tìm máy chủ đầu tiên có mức tải CPU từ 80% trở lên, lấy khóa chỉ mục của nó, kiểm tra xem có máy chủ nào quá tải >= 90% không và xác nhận toàn bộ máy chủ đều đang hoạt động (up === true).
cat <<'EOF' > "$WORKDIR/server_audit.php"
<?php
declare(strict_types=1);
$servers = [
['host' => 'web-01', 'cpu' => 42, 'up' => true],
['host' => 'web-02', 'cpu' => 88, 'up' => true],
['host' => 'db-01', 'cpu' => 91, 'up' => true],
];
$firstHigh = array_find($servers, fn(array $s): bool => $s['cpu'] >= 80);
$firstKey = array_find_key($servers, fn(array $s): bool => $s['cpu'] >= 80);
$anyHigh = array_any($servers, fn(array $s): bool => $s['cpu'] >= 90);
$allUp = array_all($servers, fn(array $s): bool => $s['up'] === true);
$isList = array_is_list($servers);
echo "Host={$firstHigh['host']} | Key={$firstKey} | AnyHigh=" . ($anyHigh ? 'true' : 'false')
. " | AllHealthy=" . ($allUp ? 'true' : 'false')
. " | IsList=" . ($isList ? 'true' : 'false') . "\n";
EOF
php "$WORKDIR/server_audit.php"
Kết quả quan sát được:
Host=web-02 | Key=1 | AnyHigh=true | AllHealthy=true | IsList=true.array_find(), ngay sau khi kiểm tra phần tử ở khóa 1 (web-02 có cpu = 88 >= 80), hàm lập tức trả về phần tử đó và không duyệt tiếp đến db-01.Cảnh báo an toàn: Hãy luôn khai báo kiểu trả về : bool cho arrow function trong array_any() và array_all() kèm declare(strict_types=1) để tránh trả về nhầm kiểu dữ liệu.
Khi cần tổng hợp báo cáo từ danh sách thô, việc kết hợp tuần tự các hàm bậc cao giúp tách biệt rõ ba bước: lọc dữ liệu (array_filter), chuẩn hóa hình thái (array_map) và gộp kết quả (array_reduce).
Bước 3: Tạo kịch bản pipeline_report.php tính tổng dung lượng các gói dịch vụ đang hoạt động
Mục đích: Lọc các gói đang kích hoạt, chuẩn hóa tên gói bằng mb_strtoupper() và tính tổng hạn mức băng thông bằng array_reduce().
cat <<'EOF' > "$WORKDIR/pipeline_report.php"
<?php
declare(strict_types=1);
$plans = [
['code' => ' basic ', 'quotaGb' => 50, 'active' => true],
['code' => 'legacy', 'quotaGb' => 20, 'active' => false],
['code' => ' pro ', 'quotaGb' => 200, 'active' => true],
];
$activePlans = array_values(array_filter(
$plans,
fn(array $p): bool => $p['active'] === true
));
$normalized = array_map(
fn(array $p): array => [
'code' => mb_strtoupper(mb_trim($p['code']), 'UTF-8'),
'quotaGb' => $p['quotaGb'],
],
$activePlans
);
$totalQuota = array_reduce(
$normalized,
fn(int $carry, array $p): int => $carry + $p['quotaGb'],
0
);
echo "Count=" . count($normalized) . " | First={$normalized[0]['code']} | TotalGB={$totalQuota}\n";
EOF
php "$WORKDIR/pipeline_report.php"
Kết quả quan sát được:
Count=2 | First=BASIC | TotalGB=250.array_reduce() là giá trị khởi tạo $initial = 0. Nếu bỏ quên tham số này, biến tích lũy $carry ở vòng đầu sẽ mang giá trị null và gây lỗi TypeError khi khai báo int $carry.Cảnh báo an toàn: Tuyệt đối không gọi array_filter($quantities) không có callback trên mảng số lượng tồn kho, vì PHP sẽ xóa mất các mặt hàng có số lượng bằng 0 hợp lệ.
Nếu trả về trực tiếp kết quả của array_filter() qua json_encode(), khi phần tử ở khóa 0 bị lọc bỏ, mảng kết quả sẽ bắt đầu từ khóa 1. Khi đó array_is_list() trả về false và json_encode() biến danh sách [...] thành đối tượng JSON {"1": ...}, làm ứng dụng Frontend bị lỗi.
Bước 4: Kiểm chứng array_is_list() trước và sau khi bọc array_values()
Mục đích: Chứng minh array_filter() giữ nguyên khóa gốc làm gãy cấu trúc danh sách liên tục, và cách khắc phục bằng array_values().
php -r '
$filtered = array_filter([10, 25, 30], fn(int $x): bool => $x > 20);
echo "Before=" . (array_is_list($filtered) ? "true" : "false")
. "|After=" . (array_is_list(array_values($filtered)) ? "true" : "false") . "\n";
'
Kết quả quan sát được:
Before=false|After=true. Sau khi bọc qua array_values($filtered), mảng được đánh lại chỉ mục 0, 1 và luôn xuất ra JSON array [25, 30] chuẩn.
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 áp dụng các hàm mảng trên môi trường Production, cần lưu ý hai ranh giới kỹ thuật:
array_filter() rồi array_map() đều tạo một mảng mới trong RAM. Với tập dữ liệu hàng trăm nghìn bản ghi, hãy dùng Generator (yield) hoặc đọc từng dòng từ con trỏ cơ sở dữ liệu.null trong array_find(): Nếu mảng có chứa phần tử mang giá trị hợp lệ là null, array_find($arr, $cb) === null không phân biệt được giữa tìm thấy phần tử null và không tìm thấy. Hãy dùng array_find_key($arr, $cb) !== null.Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:
array_all([], fn($x) => $x > 0) trên mảng rỗng trả về true hay false? -> Trả về true (vacuous truth). Nếu cần danh sách không rỗng, hãy kiểm tra $items !== [] && array_all($items, $cb).array_find_key() thay vì array_find()? -> Khi cần lấy khóa của phần tử hoặc khi mảng có thể chứa phần tử mang giá trị null.array_filter() thường làm hỏng định dạng mảng JSON trả về cho Frontend? -> Do array_filter() giữ nguyên khóa gốc, làm đứt quãng chỉ mục số khiến json_encode() xuất ra object {...} thay vì array [...]; cần bọc bằng array_values().array_map() khác gì với array_filter() và array_find()? -> Bị đảo ngược. array_map($callback, $array) đặt callback lên trước, còn array_filter() và nhóm array_find*() đều nhận $array làm tham số đầu tiên.Khép lại Chặng 2, bạn đã làm chủ cách xử lý chuỗi UTF-8 và mảng hiện đại trong PHP 8.4. Hãy bước sang Chặng 3 tại PHP EP 11: Class, Object và Constructor Promotion trong PHP để đóng gói dữ liệu thành các lớp đối tượng chặt chẽ.
Ôn bài trước: PHP EP 9: Xử lý chuỗi UTF-8 bằng mbstring trong PHP. Học tiếp: PHP EP 11: Class, Object và Constructor Promotion 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.