0

EzCode # Bài 3 — Mảng và nhúng PHP vào HTML

Học xong bài này bạn sẽ:

  • Đọc hiểu được $course['title'] — cách dữ liệu từ database đi vào code PHP
  • Dùng foreach in ra một danh sách dài mà chỉ viết HTML một lần
  • Đọc được trọn vẹn file Views/page_home.php của EzCode

Cần học trước: Bài 1, Bài 2


1. Vấn đề đặt ra

Trang chủ EzCode hiện 14 khóa học. Mỗi khóa là một thẻ giống hệt nhau: ảnh bìa, tên khóa, giá, nút Đăng ký. Chỉ khác nội dung bên trong.

Nếu viết bằng HTML thuần, bạn phải chép đoạn này 14 lần:

<div class="course">
  <img src="..." alt="..." />
  <h3>Khóa học HTML CSS cơ bản</h3>
  <p>Giá: 300,000</p>
  <a href="..." class="btn">Đăng ký</a>
</div>

Ba vấn đề lập tức xuất hiện:

  1. Giảng viên thêm khóa học thứ 15 thì sao? Bạn phải mở file HTML sửa tay. Website mà cần lập trình viên sửa tay mỗi lần thêm dữ liệu thì vô dụng.
  2. Muốn đổi màu nút Đăng ký? Bạn phải sửa 14 chỗ. Sót một chỗ là giao diện lệch.
  3. Dữ liệu thật nằm trong database, không nằm trong file HTML.

Giải pháp gồm hai phần, và bài này dạy cả hai:

  • Mảng — cấu trúc giữ nhiều dữ liệu cùng lúc, chính là hình dạng mà database trả về.
  • foreach — vòng lặp duyệt qua mảng, viết HTML một lần nhưng in ra bao nhiêu lần cũng được.

2. Kiến thức mới

2.1. Mảng chỉ số

Mảng là một cái hộp chứa nhiều giá trị, mỗi giá trị có một số thứ tự gọi là chỉ số (index).

<?php
$giaCacKhoa = [300000, 500000, 400000];

echo $giaCacKhoa[0];      // 300000
echo $giaCacKhoa[1];      // 500000
echo $giaCacKhoa[2];      // 400000
?>

Chỉ số bắt đầu từ 0, không phải 1. Phần tử đầu tiên là [0]. Đây là chỗ người mới hay nhầm.

Thêm phần tử vào cuối mảng:

<?php
$gia = [];               // mảng rỗng
$gia[] = 300000;         // giờ có 1 phần tử ở chỉ số 0
$gia[] = 500000;         // giờ có 2 phần tử
?>

Cú pháp $mang[] = giaTri; này xuất hiện trong Controllers/UserController.php khi tác giả gom thông báo lỗi: $errors[] = "Tên không được để trống";

2.2. Mảng kết hợp

Thay vì đánh số, bạn có thể đặt tên cho từng ô. Mảng như vậy gọi là mảng kết hợp (associative array).

<?php
$course = [
    'id'    => 1,
    'title' => 'Khóa học HTML CSS cơ bản',
    'price' => 300000,
    'image' => 'kh-lap-trinh-web-html-css.webp'
];

echo $course['title'];      // Khóa học HTML CSS cơ bản
echo $course['price'];      // 300000
?>

Ký hiệu => đọc là "ứng với". Bên trái là khóa (key), bên phải là giá trị (value).

Đây là điều quan trọng nhất của cả bài học.

Khi bạn hỏi database "cho tôi khóa học có id bằng 1", PHP trả về đúng một mảng kết hợp như trên. Mỗi khóa của mảng chính là tên một cột trong bảng.

Bảng courses trong ezcode.sql có các cột id, teacher_id, category_id, title, description, price, image, status. Vì vậy một dòng lấy từ bảng đó sẽ là mảng có đúng 8 khóa mang đúng 8 tên đó.

Bây giờ bạn đã hiểu vì sao code EzCode viết $course['title'] chứ không phải $course->title hay $course[3].

2.3. Mảng lồng nhau

