Menu

Câu hỏi phỏng vấn PHP và câu trả lời (PHP 8)

24 câu hỏi phỏng vấn PHP với câu trả lời ngắn gọn, chính xác cho PHP 8, từ == và ===, isset và empty đến tham chiếu trong foreach, self và static, generator, SQL injection và băm mật khẩu. Phần lớn câu trả lời kèm code bạn có thể chạy và sửa.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Đây là những câu hỏi PHP hay gặp nhất khi phỏng vấn, kèm câu trả lời ngắn gọn, chính xác cho PHP 8.3 và code ở những chỗ code nói rõ hơn lời. Hãy đoán đầu ra của mỗi khối trước khi bấm Chạy: đó chính là điều nhà tuyển dụng kiểm tra.

Câu hỏi phỏng vấn PHP cơ bản

PHP là gì và chạy như thế nào?

PHP là ngôn ngữ kịch bản phía máy chủ. Web server (Nginx hoặc Apache) chuyển request cho PHP (thường là PHP-FPM), PHP chạy script từ trên xuống, và mọi thứ nó in ra trở thành response, thường là HTML. Mỗi request bắt đầu từ đầu: biến không tồn tại qua các request trừ khi bạn lưu chúng vào session, cơ sở dữ liệu hoặc cache. PHP cũng chạy script trên dòng lệnh (php script.php).

== và === khác nhau thế nào?

== so sánh sau khi ép kiểu tự động (type juggling), === so sánh cả giá trị lẫn kiểu mà không chuyển đổi gì. Phần khởi động ở trên cho thấy == hoạt động ra sao: chuỗi số được so sánh như số, nên '10' == '1e1' là true. Từ PHP 8, một số so sánh với chuỗi không phải số sẽ được so sánh như chuỗi, nên 0 == 'a' là false (trong PHP 7 nó là true). Bấm vào các ô để so sánh hai toán tử:

Loose (==) and strict (===) comparison
Point at or tap a cell to read it as PHP.
$a == $btruefalse01"0""1"""null[]"a""10""1e1"
true
false
0
1
"0"
"1"
""
null
[]
"a"
"10"
"1e1"
Every cell computed by PHP 8.3.19.

Mặc định hãy dùng ===. in_array() và array_search() dùng == trừ khi bạn truyền true làm đối số thứ ba.

echo và print khác nhau thế nào?

Cả hai đều xuất chuỗi. echo nhận nhiều đối số (echo $a, $b;) và không trả về gì; print nhận một đối số và trả về 1, nên dùng được trong biểu thức. Cả hai đều không phải hàm, nên dấu ngoặc đơn là tùy chọn. Trên thực tế ai cũng dùng echo.

Nháy đơn và nháy kép?

Chuỗi trong nháy kép thay biến và chuỗi thoát (escape sequence) bằng giá trị: "Hi $name\n". Chuỗi trong nháy đơn giữ nguyên nghĩa đen, trừ \' và \\: 'Hi $name\n' in ra đúng các ký tự đó. Chênh lệch tốc độ không đáng kể, nên lựa chọn là vì dễ đọc: nháy kép khi cần chèn biến, nháy đơn khi không cần.

PHP có những kiểu dữ liệu nào?

Bốn kiểu vô hướng (int, float, string, bool), hai kiểu phức hợp (array và object), và hai kiểu đặc biệt (null và resource). callable, iterable, mixed và void chỉ tồn tại trong khai báo kiểu, không bao giờ là kiểu của một giá trị. gettype() và get_debug_type() cho biết kiểu của giá trị lúc chạy, còn var_dump() in kiểu kèm giá trị.

isset, empty và is_null khác nhau thế nào?

isset($x) là true khi biến tồn tại và khác null. empty($x) là true khi biến không tồn tại hoặc mang giá trị falsy (0, '0', '', [], false, null). is_null($x) là true với null, nhưng báo cảnh báo khi biến chưa được định nghĩa. isset và empty không bao giờ cảnh báo, vì thế chúng được dùng cho khóa của mảng.

isset, empty and is_null for each value of $x
Point at or tap a cell to read it as PHP.
$xissetemptyis_null
null
false
0
"0"
""
" "
[]
"a"
0.0
Every cell computed by PHP 8.3.19.

Hàng mà mọi người hay trả lời sai là "0": chuỗi số không là rỗng, còn " " (một khoảng trắng) thì không. Trang isset có bảng đầy đủ.

include và require khác nhau thế nào?

