Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
PHP

PHP EP 10: Mảng PHP và các hàm array_find, array_any, array_all

PHP EP 10: Mảng PHP và các hàm array_find, array_any, array_all

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.

Tìm phần tử trong mảng mà không cần biến cờ

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):

  • Ngày kiểm chứng: 2026-10-26.
  • Môi trường thực thi: Container cô lập php-course-qa:8.4 (--network none, --read-only, --tmpfs /tmp:rw,exec,nosuid,size=64m, user phplab UID 1001).
  • Phiên bản runtime: PHP 8.4.27 (cli) trên Alpine Linux 3.21 (aarch64), Zend Engine v4.4.27.
  • Giới hạn kiểm chứng: Kiểm chứng các hàm mảng trong bộ nhớ RAM của tiến trình CLI; không tuyên bố kiểm chứng xử lý tập dữ liệu khổng lồ vượt quá memory_limit (vốn cần Generator như đã học ở PHP EP 7).

Hợp đồng của các hàm xử lý mảng PHP 8.4

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).

Hash Table và cơ chế dừng sớm khi tìm kiếm

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.

Luồng lựa chọn thao tác mảng từ kiểm tra array_is_list đến tìm kiếm và biến đổi.
Hình 1. Luồng lựa chọn thao tác mảng từ kiểm tra array_is_list đến tìm kiếm và biến đổi. Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

Kiểm tra các hàm mảng mới trong PHP 8.4

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:

  • Đầu ra hiển thị: Host=web-02 | Key=1 | AnyHigh=true | AllHealthy=true | IsList=true.
  • Khi thực thi 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.

Kết hợp array_filter, array_map và array_reduce

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:

  • Màn hình in ra: Count=2 | First=BASIC | TotalGB=250.
  • Tham số thứ ba của 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ệ.

Kiểm tra chỉ mục sau array_filter và JSON List

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:

  • Đầu ra hiển thị: 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.
Đối chiếu array_search truyền thống với bộ hàm array_find và array_any hiện đại.
Hình 2. Đối chiếu array_search truyền thống với bộ hàm array_find và array_any hiện đại. Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

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"

Giới hạn bộ nhớ và ý nghĩa của giá trị null

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:

  1. Đánh đổi bộ nhớ của đường ống mảng: Mỗi bước 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.
  2. Phân biệt kết quả 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.

Tự kiểm tra và giới hạn

Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:

  • Câu hỏi 1: Gọi 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).
  • Câu hỏi 2: Khi nào bắt buộc phải dùng 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.
  • Câu hỏi 3: Vì sao kết quả của 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().
  • Câu hỏi 4: Thứ tự tham số của 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.

Bài liên quan trong bộ PHP

Tìm hiểu thêm