Database không trả về một dòng, nó trả về nhiều dòng. Nhiều mảng kết hợp gộp lại thành một mảng chỉ số:

<?php
$coursesList = [
    [
        'id'    => 1,
        'title' => 'Khóa học HTML CSS cơ bản',
        'price' => 300000
    ],
    [
        'id'    => 2,
        'title' => 'Flutter cho người mới bắt đầu',
        'price' => 500000
    ]
];

echo $coursesList[0]['title'];      // Khóa học HTML CSS cơ bản
echo $coursesList[1]['price'];      // 500000
?>

Cách đọc $coursesList[1]['price']: lấy phần tử thứ 1 của $coursesList (là một mảng kết hợp), rồi lấy ô price của mảng đó.

Muốn nhìn toàn bộ cấu trúc, dùng print_r():

<?php
echo "<pre>";
print_r($coursesList);
echo "</pre>";
?>

Kết quả:

Array
(
    [0] => Array
        (
            [id] => 1
            [title] => Khóa học HTML CSS cơ bản
            [price] => 300000
        )

    [1] => Array
        (
            [id] => 2
            [title] => Flutter cho người mới bắt đầu
            [price] => 500000
        )
)

Thẻ <pre> giữ nguyên xuống dòng và khoảng trắng, không có nó thì kết quả dồn thành một cục khó đọc. Hãy nhớ mẹo echo "<pre>"; print_r(...); echo "</pre>"; — bạn sẽ dùng nó rất nhiều khi gỡ lỗi.

2.4. foreach — duyệt mảng

<?php
$coursesList = [
    ['title' => 'HTML CSS',  'price' => 300000],
    ['title' => 'Flutter',   'price' => 500000],
    ['title' => 'Python',    'price' => 300000]
];

foreach ($coursesList as $course) {
    echo $course['title'] . " — " . number_format($course['price']) . "<br>";
}
?>

Kết quả:

HTML CSS — 300,000
Flutter — 500,000
Python — 300,000

Cách đọc câu foreach ($coursesList as $course): "với mỗi phần tử trong $coursesList, hãy gọi nó là $course rồi làm những việc sau".

  • $coursesList là mảng cần duyệt — luôn đặt tên số nhiều.
  • $course là biến tạm giữ từng phần tử — luôn đặt tên số ít.

Quy ước số nhiều với số ít này được EzCode dùng nhất quán: $coursesList$course, $lessionsList$lessions, $students$student, $categories$category.

Duyệt cả khóa lẫn giá trị:

<?php
$course = ['title' => 'HTML CSS', 'price' => 300000];

foreach ($course as $khoa => $giaTri) {
    echo "$khoa = $giaTri <br>";
}
?>

Kết quả:

title = HTML CSS
price = 300000

Dạng này ít gặp hơn, nhưng Models/VNPay.php dùng nó ở Bài 21 để tạo chữ ký thanh toán.

2.5. count(), empty(), isset()

count($mang) đếm số phần tử:

<?php
$coursesList = [1, 2, 3];
echo count($coursesList);      // 3
?>

empty($bien) trả về true khi biến "rỗng". PHP coi những thứ sau là rỗng: mảng không có phần tử nào, chuỗi rỗng "", số 0, chuỗi "0", null, và false.

<?php
$rong = [];
if (empty($rong)) {
    echo "Không tìm thấy khóa học nào";
}
?>

isset($bien) trả về true khi biến tồn tại và khác null.

Khác biệt quan trọng giữa hai hàm:

<?php
$so = 0;

var_dump(isset($so));      // bool(true)  — biến có tồn tại
var_dump(empty($so));      // bool(true)  — nhưng giá trị 0 bị coi là rỗng

var_dump(isset($chuaCo));  // bool(false) — biến chưa hề tồn tại
var_dump(empty($chuaCo));  // bool(true)
?>

Điểm mấu chốt: isset() hỏi "có tồn tại không", empty() hỏi "có giá trị đáng kể không".