Cả hai đều chèn và chạy một file khác. Nếu thiếu file, include phát ra cảnh báo và script chạy tiếp, còn require ném ra Error ("Failed opening required"), khiến script dừng trừ khi có code bắt lỗi. Các biến thể _once (include_once, require_once) bỏ qua file đã nạp, giúp tránh lỗi "cannot redeclare function". Dùng require (hoặc require_once) cho những file mà script không thể chạy thiếu, tức gần như tất cả; autoloader của Composer khiến phần lớn các lệnh include thủ công không còn cần thiết.

GET và POST khác nhau thế nào?

GET gửi dữ liệu trong chuỗi query của URL ($_GET), nên dữ liệu hiện ra, lưu bookmark được và bị giới hạn độ dài; dùng nó để đọc dữ liệu, như tìm kiếm và phân trang. POST gửi dữ liệu trong phần thân request ($_POST); dùng nó cho mọi thứ làm thay đổi trạng thái, như đăng nhập, lưu dữ liệu hay thanh toán. Request GET không bao giờ được thay đổi dữ liệu, vì liên kết có thể bị tải trước và bị crawler truy cập.

Toán tử null coalescing là gì?

$a ?? $b trả về $a nếu nó tồn tại và khác null, ngược lại trả về $b, không cảnh báo khi biến hoặc khóa chưa được định nghĩa: $page = $_GET['page'] ?? 1;. ??= chỉ gán khi vế trái là null hoặc không tồn tại. Nó khác ?:, toán tử dùng giá trị dự phòng với mọi giá trị falsy, nên 0 ?: 5 là 5 nhưng 0 ?? 5 là 0.

Câu hỏi phỏng vấn PHP trung cấp

Chuyện gì xảy ra khi duyệt theo tham chiếu trong foreach?

foreach ($arr as &$v) cho phép bạn sửa phần tử tại chỗ, nhưng sau vòng lặp $v vẫn là tham chiếu tới phần tử cuối. Dùng lại tên đó trong vòng lặp thứ hai sẽ ghi đè phần tử ấy:

Phần tử cuối là 20, không phải 30: trong vòng lặp thứ hai, $v (vẫn là $arr[2]) lần lượt được gán 10, rồi 20, rồi chính nó. Bỏ // trước unset($v) rồi chạy lại. Trang foreach trình bày chi tiết về tham chiếu.

Lớp trừu tượng và interface khác nhau thế nào? Còn trait là gì?

Interface là một hợp đồng: các chữ ký phương thức (và hằng số) không có phần cài đặt, và một lớp có thể implement nhiều interface. Lớp trừu tượng có thể chứa thuộc tính, hàm khởi tạo và phương thức hoàn chỉnh, cùng các phương thức trừu tượng mà lớp con phải viết; một lớp chỉ extends được một lớp. Trait là một nhóm phương thức được sao chép vào lớp bằng use, để tái sử dụng code giữa các lớp không có chung lớp cha.

Một câu trả lời ngắn mà nhà tuyển dụng thích: dùng interface để nói một thứ làm được gì, lớp trừu tượng để dùng chung cách làm, và trait khi các lớp không liên quan cần cùng vài phương thức.

self và static khác nhau thế nào?

self chỉ lớp nơi code được viết. static chỉ lớp thực sự được gọi lúc chạy, gọi là late static binding. Điều này quan trọng trong các phương thức của lớp cha tạo ra hoặc mô tả "loại đối tượng này":

User::createStatic() tạo ra một User. User::createSelf() tạo ra một Model, vi phạm kiểu trả về static, nên PHP ném ra TypeError. Đó là lý do các factory method dùng new static().

Magic method là gì?

Là các phương thức PHP tự gọi cho bạn, tất cả đều bắt đầu bằng hai dấu gạch dưới. Những cái phổ biến: __construct() khi đối tượng được tạo, __destruct() khi bị hủy, __toString() khi được dùng như chuỗi, __get() và __set() cho thuộc tính không truy cập được, __call() và __callStatic() cho phương thức không tồn tại, __invoke() khi đối tượng được gọi như hàm, và __clone() sau clone.

array_merge() và toán tử + khác nhau thế nào?

array_merge() nối thêm: khóa số được đánh số lại, còn khóa chuỗi xuất hiện sau ghi đè khóa trước. + là phép hợp: nó giữ mảng bên trái và chỉ thêm những khóa còn thiếu, nên với khóa trùng thì giá trị đầu tiên được giữ.

+ là công cụ phù hợp cho giá trị mặc định: $options + ['timeout' => 30] giữ những gì nơi gọi truyền vào và điền phần còn lại.

declare(strict_types=1) làm gì?

Không có nó, PHP chuyển đối số vô hướng sang kiểu tham số đã khai báo khi có thể: truyền "5" vào tham số int cho ra 5. Với declare(strict_types=1); là câu lệnh đầu tiên của file, các lời gọi từ file đó phải truyền đúng kiểu, nếu không sẽ ném ra TypeError (truyền int vào tham số float là trường hợp mở rộng duy nhất được phép).

Xóa dòng declare rồi chạy lại: lời gọi thứ hai trả về 10 thay vì ném lỗi.

Cookie được lưu trong trình duyệt và gửi kèm mọi request, nên người truy cập đọc và sửa được nó. Session lưu dữ liệu trên máy chủ và chỉ giữ một id ngẫu nhiên trong cookie (PHPSESSID). Hãy đặt mọi thứ cần được tin cậy, như id của người dùng đã đăng nhập, vào $_SESSION; sau khi đăng nhập, gọi session_regenerate_id(true). Trang session và cookie có code minh họa.

Làm sao chống SQL injection?

Đừng bao giờ tạo câu SQL bằng cách nối chuỗi đầu vào. Hãy dùng prepared statement, để giá trị được gửi tách riêng khỏi câu truy vấn:

<?php
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

Placeholder chỉ dùng được cho giá trị. Tên bảng, tên cột và ASC/DESC phải được kiểm tra với một danh sách cho phép trước khi đưa vào SQL. Xem PDO.

Làm sao chống XSS?

Escape đầu ra, không phải đầu vào: bọc mọi giá trị in vào HTML trong htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), hoặc dùng template engine tự làm việc đó (Twig, Blade). Bên trong <script>, dùng json_encode() với JSON_HEX_TAG. Thêm header Content-Security-Policy làm lớp bảo vệ thứ hai.

Nên lưu mật khẩu thế nào?

Bằng password_hash($password, PASSWORD_DEFAULT) và kiểm tra bằng password_verify($password, $hash). Chúng dùng bcrypt (hoặc Argon2id) với salt ngẫu nhiên lưu ngay trong hash và chi phí tính toán được chủ ý đặt cao. md5() và sha1() không dùng được cho mật khẩu vì chúng nhanh và không có salt. Dùng password_needs_rehash() lúc đăng nhập để nâng cấp các hash cũ.

Câu hỏi phỏng vấn PHP nâng cao

Generator là gì và khi nào nên dùng?

Một hàm chứa yield trả về một Generator tạo ra từng giá trị một, chỉ khi được yêu cầu. Nhờ đó bạn duyệt được một dãy rất lớn hoặc vô hạn mà không phải tạo mảng trong bộ nhớ:

Các trường hợp dùng điển hình là đọc file lớn theo từng dòng, truyền dần các hàng từ cơ sở dữ liệu và pipeline lười (lazy). Generator còn hỗ trợ khóa (yield $key => $value), ủy quyền (yield from) và nhận giá trị bằng send().

Closure là gì, và use làm gì?

Một hàm ẩn danh (function () { ... }) là một đối tượng Closure. Nó không thấy biến bên ngoài trừ khi bạn liệt kê chúng trong use, thao tác này sao chép giá trị tại thời điểm closure được tạo; use (&$x) thì bắt theo tham chiếu. Arrow function (fn ($x) => $x * $factor) tự động bắt biến bên ngoài theo giá trị, và chỉ chứa một biểu thức.

abstract, final và readonly khác nhau thế nào?

abstract đánh dấu một lớp không thể khởi tạo, hoặc một phương thức mà lớp con phải cài đặt. final cấm ghi đè phương thức hoặc kế thừa lớp. readonly (PHP 8.1) khiến một thuộc tính chỉ được ghi một lần, từ bên trong lớp, thường là trong hàm khởi tạo; PHP 8.2 thêm lớp readonly, trong đó mọi thuộc tính đều là readonly.

Composer là gì, và PSR-4 là gì?

Composer là trình quản lý phụ thuộc của PHP: composer.json liệt kê các package, composer.lock cố định phiên bản chính xác, còn vendor/autoload.php nạp mọi thứ. PSR-4 là chuẩn autoload ánh xạ một tiền tố namespace sang một thư mục, nên App\Mail\Mailer nằm ở src/Mail/Mailer.php và được nạp ở lần dùng đầu tiên. Hãy giải thích install (từ file lock) khác update (phiên bản mới, file lock mới) ra sao.

PHP 8 có gì thay đổi?

