Dấu ? đặt ở đâu cho đúng? Gỡ rối "nỗi oan" của Nullable Type trong PHP
Chào anh em Viblo, lại là mình đây!
Kể từ khi PHP lột xác từ phiên bản 7.x (và giờ là 8.x), ngôn ngữ này đã dần rũ bỏ cái mác "lỏng lẻo" để hướng tới sự chặt chẽ của Type Hinting (chỉ định kiểu dữ liệu). Một trong những tính năng mình cực kỳ tâm đắc khi làm backend chính là Nullable Types (Kiểu dữ liệu có thể chứa Null).
Thế nhưng, tuần trước khi ngồi review Pull Request (PR) cho một cậu em fresher trong team, mình nhận ra một lỗi sai cú pháp vô cùng kinh điển mà chắc hẳn anh em nào mới làm quen với Type Hinting trong PHP cũng từng một lần bối rối: Vị trí của dấu ?.
Cậu em đó viết một hàm cập nhật thông tin user, trong đó số điện thoại là trường không bắt buộc (có thể null). Và dòng code đó trông như thế này:
// Sai cú pháp!
public function updateProfile(string ?$phone)
{
// ... logic cập nhật
}
Và tất nhiên, PHP ngay lập tức ném thẳng vào mặt một cái Parse Error đỏ chót! Tại sao vậy? Hãy cùng mổ xẻ bản chất của dấu ? này nhé.
1. Tư duy sai lầm: "Biến này có thể Null"
Khi viết string ?$phone, bộ não của chúng ta đang suy diễn theo logic ngôn ngữ tự nhiên: "Cái biến $phone này có thể là null". Vì thế, dấu ? được đặt sát ngay trước tên biến để ám chỉ sự không chắc chắn của biến đó.
Thực ra tư duy này không hề đáng trách. Trong một số ngôn ngữ hoặc cú pháp template (như Optional Chaining ?. trong JavaScript mà chúng ta hay dùng), dấu hỏi thường gắn liền trực tiếp với tên biến hoặc thuộc tính. Nhưng trong hệ thống Type System của PHP, quy tắc lại hoàn toàn khác.
2. Bản chất: Dấu ? thuộc về Kiểu dữ liệu (Type), không thuộc về Biến
Trong PHP, dấu ? là một modifier (bộ bổ nghĩa) dùng để đánh dấu kiểu dữ liệu. Nó phải đứng sát ngay trước tên kiểu (Type), chứ không phải trước biến.
Cú pháp chuẩn xác phải là:
// Chuẩn không cần chỉnh!
public function updateProfile(?string $phone)
{
// ... logic cập nhật
}
Hãy đọc to dòng code trên lên, bạn sẽ thấy sự khác biệt về mặt ngữ nghĩa: "Biến $phone bắt buộc phải mang kiểu dữ liệu là một-chuỗi-có-thể-null (?string)".
Dấu ? gắn chặt vào chữ string để tạo ra một khái niệm kiểu dữ liệu hoàn toàn mới. Nó nói với trình biên dịch của PHP rằng: Giá trị truyền vào đây hoặc là một chuỗi (String), hoặc là Null.
3. Minh chứng từ PHP 8.0: Sự xuất hiện của Union Types
Nếu giải thích ở trên vẫn khiến anh em thấy hơi "lý thuyết", thì hãy nhìn vào cách PHP 8.0 giới thiệu tính năng Union Types (Kiểu kết hợp).
Từ PHP 8.0, bạn có thể định nghĩa một biến nhận vào nhiều kiểu dữ liệu khác nhau bằng toán tử |. Và thực chất, cú pháp ?string chỉ là một cách viết tắt (syntactic sugar) của string|null.
// Hai cách viết này có ý nghĩa hoàn toàn y hệt nhau ở tầng biên dịch:
// Cách 1: Dùng Nullable (Ngắn gọn)
public function setAvatar(?string $url) {}
// Cách 2: Dùng Union Type (Bản chất)
public function setAvatar(string|null $url) {}
Nhìn vào cách 2, bạn thấy rõ ràng chữ null là một Kiểu dữ liệu đứng ngang hàng với string. Chúng ta viết string|null $url chứ không ai viết là string null|$url cả. Điều này chứng minh 100% rằng tính chất "có thể null" là đặc tính của hệ thống Type, độc lập hoàn toàn với cái tên biến $url hay $phone đứng đằng sau nó.
4. Ứng dụng ở mọi nơi
Việc hiểu đúng vị trí của dấu ? không chỉ áp dụng cho tham số của hàm, mà nó nhất quán ở mọi nơi bạn khai báo kiểu dữ liệu trong PHP:
Khai báo kiểu trả về (Return Type):
// Trả về một object User hoặc null nếu không tìm thấy
public function findUserById(int $id): ?User
{
return User::find($id);
}
Khai báo thuộc tính của Class (Class Properties) - Từ PHP 7.4:
class UserDTO
{
public int $id;
public string $name;
public ?string $bio; // Bio có thể bỏ trống (null)
}
Lời kết
Những lỗi sai về cú pháp (Syntax error) thường rất dễ fix vì IDE hoặc trình biên dịch sẽ báo ngay lập tức. Tuy nhiên, việc hiểu sâu sắc TẠI SAO cú pháp lại được thiết kế như vậy (dấu ? thuộc về Type chứ không thuộc về Variable) sẽ giúp tư duy lập trình của anh em trở nên sắc bén hơn rất nhiều. Khi đã nắm rõ luật chơi của Type System, anh em sẽ code tự tin hơn, thiết kế ra những hàm chặt chẽ hơn và bớt đi vô số những con bug tiềm ẩn liên quan đến lỗi NullPointerException huyền thoại.
Hãy mở lại code base của mình, bật declare(strict_types=1); lên và rà soát lại xem các kiểu dữ liệu của mình đã được định nghĩa "chuẩn bài" chưa nhé. Happy coding!
All rights reserved