Và một điểm nữa rất hữu ích: isset()empty() không báo lỗi khi biến chưa tồn tại. Mọi cách kiểm tra khác đều báo Warning: Undefined variable. Đây là lý do EzCode dùng isset($_SESSION['user_id']) khắp nơi thay vì $_SESSION['user_id'] != null.

2.6. Nhúng PHP vào HTML: cú pháp foreach:endforeach;

Giống như if:endif; ở Bài 2, foreach cũng có cú pháp thay thế:

<?php foreach ($coursesList as $course) : ?>
    <div class="course">
        <h3><?= $course['title'] ?></h3>
    </div>
<?php endforeach; ?>

Hãy nhìn kỹ cấu trúc này, vì nó là khuôn mẫu của mọi file View trong EzCode:

  1. Mở thẻ PHP, viết foreach (...) :, đóng thẻ PHP.
  2. Viết HTML bình thường, dùng <?= ?> ở chỗ cần chèn dữ liệu.
  3. Mở thẻ PHP, viết endforeach;, đóng thẻ PHP.

Phần HTML ở giữa được in ra một lần cho mỗi phần tử của mảng.

Vì sao không dùng echo với chuỗi HTML dài? Bạn hoàn toàn có thể viết:

<?php
foreach ($coursesList as $course) {
    echo '<div class="course"><h3>' . $course['title'] . '</h3></div>';
}
?>

Nhưng cách này có ba nhược điểm: trình soạn thảo không tô màu HTML bên trong chuỗi, bạn phải cẩn thận với dấu nháy đơn và nháy kép, và khi HTML dài 20 dòng thì đọc không nổi. Cách nhúng ở trên giữ HTML là HTML.


3. Áp dụng vào EzCode

Mở file Views/page_home.php. Đây là file View đầu tiên bạn đọc trong giáo trình, và nó chỉ có 26 dòng.

Phần cốt lõi nằm ở dòng 14 đến 21:

📁 Views/page_home.phpđọc trước

    <?php foreach ($coursesList as $course) : ?>
      <div class="course">
        <img src="public/img/courses/<?= $course['image']?>" alt="<?= $course['title']?>" />
        <h3><?= $course['title']?></h3>
        <p>Giá: <?= number_format($course['price'])?></p>
        <a href="?ctrl=course&act=detail&id=<?=$course['id']?>" class="btn">Đăng ký</a>
      </div>
    <?php endforeach; ?>

Tám dòng này gói gọn mọi thứ bạn vừa học. Ta mổ xẻ từng phần.

Dòng 1 — foreach ($coursesList as $course) :

$coursesList đến từ đâu? Nó không được khai báo trong file này. Nó được tạo ra trong Controllers/PageController.php trước khi file View được nạp vào. Cơ chế truyền biến từ Controller sang View sẽ được giải thích ở Bài 10; bây giờ chỉ cần biết $coursesList là mảng lồng nhau chứa 14 khóa học lấy từ bảng courses.

Dòng 3 — thẻ img

<img src="public/img/courses/<?= $course['image']?>" alt="<?= $course['title']?>" />

Đường dẫn ảnh được ghép từ hai phần: chuỗi cố định public/img/courses/ cộng với giá trị cột image. Với khóa học đầu tiên, $course['image']kh-lap-trinh-web-html-css.webp, nên HTML gửi về trình duyệt sẽ là:

<img src="public/img/courses/kh-lap-trinh-web-html-css.webp" alt="Khóa học HTML CSS cơ bản" />

Dòng 6 — liên kết Đăng ký

<a href="?ctrl=course&act=detail&id=<?=$course['id']?>" class="btn">Đăng ký</a>

Địa chỉ được ghép động từ id của từng khóa. Khóa số 1 cho ra ?ctrl=course&act=detail&id=1, khóa số 2 cho ra id=2. Ba tham số ctrl, act, id chính là cách EzCode định tuyến — Bài 4 sẽ giới thiệu, Bài 9 sẽ dựng.

Chú ý ở dòng này tác giả viết <?=$course['id']?> không có dấu cách sau <?=, khác với các dòng trên. Cả hai cách đều chạy. Khi gõ lại ở Bài 10, bạn cứ chép y hệt.