Các tính năng nổi bật: ở 8.0 là named argument, match, toán tử nullsafe ?->, union type, constructor property promotion, attribute, str_contains() và phép so sánh chuỗi với số hợp lý hơn; ở 8.1 là enum, thuộc tính readonly, cú pháp first-class callable (strlen(...)) và fiber; ở 8.2 là lớp readonly và các kiểu độc lập null/true/false; ở 8.3 là hằng số lớp có kiểu, attribute #[\Override] và json_validate(); ở 8.4 là property hook, phạm vi truy cập bất đối xứng (public private(set)), array_find() và new Foo()->method() không cần bọc trong ngoặc đơn. Nhiều hàm giờ cũng ném TypeError hoặc ValueError thay vì trả về false kèm cảnh báo.

PHP quản lý bộ nhớ thế nào?

Mỗi giá trị có một bộ đếm tham chiếu; khi nó về 0, bộ nhớ được giải phóng ngay lập tức. Các đối tượng tham chiếu lẫn nhau theo vòng tròn không bao giờ về 0, nên một bộ thu gom vòng (cycle collector) chạy mỗi khi bộ đệm các gốc vòng tiềm năng đầy (mặc định 10.000 mục) và giải phóng chúng (gc_collect_cycles() chạy nó theo yêu cầu). Mảng và chuỗi dùng copy-on-write: gán $b = $a chia sẻ dữ liệu cho tới khi một trong hai bị sửa. Vì mỗi request bắt đầu và kết thúc gọn gàng, rò rỉ bộ nhớ chủ yếu đáng lo ở các worker chạy lâu và các consumer của hàng đợi.

Mảng PHP được cài đặt thế nào?

Mảng PHP là một bảng băm có thứ tự: nó giữ thứ tự chèn và ánh xạ khóa int hoặc string tới giá trị, vì thế cùng một kiểu dùng được như danh sách, từ điển và ngăn xếp. Tra cứu theo khóa (isset($arr[$key])) trung bình mất thời gian hằng số, còn in_array() duyệt qua mọi giá trị. Khóa chuỗi trông như số nguyên sẽ được chuyển đổi: $a['1'] và $a[1] là cùng một phần tử.

Bài tập code thường gặp khi phỏng vấn

Các bài tập ngắn hay được yêu cầu làm trực tiếp. Mỗi hàm là một lời giải phổ biến; hãy chạy chúng, thêm đầu vào của riêng bạn (kể cả đầu vào rỗng), và sẵn sàng giải thích độ phức tạp thời gian, vì đó thường là điều nhà tuyển dụng kiểm tra.

Câu hỏi thường gặp

Chuẩn bị cho buổi phỏng vấn lập trình viên PHP thế nào?

Hãy chắc rằng bạn giải thích và viết code được cho các phần cơ bản mà không cần tra cứu: so sánh và kiểu dữ liệu, mảng và các hàm mảng, OOP (interface, lớp trừu tượng, trait, static), ngoại lệ và bảo mật (prepared statement, htmlspecialchars, password_hash). Sau đó nắm vững một framework (Laravel hoặc Symfony) và Composer đủ để trình bày về một dự án bạn đã làm.

Nên nắm phiên bản PHP nào khi đi phỏng vấn?

PHP 8. Nhà tuyển dụng mong bạn biết các tính năng thêm vào từ 8.0 đến 8.3: named argument, match, toán tử nullsafe ?->, union type, constructor property promotion, enum, thuộc tính readonly và cú pháp first-class callable. Chỉ nhắc tới hành vi của PHP 7 ở những chỗ đã thay đổi, như 0 == "a".

Fresher thường được hỏi những câu PHP nào?

Chủ yếu là kiến thức nền của ngôn ngữ: == và ===, echo và print, nháy đơn và nháy kép, isset và empty, include và require, GET và POST, session và cookie, cùng các bài tập mảng đơn giản như đảo ngược, lọc hay đếm giá trị. Hãy chuẩn bị cho ít nhất một bài tập code ngắn.

Lập trình viên có kinh nghiệm thường được hỏi những câu PHP nào?

Thiết kế và cách mọi thứ hoạt động bên dưới: interface và lớp trừu tượng, trait, late static binding, tham chiếu, generator và bộ nhớ, ngoại lệ và xử lý lỗi, SQL injection và transaction, cache, autoload của Composer, và cách bạn tổ chức hoặc kiểm thử một tính năng. Câu trả lời cần kèm theo sự đánh đổi, không chỉ là định nghĩa.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu