+1

"Cú lừa" thế kỷ mang tên in_array(): Lỗi ngớ ngẩn mà Backend PHP nào cũng từng dính

Chào anh em Viblo, lại là mình đây!

Series bóc phốt những "góc khuất" của nghề Backend hôm nay sẽ quay trở lại với người bạn già PHP. Khi làm việc với mảng (Array) trong PHP, chắc hẳn 99,9% anh em đều đã từng xài qua hàm in_array(). Hàm này dùng để kiểm tra xem một giá trị có tồn tại trong một mảng hay không. Nghe thì đơn giản, cú pháp cũng dễ ẹc: in_array($needle, $haystack).

Nhưng đằng sau cái vẻ ngoài vô hại đó là một cái bẫy chết người mà nếu bạn không cẩn thận, nó có thể phá nát logic hệ thống và tốn của bạn cả đêm không ngủ chỉ để debug. Hôm nay, mình sẽ chia sẻ lại một "vết nhơ" trong sự nghiệp code PHP của mình liên quan đến cái hàm nhỏ bé này.

1. Bối cảnh: Khi "Pending" tự nhiên biến thành "Banned"

Chuyện xảy ra ở một dự án quản lý tài khoản người dùng hồi mình mới vào nghề. Database lưu trạng thái người dùng (status) bằng số nguyên:

  • 0: Pending (Chờ duyệt)
  • 1: Active (Đang hoạt động)
  • 2: Banned (Bị khóa)

Hôm đó, sếp yêu cầu mình viết một đoạn code chặn những người dùng bị khóa hoặc đang chờ xóa tài khoản không được phép đăng nhập. Mình định nghĩa một mảng các trạng thái cấm bằng chữ cho dễ đọc, rồi dùng in_array() để check status truyền lên.

Code của mình trông như thế này:

$user_status = 0; // User này mới đăng ký, đang Pending

$forbidden_statuses = ['banned', 'deleted', 'suspended'];

if (in_array($user_status, $forbidden_statuses)) {
    echo "Tài khoản của bạn đã bị khóa!";
} else {
    echo "Đăng nhập thành công!";
}

Theo logic thông thường, biến $user_status là 0, không hề có trong mảng kia, nên user phải được đăng nhập. Nhưng thực tế, code lại in ra: "Tài khoản của bạn đã bị khóa!". Tất cả những user mới đăng ký đều không thể truy cập hệ thống. Mình đã ngớ người mất một lúc lâu.

2. Nguyên nhân: Quái vật mang tên "Type Juggling" (Ép kiểu ngầm)

Lý do nằm ở cơ chế so sánh lỏng lẻo (loose comparison - ==) mặc định của PHP.

Khi bạn gọi in_array() mà không có tham số thứ 3, PHP sẽ đi lấy cái $user_status (là số 0) so sánh lần lượt với từng phần tử trong mảng bằng toán tử ==.

Và đây là điều điên rồ của PHP (đặc biệt là các phiên bản PHP 7.x trở về trước): Khi so sánh một số nguyên (Integer) với một chuỗi (String), PHP sẽ cố gắng ép cái chuỗi kia về số nguyên trước rồi mới so sánh.

  • PHP lấy số 0 so sánh với chuỗi 'banned'.
  • Ép chuỗi 'banned' về số nguyên. Vì chữ 'b' không phải là số, PHP biến nguyên cái chuỗi đó thành số 0.
  • Bùm! 0 == 0 trả về true.

Thế là in_array() hớn hở báo về rằng nó đã tìm thấy phần tử đó trong mảng. Một cú lừa ngoạn mục! (Lưu ý: Sang đến PHP 8.0, đội ngũ phát triển đã nhận ra sự vô lý này và sửa lại cơ chế ép kiểu an toàn hơn, nhưng những hệ thống legacy chạy PHP 7 thì vẫn dính chưởng bình thường).

3. Cách "thuần phục": Chìa khóa thứ 3 (Strict Mode)

Sau cú vấp đó, mình học được một nguyên tắc sống còn mà bất cứ Senior PHP nào cũng phải thuộc nằm lòng: Luôn luôn bật Strict Mode cho in_array().

Hàm in_array có nhận một tham số thứ 3 (mặc định là false). Nếu bạn truyền true vào, PHP sẽ chuyển sang dùng so sánh nghiêm ngặt (strict comparison - ===), tức là phải giống nhau cả về GIÁ TRỊ lẫn KIỂU DỮ LIỆU.

Cách sửa lại đoạn code trên:

// Thêm true vào cuối
if (in_array($user_status, $forbidden_statuses, true)) {
    echo "Tài khoản của bạn đã bị khóa!";
}

Lúc này, số 0 đem so sánh === với chuỗi 'banned' sẽ lập tức văng false vì khác kiểu dữ liệu. Lỗi đã được fix gọn gàng.

4. Góc tối ưu (Performance): Có nên lạm dụng in_array()?

Ngoài câu chuyện về lỗi ép kiểu, còn một vấn đề nữa liên quan đến Performance.

Hàm in_array() có độ phức tạp thuật toán là O(n). Nghĩa là nó phải duyệt từ đầu đến cuối mảng (hoặc cho đến khi tìm thấy) để trả về kết quả. Nếu mảng $haystack của bạn có 10.000 phần tử, và bạn nhét hàm in_array() vào trong một vòng lặp foreach chạy 1.000 lần nữa, CPU server của bạn sẽ khóc thét.

Tuyệt chiêu thay thế: Nếu bạn phải tra cứu một mảng rất lớn và gọi nhiều lần, hãy chuyển cái mảng (Array) đó thành dạng Key-Value bằng hàm array_flip(), sau đó dùng hàm isset() để kiểm tra Key. Tốc độ của isset() trên mảng Hash Table của PHP là O(1) – cực kỳ nhanh!

$huge_array = ['apple', 'banana', 'orange', /*... hàng ngàn phần tử khác*/];

// Cách cũ: Chậm
if (in_array('apple', $huge_array, true)) { ... }

// Cách mới: Tối ưu cho dữ liệu lớn
$flipped_array = array_flip($huge_array); 
// Mảng giờ thành: ['apple' => 0, 'banana' => 1, 'orange' => 2]

if (isset($flipped_array['apple'])) { 
    // Chạy nhanh như chớp!
}

Lời kết

Code chạy được là một chuyện, hiểu sâu về ngôn ngữ để code chạy đúng và chạy nhanh lại là một câu chuyện khác. Chỉ một tham số thứ 3 bị bỏ quên, hoặc một cơ chế ép kiểu ngầm không nắm rõ cũng đủ để tạo ra một "thảm họa" trên production.

Hãy biến việc thêm chữ true vào hàm in_array() thành phản xạ vô điều kiện của bạn khi gõ code PHP nhé. Chúc anh em không bao giờ phải chửi thề vì những con bug giời ơi đất hỡi như thế này nữa!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.