EzCode # Bài 2 — Cú pháp PHP căn bản
Học xong bài này bạn sẽ:
- Khai báo và dùng được biến với các kiểu dữ liệu cơ bản
- Viết được
if,switch,for,whilevà hàm của riêng bạn- Định dạng được giá tiền và ngày tháng đúng như EzCode đang làm
Cần học trước: Bài 1
1. Vấn đề đặt ra
Mở trang chủ EzCode, bạn sẽ thấy mỗi khóa học hiện dòng chữ:
Giá: 300,000
Nhưng trong cơ sở dữ liệu, giá của khóa học đó được lưu là con số trần trụi 300000. Không có dấu phẩy nào cả.
Sang trang lịch sử thanh toán, bạn thấy trạng thái đơn hàng ghi "Đang xử lý". Nhưng trong cơ sở dữ liệu, cột status lưu chuỗi pending.
Hai chỗ này cho thấy một quy luật chung của mọi website: dữ liệu lưu trong database khác với thứ hiển thị cho người dùng. Giữa hai bên phải có code làm nhiệm vụ biến đổi.
Bài này dạy bạn những công cụ cơ bản để làm việc biến đổi đó: biến để giữ dữ liệu, điều kiện để chọn cách hiển thị, và hàm để đóng gói lại dùng nhiều lần.
2. Kiến thức mới
2.1. Biến
Biến là một cái hộp có tên, dùng để chứa dữ liệu. Trong PHP, tên biến luôn bắt đầu bằng dấu $.
<?php
$gia = 300000;
$tenKhoaHoc = "Khóa học HTML CSS cơ bản";
$daXuatBan = true;
?>
Bốn điều cần nhớ:
- Luôn có dấu
$ở đầu, cả khi khai báo lẫn khi dùng. - Không cần khai báo kiểu. Bạn không viết
int $gianhư trong Java hay C. PHP tự nhận biết. - Phân biệt chữ hoa chữ thường.
$giavà$Gialà hai biến khác nhau. Đây là nguồn lỗi rất hay gặp. - Tên biến chỉ chứa chữ cái, chữ số và dấu gạch dưới, và không được bắt đầu bằng chữ số.
Cách đặt tên trong EzCode: tác giả dùng kiểu $tenBien viết liền có chữ hoa ở giữa ($coursesList, $teacherCourses) và cả kiểu $ten_bien có gạch dưới ($course_id, $user_id). Cả hai đều dùng được; bạn nên giữ nguyên đúng tên tác giả đặt khi gõ lại code.
2.2. Các kiểu dữ liệu
| Kiểu | Tên tiếng Anh | Ví dụ | Dùng ở đâu trong EzCode |
|---|---|---|---|
| Chuỗi | string | "Khóa học Python" |
Tên khóa học, email, mô tả |
| Số nguyên | integer | 300000 |
Giá khóa học, id, thứ tự bài học |
| Số thực | float | 300000.50 |
Số tiền trong bảng payments |
| Luận lý | boolean | true, false |
Kết quả kiểm tra đăng nhập |
| Rỗng | null | null |
Khi truy vấn không tìm thấy dòng nào |
| Mảng | array | [1, 2, 3] |
Danh sách khóa học — học ở Bài 3 |
Muốn biết một biến đang giữ gì và thuộc kiểu nào, dùng var_dump():
<?php
$gia = 300000;
var_dump($gia); // in ra: int(300000)
$ten = "Python";
var_dump($ten); // in ra: string(6) "Python"
$co = true;
var_dump($co); // in ra: bool(true)
?>
var_dump() là công cụ gỡ lỗi quan trọng nhất của bạn. Khi code chạy sai mà không hiểu vì sao, hãy var_dump() biến ra xem nó thực sự đang chứa gì.
2.3. Chuỗi: nối và nội suy
Có hai cách ghép chuỗi lại với nhau.
Cách 1 — nối bằng dấu chấm:
<?php
$ten = "Hoàng";
echo "Xin chào, " . $ten . "!"; // Xin chào, Hoàng!
?>
Cách 2 — nội suy trong nháy kép:
<?php
$ten = "Hoàng";
echo "Xin chào, $ten!"; // Xin chào, Hoàng!
?>
Nội suy chỉ hoạt động với nháy kép. Nháy đơn in ra nguyên văn:
<?php
$ten = "Hoàng";
echo "Xin chào, $ten!"; // Xin chào, Hoàng!
echo 'Xin chào, $ten!'; // Xin chào, $ten!
?>
Khi biến là phần tử của mảng và nằm trong chuỗi, bạn nên bọc trong ngoặc nhọn cho rõ ràng:
<?php
echo "Giá là {$course['price']} đồng";
?>
Trong EzCode, bạn sẽ gặp nội suy ở những chỗ như header("Location: ?ctrl=course&act=detail&id=$id"); trong Controllers/CourseController.php.
2.4. Toán tử
Toán tử số học: +, -, *, /, % (chia lấy dư).
Toán tử gán rút gọn:
<?php
$sql = "SELECT * FROM courses";
$sql .= " WHERE status = 1"; // nối thêm vào cuối
echo $sql; // SELECT * FROM courses WHERE status = 1
$tong = 0;
$tong += 100; // tương đương $tong = $tong + 100
?>
Toán tử .= rất quan trọng: đây chính là cách Models/Courses.php dựng câu SQL động ở Bài 12.
Toán tử so sánh:
| Toán tử | Ý nghĩa | Ví dụ |
|---|---|---|
== |
Bằng nhau về giá trị | "5" == 5 là true |
=== |
Bằng nhau cả giá trị và kiểu | "5" === 5 là false |
!= |
Khác nhau về giá trị | "5" != 5 là false |
!== |
Khác giá trị hoặc khác kiểu | "5" !== 5 là true |
> < >= <= |
Lớn hơn, nhỏ hơn | 300000 > 200000 là true |
Vì sao phải phân biệt == và ===?
PHP rất dễ dãi khi so sánh bằng ==. Nó tự chuyển đổi kiểu trước khi so, dẫn tới kết quả bất ngờ:
<?php
var_dump("5" == 5); // bool(true) — chuỗi "5" bị đổi thành số 5
var_dump(0 == ""); // bool(true) ở PHP 7, bool(false) từ PHP 8
var_dump("abc" == 0); // bool(false) ở PHP 8
?>
=== không chuyển đổi gì cả, an toàn hơn nhiều.
EzCode dùng cả hai. Ví dụ trong Controllers/UserController.php có if ($_SERVER['REQUEST_METHOD'] === 'POST') dùng ba dấu bằng vì so sánh chuỗi với chuỗi, cần chính xác tuyệt đối. Còn trong Views/payment_history.php có if ($payment['status'] == 'pending') chỉ dùng hai dấu bằng.
Nguyên tắc cho bạn: cứ dùng === mặc định. Chỉ dùng == khi bạn thực sự muốn PHP tự chuyển kiểu.
Toán tử logic: && (và), || (hoặc), ! (phủ định).
<?php
$dangNhap = true;
$laGiangVien = false;
if ($dangNhap && $laGiangVien) {
echo "Vào được dashboard";
}
if (!$dangNhap) {
echo "Vui lòng đăng nhập";
}
?>
Đoạn này chính là logic chặn quyền trong Controllers/TeacherController.php mà bạn sẽ viết ở Bài 17.
2.5. Câu lệnh điều kiện if
<?php
$gia = 500000;
if ($gia > 400000) {
echo "Khóa học cao cấp";
} elseif ($gia > 200000) {
echo "Khóa học trung cấp";
} else {
echo "Khóa học phổ thông";
}
?>
2.6. Cú pháp thay thế if: ... endif;
PHP cho phép viết if theo một kiểu khác, thay dấu ngoặc nhọn bằng dấu hai chấm và từ khóa endif;:
<?php
if ($gia > 400000):
echo "Khóa học cao cấp";
else:
echo "Khóa học phổ thông";
endif;
?>
Nhìn qua thì có vẻ thừa. Nhưng khi trộn với HTML thì nó dễ đọc hơn hẳn:
<?php if (isset($_SESSION['user_id'])): ?>
<li><a href="?ctrl=user&act=logout">Đăng xuất</a></li>
<?php else: ?>
<li><a href="?ctrl=user&act=login">Đăng nhập</a></li>
<?php endif; ?>
So với cách dùng ngoặc nhọn:
<?php if (isset($_SESSION['user_id'])) { ?>
<li><a href="?ctrl=user&act=logout">Đăng xuất</a></li>
<?php } else { ?>
<li><a href="?ctrl=user&act=login">Đăng nhập</a></li>
<?php } ?>
Với đoạn ngắn thì khác biệt không nhiều. Nhưng khi khối HTML dài 30 dòng, dấu } lạc lõng giữa HTML rất khó lần ra nó đóng cho cái gì, còn endif; thì nói thẳng ra là nó đóng cho if.
Toàn bộ 22 file trong
Views/của EzCode dùng cú pháp này. Bạn bắt buộc phải quen với nó.
2.7. Câu lệnh switch
Khi phải so sánh một biến với nhiều giá trị cố định, switch gọn hơn chuỗi if elseif:
<?php
$trangThai = "pending";
switch ($trangThai) {
case 'completed':
echo 'Thành công';
break;
case 'pending':
echo 'Đang xử lý';
break;
default:
echo 'Không xác định';
}
?>
Ba điểm phải nhớ:
break;là bắt buộc. Thiếu nó, PHP sẽ chạy tiếp xuống cáccasebên dưới. Đây gọi là "rơi xuyên" (fall-through) và gần như luôn là lỗi.default:chạy khi khôngcasenào khớp. Luôn nên có, để dữ liệu lạ không làm trang trống trơn.switchdùng so sánh lỏng==, không phải===.
2.8. Vòng lặp for và while
<?php
// for: khi biết trước số lần lặp
for ($i = 1; $i <= 5; $i++) {
echo "Bài học số $i <br>";
}
// while: khi lặp tới khi điều kiện sai
$dem = 1;
while ($dem <= 5) {
echo "Bài học số $dem <br>";
$dem++;
}
?>
Cấu trúc for gồm ba phần cách nhau bởi dấu chấm phẩy: khởi tạo, điều kiện tiếp tục, và bước nhảy sau mỗi vòng.
Trong thực tế, EzCode gần như không dùng for hay while. Nó dùng foreach để duyệt mảng — Bài 3 sẽ dạy. Nhưng bạn vẫn cần biết hai vòng lặp này vì chúng là kiến thức nền.
2.9. Hàm
Hàm là một đoạn code có tên, đóng gói lại để gọi nhiều lần.
<?php
function tinhGiamGia($gia, $phanTram) {
$soTienGiam = $gia * $phanTram / 100;
return $gia - $soTienGiam;
}
echo tinhGiamGia(500000, 20); // 400000
echo tinhGiamGia(300000, 10); // 270000
?>
functionlà từ khóa khai báo.$giavà$phanTramlà tham số — dữ liệu truyền vào.returntrả kết quả về cho nơi gọi. Gặpreturnlà hàm dừng ngay lập tức.
Tham số mặc định: bạn có thể cho tham số một giá trị sẵn, khi gọi không truyền thì nó dùng giá trị đó.
<?php
function chao($ten, $loiChao = "Xin chào") {
return "$loiChao, $ten!";
}
echo chao("Hoàng"); // Xin chào, Hoàng!
echo chao("Hoàng", "Chúc mừng"); // Chúc mừng, Hoàng!
?>
Tham số có giá trị mặc định phải đặt sau cùng. EzCode dùng kỹ thuật này ở TeacherController::students($course_id = null) mà bạn sẽ gặp ở Bài 19: một hàm phục vụ được hai trang khác nhau tùy có truyền tham số hay không.
2.10. number_format() — định dạng số
Đây là hàm giải quyết đúng vấn đề nêu ở mục 1.
<?php
echo number_format(300000); // 300,000
echo number_format(1234567); // 1,234,567
?>
Mặc định nó dùng dấu phẩy ngăn cách hàng nghìn theo kiểu Anh Mỹ. Người Việt quen dùng dấu chấm. Muốn vậy thì truyền đủ bốn tham số:
<?php
echo number_format(300000, 0, ',', '.'); // 300.000
?>
Bốn tham số lần lượt là: số cần định dạng, số chữ số thập phân, ký tự ngăn phần thập phân, ký tự ngăn hàng nghìn.
EzCode chỉ dùng dạng một tham số number_format($course['price']), nên trang web hiện 300,000 chứ không phải 300.000. Đây không phải lỗi, chỉ là lựa chọn của tác giả.
2.11. date() và strtotime() — định dạng ngày
Cơ sở dữ liệu lưu ngày dưới dạng 2025-08-04 09:05:37. Người Việt muốn đọc 04/08/2025.
<?php
$ngayTuDb = "2025-08-04 09:05:37";
echo date('d/m/Y', strtotime($ngayTuDb)); // 04/08/2025
echo date('d/m/Y H:i', strtotime($ngayTuDb)); // 04/08/2025 09:05
?>
strtotime()đổi chuỗi ngày thành một con số mà PHP hiểu được.date()đổi con số đó thành chuỗi theo khuôn dạng bạn chỉ định.
Bảng ký hiệu hay dùng:
| Ký hiệu | Nghĩa | Ví dụ |
|---|---|---|
d |
Ngày, 2 chữ số | 04 |
m |
Tháng, 2 chữ số | 08 |
Y |
Năm, 4 chữ số | 2025 |
H |
Giờ, 24 giờ | 09 |
i |
Phút | 05 |
Cặp date('d/m/Y', strtotime(...)) xuất hiện ở Views/user_course.php, Views/payment_history.php và Views/teacher_students.php.
3. Áp dụng vào EzCode
3.1. switch để dịch trạng thái thanh toán
Mở file Views/payment_history.php và tìm tới dòng 52.
📁
Views/payment_history.php— đọc trước
switch($payment['status']) {
case 'completed':
echo 'Thành công';
break;
case 'pending':
echo 'Đang xử lý';
break;
case 'failed':
echo 'Thất bại';
break;
case 'cancelled':
echo 'Đã hủy';
break;
default:
echo 'Không xác định';
}
Đoạn này thụt lề rất sâu vì nó nằm lồng bên trong nhiều lớp thẻ HTML. Khi bạn gõ lại ở Bài 21, hãy giữ nguyên độ thụt lề đó.
Cách nó hoạt động:
$payment['status']là giá trị lấy từ cộtstatuscủa bảngpayments.- Cột này được khai báo trong
ezcode.sqllàenum('pending','completed','failed','cancelled'), nghĩa là nó chỉ nhận đúng bốn giá trị đó. switchso khớp và in ra tiếng Việt tương ứng.defaultin "Không xác định". Vì cột làenumnên về lý thuyếtdefaultkhông bao giờ chạy tới, nhưng cột này cho phépNULL, và nếu ai đó thêm giá trị mới vàoenumsau này thìdefaultcứu bạn khỏi một ô trống.
Hãy để ý: mỗi case đều có break;. Nếu bỏ break; ở case 'completed', một đơn hàng thành công sẽ in ra Thành côngĐang xử lý vì PHP rơi xuyên xuống case dưới.
3.2. number_format() để hiển thị giá
Mở Views/page_home.php, dòng 18.
📁
Views/page_home.php— đọc trước
<p>Giá: <?= number_format($course['price'])?></p>
Một dòng ngắn nhưng gói gọn ba thứ bạn vừa học ở Bài 1 và Bài 2:
- Cú pháp rút gọn
<?= ?>thay cho<?php echo ?>. - Hàm
number_format()biến300000thành300,000. $course['price']— lấy phần tử của mảng, Bài 3 sẽ giải thích.
Chú ý là không có dấu cách giữa ) và ?>. Tác giả viết liền. Khi gõ lại ở Bài 10 bạn cứ viết y hệt.
4. Chạy thử
Tạo file hoc/bai02.php:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$gia = 300000;
$trangThai = "pending";
$ngayMua = "2025-08-04 09:05:37";
echo "<h2>Thử định dạng dữ liệu</h2>";
echo "<p>Giá gốc trong database: " . $gia . "</p>";
echo "<p>Giá hiển thị kiểu Anh Mỹ: " . number_format($gia) . "</p>";
echo "<p>Giá hiển thị kiểu Việt Nam: " . number_format($gia, 0, ',', '.') . "đ</p>";
echo "<p>Ngày gốc trong database: " . $ngayMua . "</p>";
echo "<p>Ngày hiển thị: " . date('d/m/Y H:i', strtotime($ngayMua)) . "</p>";
echo "<p>Trạng thái gốc: " . $trangThai . "</p>";
echo "<p>Trạng thái hiển thị: ";
switch ($trangThai) {
case 'completed':
echo 'Thành công';
break;
case 'pending':
echo 'Đang xử lý';
break;
default:
echo 'Không xác định';
}
echo "</p>";
?>
Mở http://localhost/EZCode_khoahoc/hoc/bai02.php.
Kết quả đúng:
Giá gốc trong database: 300000
Giá hiển thị kiểu Anh Mỹ: 300,000
Giá hiển thị kiểu Việt Nam: 300.000đ
Ngày gốc trong database: 2025-08-04 09:05:37
Ngày hiển thị: 04/08/2025 09:05
Trạng thái gốc: pending
Trạng thái hiển thị: Đang xử lý
Hãy thử đổi $trangThai thành "abc" rồi tải lại — bạn phải thấy "Không xác định", đó là default đang làm việc.
5. Bài tập
Bài tập 1
Viết hàm dinhDangGia($so) trả về chuỗi giá tiền theo kiểu Việt Nam: dấu chấm ngăn hàng nghìn và có chữ đ ở cuối. Ví dụ dinhDangGia(300000) trả về 300.000đ.
Gọi thử hàm với ba giá trị: 300000, 25000, 1500000.
Bài tập 2
Viết hàm tenTrangThai($ma) nhận vào một trong bốn mã pending, completed, failed, cancelled và trả về (không phải echo) chuỗi tiếng Việt tương ứng. Phải có default trả về "Không xác định".
Gọi thử với cả bốn mã và thêm một mã sai như "xyz" để kiểm tra default.
Gợi ý: khác với đoạn trong
payment_history.phpdùngechobên trongswitch, bài này bạn dùngreturn. Khi gặpreturn, hàm dừng ngay, nên bạn không cầnbreak;nữa. Hãy thử cả hai cách để thấy sự khác biệt.
Bài tập 3
Dùng vòng lặp for in ra bảng cửu chương 5 dưới dạng bảng HTML có viền, gồm hai cột: phép tính và kết quả. Ví dụ dòng đầu là 5 x 1 và 5.
6. Đáp án
Đáp án bài tập 1
📄 hoc/bai02_bt1.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
function dinhDangGia($so) {
return number_format($so, 0, ',', '.') . 'đ';
}
echo "<p>" . dinhDangGia(300000) . "</p>"; // 300.000đ
echo "<p>" . dinhDangGia(25000) . "</p>"; // 25.000đ
echo "<p>" . dinhDangGia(1500000) . "</p>"; // 1.500.000đ
?>
Điểm mấu chốt: hàm này đóng gói một quy tắc hiển thị lại một chỗ. Nếu sau này công ty đổi ý muốn hiện VNĐ thay vì đ, bạn chỉ sửa một dòng trong hàm thay vì đi sửa 15 file View.
Đây chính là điều EzCode chưa làm: tác giả gọi number_format() rải rác ở 12 chỗ khác nhau trong các View, chỗ thì có chữ đ chỗ thì không. Bạn có thể tự thấy sự thiếu nhất quán đó khi so Views/page_home.php (không có đ) với Views/teacher_course.php (có đ).
Đáp án bài tập 2
📄 hoc/bai02_bt2.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
function tenTrangThai($ma) {
switch ($ma) {
case 'completed':
return 'Thành công';
case 'pending':
return 'Đang xử lý';
case 'failed':
return 'Thất bại';
case 'cancelled':
return 'Đã hủy';
default:
return 'Không xác định';
}
}
echo "<p>completed -> " . tenTrangThai('completed') . "</p>";
echo "<p>pending -> " . tenTrangThai('pending') . "</p>";
echo "<p>failed -> " . tenTrangThai('failed') . "</p>";
echo "<p>cancelled -> " . tenTrangThai('cancelled') . "</p>";
echo "<p>xyz -> " . tenTrangThai('xyz') . "</p>";
?>
Vì sao không cần break;? Vì return làm hàm kết thúc ngay lập tức, không có cơ hội nào để rơi xuyên xuống case dưới. Còn trong payment_history.php, tác giả dùng echo — echo chỉ in ra rồi chạy tiếp, nên bắt buộc phải có break; để chặn lại.
Nếu bạn thử xóa hết break; trong phiên bản dùng echo, gọi với pending sẽ in ra Đang xử lýThất bạiĐã hủyKhông xác định.
Đáp án bài tập 3
📄 hoc/bai02_bt3.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Bảng cửu chương 5</title>
</head>
<body>
<h2>Bảng cửu chương 5</h2>
<table border="1" cellpadding="8">
<tr>
<th>Phép tính</th>
<th>Kết quả</th>
</tr>
<?php for ($i = 1; $i <= 10; $i++): ?>
<tr>
<td>5 x <?= $i ?></td>
<td><?= 5 * $i ?></td>
</tr>
<?php endfor; ?>
</table>
</body>
</html>
Chú ý ba điểm:
- Vòng
fordùng cú pháp thay thếfor: ... endfor;— cùng một ý tưởng vớiif: ... endif;ở mục 2.6, và cũng dễ đọc hơn khi trộn HTML. - Trong
<td>dùng cú pháp rút gọn<?= ?>. <?= 5 * $i ?>cho thấy bạn có thể đặt cả một biểu thức tính toán vào trong<?= ?>, không nhất thiết phải là biến.
Cách bố trí này — vòng lặp bao quanh một khối HTML lặp lại — chính là khuôn mẫu của mọi View trong EzCode. Bài 3 sẽ đổi for thành foreach và bạn đã sẵn sàng đọc Views/page_home.php.
➡️ Bài tiếp theo: Bài 3 — Mảng và nhúng PHP vào HTML
⬅️ Về mục lục · Bài trước
All rights reserved