⚠️ Lưu ý: Đoạn code trên có hai vấn đề mà bạn sẽ sửa ở Bài 22.

Thứ nhất, <?= $course['title']?> in thẳng dữ liệu từ database ra HTML mà không lọc. Nếu một giảng viên đặt tên khóa học là <script>alert(1)</script> thì đoạn mã đó sẽ chạy trên máy của mọi người xem trang chủ. Đây là lỗ hổng XSS, là lỗi số 10 trong Bài 22.

Thứ hai, đường dẫn public/img/courses/ không khớp với vị trí thật của ảnh mẫu — chúng nằm ở public/img/. Vì vậy 12 trong số 14 khóa học sẽ bị vỡ ảnh. Đây là lỗi số 17 trong Bài 22.

Ở giai đoạn này bạn cứ đọc và gõ y hệt để khớp với repo. Sửa sớm sẽ làm code của bạn lệch với giáo trình.


4. Chạy thử

Tạo file hoc/bai03.php. Bài này bạn tự khai báo mảng bằng tay để mô phỏng dữ liệu database — Bài 8 mới lấy dữ liệu thật.

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

$coursesList = [
    ['id' => 1, 'title' => 'Khóa học HTML CSS cơ bản',    'price' => 300000],
    ['id' => 2, 'title' => 'Flutter cho người mới',       'price' => 500000],
    ['id' => 3, 'title' => 'Python cơ bản',               'price' => 300000],
];
?>
<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <title>Bài 3 - Mảng và foreach</title>
</head>
<body>
    <h2>Khóa học nổi bật</h2>

    <?php foreach ($coursesList as $course) : ?>
        <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">
            <h3><?= $course['title'] ?></h3>
            <p>Giá: <?= number_format($course['price']) ?></p>
            <a href="?ctrl=course&act=detail&id=<?= $course['id'] ?>">Đăng ký</a>
        </div>
    <?php endforeach; ?>

    <hr>
    <h3>Nhìn vào cấu trúc mảng</h3>
    <pre><?php print_r($coursesList); ?></pre>
</body>
</html>

Mở http://localhost/EZCode_khoahoc/hoc/bai03.php.

Kết quả đúng: ba khối viền xám, mỗi khối có tên khóa học, giá đã định dạng và một liên kết Đăng ký. Bên dưới là cấu trúc mảng in ra bởi print_r.

Việc cần làm tiếp: bấm Ctrl + U để xem mã nguồn trang. Bạn sẽ thấy ba khối <div> đã được in ra đầy đủ, không còn dấu vết foreach nào. Đây là bằng chứng cho quy trình 5 bước ở Bài 1: PHP chạy ở máy chủ, trình duyệt chỉ nhận HTML.

Thử thêm phần tử thứ tư vào mảng $coursesList rồi tải lại. Trang tự có thêm khối thứ tư mà bạn không phải viết thêm dòng HTML nào. Đó chính là điều bài học này muốn bạn thấy.


5. Bài tập

Bài tập 1

Tạo hoc/bai03_bt1.php chứa mảng 5 học viên, mỗi người là mảng kết hợp có ba khóa name, email, phone.

Dùng foreach in ra một bảng HTML có viền, gồm bốn cột: số thứ tự, họ tên, email, số điện thoại.

Gợi ý cho cột số thứ tự: dùng dạng foreach ($mang as $chiSo => $phanTu) rồi in $chiSo + 1.

Bài tập 2

Vẫn trong file trên, bọc phần bảng bằng một câu điều kiện: nếu mảng rỗng thì in ra dòng chữ "Chưa có học viên nào" thay vì in bảng trống.

Thử nghiệm bằng cách tạm đổi mảng thành $students = []; rồi tải lại trang, sau đó khôi phục.

Gợi ý: dùng cấu trúc <?php if (empty($students)): ?> ... <?php else: ?> ... <?php endif; ?>. Đây đúng là cách Views/course_list.phpViews/teacher_students.php đang làm.

Bài tập 3

Tạo hoc/bai03_bt3.php chứa mảng khóa học như phần Chạy thử. Dùng foreach tính tổng giá của tất cả khóa học, rồi in ra dòng:

Tổng giá 3 khóa học: 1,100,000

Số 3 phải lấy từ count(), không được viết cứng.


6. Đáp án

Đáp án bài tập 1 và 2

📄 hoc/bai03_bt1.php

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

$students = [
    ['name' => 'Trần Thị B',   'email' => 'student1@example.com', 'phone' => '0987654321'],
    ['name' => 'Phạm Văn C',   'email' => 'student2@example.com', 'phone' => '0901122334'],
    ['name' => 'Lê Thị D',     'email' => 'user4@example.com',    'phone' => '0901000001'],
    ['name' => 'Đỗ Văn G',     'email' => 'user7@example.com',    'phone' => '0901000004'],
    ['name' => 'Ngô Thị L',    'email' => 'user12@example.com',   'phone' => '0901000009'],
];
?>
<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <title>Danh sách học viên</title>
</head>
<body>
    <h2>Danh sách học viên</h2>

    <?php if (empty($students)): ?>
        <p>Chưa có học viên nào</p>
    <?php else: ?>
        <table border="1" cellpadding="8">
            <tr>
                <th>STT</th>
                <th>Họ và tên</th>
                <th>Email</th>
                <th>Số điện thoại</th>
            </tr>
            <?php foreach ($students as $chiSo => $student): ?>
                <tr>
                    <td><?= $chiSo + 1 ?></td>
                    <td><?= $student['name'] ?></td>
                    <td><?= $student['email'] ?></td>
                    <td><?= $student['phone'] ?></td>
                </tr>
            <?php endforeach; ?>
        </table>
    <?php endif; ?>
</body>
</html>

Vì sao $chiSo + 1? Vì mảng đánh số từ 0, mà người dùng đếm từ 1.

Cấu trúc if (empty(...)) : ... else: ... endif; bao quanh một bảng chính là khuôn mẫu bạn sẽ gõ lại ở Bài 19 khi làm Views/teacher_students.php.

Đáp án bài tập 3

📄 hoc/bai03_bt3.php

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

$coursesList = [
    ['id' => 1, 'title' => 'Khóa học HTML CSS cơ bản', 'price' => 300000],
    ['id' => 2, 'title' => 'Flutter cho người mới',    'price' => 500000],
    ['id' => 3, 'title' => 'Python cơ bản',            'price' => 300000],
];

$tongGia = 0;
foreach ($coursesList as $course) {
    $tongGia += $course['price'];
}

echo "<p>Tổng giá " . count($coursesList) . " khóa học: " . number_format($tongGia) . "</p>";
?>

Ba điểm đáng chú ý:

  1. $tongGia = 0; phải khai báo trước vòng lặp. Nếu đặt bên trong, mỗi vòng nó lại bị gán về 0 và kết quả cuối cùng chỉ là giá của khóa cuối.
  2. $tongGia += $course['price']; là dạng rút gọn của $tongGia = $tongGia + $course['price']; mà bạn đã học ở Bài 2.
  3. count($coursesList) lấy số lượng động. Thêm khóa học thứ tư vào mảng thì dòng chữ tự đổi thành "Tổng giá 4 khóa học".

Cách viết ngắn hơn. PHP có sẵn hai hàm làm đúng việc này:

<?php
$tongGia = array_sum(array_column($coursesList, 'price'));
echo number_format($tongGia);
?>
  • array_column($coursesList, 'price') rút cột price ra thành mảng phẳng [300000, 500000, 300000].
  • array_sum() cộng mọi phần tử lại.

Cặp hàm này được EzCode dùng thật trong Views/teacher_dashboard.php ở Bài 17:

<?= number_format(array_sum(array_column($teacherCourses ?? [], 'price'))) ?>đ

Bây giờ bạn đã đọc hiểu được dòng đó rồi.


➡️ Bài tiếp theo: Bài 4 — Form, $_GET, $_POST, header()

⬅️ Về mục lục · Bài trước


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí