EzCode # Bài 5 — Session và thông báo flash
Học xong bài này bạn sẽ:
- Hiểu vì sao website "nhớ" được bạn đã đăng nhập dù mỗi trang là một lần chạy riêng
- Dùng
$_SESSIONđể lưu trạng thái người dùng qua nhiều trang- Viết được mẫu thông báo flash mà 15 file View của EzCode đang dùng
1. Vấn đề đặt ra
Ở Bài 4 bạn đã làm được form đăng nhập: nhận email và mật khẩu, kiểm tra, rồi chuyển sang trang khác.
Nhưng có một vấn đề chưa giải quyết. Bạn đăng nhập ở trang A, rồi bấm sang trang B. Trang B không hề biết bạn là ai.
Lý do nằm ở bản chất của HTTP: nó là giao thức không trạng thái (stateless).
Hãy hình dung một nhân viên bán vé có trí nhớ chỉ kéo dài đúng một câu hỏi. Bạn hỏi "giá vé bao nhiêu", anh ta trả lời. Bạn hỏi tiếp "vậy cho tôi hai vé", anh ta hỏi lại "hai vé gì?" — vì đã quên sạch câu trước.
Máy chủ web hoạt động y hệt:
- Trình duyệt hỏi trang A. Máy chủ chạy PHP, trả HTML về, rồi quên hết mọi biến.
- Trình duyệt hỏi trang B. Máy chủ chạy PHP từ đầu như thể chưa từng gặp bạn.
Mọi biến bạn tạo ra ở trang A đều biến mất khi trang A trả về xong. Không có cách nào để $tenNguoiDung ở trang A còn sống sang trang B.
Vậy làm sao Facebook nhớ bạn đã đăng nhập? Làm sao giỏ hàng của bạn không bị mất khi chuyển trang?
Câu trả lời là session.
2. Kiến thức mới
2.1. Session hoạt động thế nào
Ý tưởng rất giống việc gửi đồ ở siêu thị:
- Bạn gửi túi xách, nhân viên đưa bạn một cái thẻ số 47.
- Túi của bạn được cất vào ô số 47 trong kho.
- Bạn đi mua sắm. Nhân viên không cần nhớ mặt bạn.
- Lúc về, bạn đưa thẻ số 47, nhân viên lấy đúng túi của bạn ra.
Session làm y như vậy:
- Lần đầu bạn vào website, PHP tạo một mã phiên ngẫu nhiên, ví dụ
a3f8c2e91b.... - PHP gửi mã đó về trình duyệt dưới dạng cookie tên
PHPSESSID. Trình duyệt tự lưu. - PHP tạo một file trên máy chủ mang tên chứa mã đó, để cất dữ liệu của bạn.
- Mỗi lần bạn mở trang mới, trình duyệt tự động gửi kèm cookie đó.
- PHP đọc mã, mở đúng file, và bạn lại có toàn bộ dữ liệu cũ.
Điểm mấu chốt: dữ liệu nằm trên máy chủ, chỉ có mã phiên nằm ở trình duyệt. Người dùng không sửa được nội dung session, họ chỉ giữ cái thẻ.
2.2. session_start()
Mọi file muốn dùng session phải gọi lệnh này trước tiên:
<?php
session_start();
?>
Ba quy tắc:
- Phải gọi trước khi in bất kỳ nội dung nào. Vì
session_start()cần gửi cookie, mà cookie nằm trong phần đầu của phản hồi. Đây chính là lỗi "headers already sent" bạn đã học ở Bài 4, mục 2.9. - Gọi một lần là đủ cho cả yêu cầu. Gọi lần hai sẽ sinh cảnh báo
Notice: session_start(): Ignoring session_start(). - Mọi file cần session đều phải có nó, trừ khi file đó được
includetừ một file đã gọi rồi.
EzCode gọi
session_start()đúng một lần duy nhất, ở dòng 6 củaindex.php. Vì mọi trang đều đi quaindex.php, nên mọi Controller và mọi View đều dùng được$_SESSIONmà không cần gọi lại. Đây là một ưu điểm của kiến trúc front controller mà Bài 9 sẽ dạy.
2.3. $_SESSION — mảng sống qua nhiều trang
Sau khi gọi session_start(), bạn có mảng kết hợp $_SESSION. Dùng nó y hệt mảng thường:
<?php
session_start();
// Ghi
$_SESSION['user_id'] = 34;
$_SESSION['user_name'] = 'Hoàng Nguyễn';
$_SESSION['user_role'] = 'student';
// Đọc
echo $_SESSION['user_name'];
// Kiểm tra tồn tại
if (isset($_SESSION['user_id'])) {
echo "Đã đăng nhập";
}
// Xóa một khóa
unset($_SESSION['user_name']);
?>
Khác biệt duy nhất so với mảng thường: nội dung của nó sống sót qua các lần tải trang.
EzCode dùng đúng năm khóa trong $_SESSION:
| Khóa | Nội dung | Được ghi ở đâu |
|---|---|---|
user_id |
Số id của người đăng nhập | UserController::login() |
user_name |
Họ tên để hiện trên menu | UserController::login() |
user_role |
student hoặc teacher |
UserController::login() |
success |
Thông báo màu xanh | Nhiều Controller |
error |
Thông báo màu đỏ | Nhiều Controller |
Ba khóa đầu là trạng thái đăng nhập, giữ nguyên suốt phiên. Hai khóa cuối là thông báo flash, chỉ sống một lần — mục 2.5 sẽ giải thích.
2.4. session_destroy() — đăng xuất
<?php
session_start();
session_destroy();
?>
session_destroy() xóa toàn bộ file phiên trên máy chủ. Người dùng lập tức trở thành khách vãng lai.
Cần biết một điểm tinh tế: session_destroy() không xóa ngay mảng $_SESSION trong lần chạy hiện tại. Nếu bạn vẫn echo $_SESSION['user_name'] ở dòng sau, nó vẫn in ra. Chỉ từ lần tải trang tiếp theo mảng mới thực sự rỗng.
Muốn xóa sạch ngay lập tức, cách chuẩn hơn là:
<?php
session_start();
$_SESSION = []; // xóa mảng ngay
session_destroy(); // xóa file phiên
?>
EzCode chỉ gọi session_destroy() rồi chuyển hướng ngay lập tức, nên không gặp vấn đề này.
2.5. Mẫu thông báo flash
Đây là phần quan trọng nhất của bài, vì nó xuất hiện ở 15 file View trong EzCode.
Bài toán: người dùng bấm nút "Cập nhật thông tin". Controller xử lý xong rồi chuyển hướng sang trang khác (theo mẫu PRG mà Bài 16 sẽ nói). Nhưng ta muốn hiện dòng chữ "Cập nhật thành công!" trên trang mới đó.
Không thể dùng biến thường, vì biến chết ngay sau khi chuyển hướng. Cũng không thể để thông báo hiện mãi mãi — người dùng bấm F5 mười lần lại thấy mười lần.
Ta cần một thông báo sống đúng một lần rồi tự biến mất. Đó là "flash message" — nhá lên rồi tắt.
Cách làm gồm hai nửa.
Nửa thứ nhất — Controller ghi thông báo rồi chuyển hướng:
<?php
$_SESSION['success'] = "Cập nhật thông tin thành công!";
header("Location: ?ctrl=user&act=profile");
exit();
?>
Nửa thứ hai — View in ra rồi xóa ngay:
<?php if (isset($_SESSION['success'])): ?>
<div class="thong-bao-xanh">
<?= $_SESSION['success'] ?>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
Dòng unset() là linh hồn của mẫu này. Nó xóa khóa success ngay sau khi in. Lần tải trang sau, isset() trả về false và cả khối bị bỏ qua.
Thử tưởng tượng nếu quên unset(): người dùng cập nhật hồ sơ một lần, rồi mỗi trang họ mở trong suốt phiên đăng nhập đều hiện "Cập nhật thông tin thành công!". Rất phiền.
2.6. Session và cookie khác nhau ra sao
Người mới hay nhầm hai khái niệm này.
| Session | Cookie | |
|---|---|---|
| Dữ liệu nằm ở | Máy chủ | Trình duyệt người dùng |
| Người dùng sửa được không | Không | Có |
| Dung lượng | Rất lớn | Khoảng 4KB |
| Sống bao lâu | Tới khi đóng trình duyệt hoặc hết hạn | Tùy bạn đặt, có thể nhiều năm |
| Dùng cho | Trạng thái đăng nhập, dữ liệu nhạy cảm | Ghi nhớ tùy chọn, chủ đề sáng tối |
Session dùng cookie để lưu mã phiên, nhưng bản thân dữ liệu thì không nằm trong cookie.
Nguyên tắc: cái gì người dùng không được phép sửa thì để trong session. Nếu bạn lưu $_COOKIE['user_role'] = 'student', người dùng chỉ cần mở Developer Tools sửa thành teacher là chiếm được quyền giảng viên. Nhưng $_SESSION['user_role'] thì họ không chạm tới được.
EzCode lưu user_role trong session. Đúng.
3. Áp dụng vào EzCode
3.1. Thông báo flash trong trang đăng nhập
Mở Views/user_login.php, dòng 5 đến 10.
📁
Views/user_login.php— đọc trước
<?php if (isset($_SESSION['success'])): ?>
<div style="background-color: #d4edda; color: #155724; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<?= $_SESSION['success'] ?>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
Sáu dòng này là mẫu flash nguyên bản. Ta lần theo một tình huống thật để thấy nó chạy ra sao:
- Người dùng điền form ở trang đăng ký và bấm nút.
UserController::register()lưu tài khoản thành công, rồi ghi:$_SESSION['success'] = "Đăng ký thành công! Vui lòng đăng nhập.";- Ngay sau đó nó gọi
header("Location: ?ctrl=user&act=login"); exit(); - Trình duyệt tải trang đăng nhập.
index.phpchạy lại từ đầu, gọisession_start(), và$_SESSION['success']vẫn còn đó vì nó nằm trên máy chủ. Views/user_login.phpđược nạp.isset($_SESSION['success'])trả vềtrue, khối màu xanh được in ra.unset()chạy, khóasuccessbị xóa.- Người dùng bấm F5. Lần này
isset()trả vềfalse, không còn thông báo nào.
Đúng một lần, rồi tự biến mất.
Chú ý màu sắc được viết thẳng trong thuộc tính style chứ không đưa vào file CSS. Cặp màu #d4edda với #155724 là chuẩn màu xanh thành công của Bootstrap; cặp #f8d7da với #721c24 là chuẩn màu đỏ báo lỗi. Bạn sẽ thấy đúng hai cặp màu này lặp lại ở cả 15 file View.
⚠️ Lưu ý: Dòng
<?= $_SESSION['success'] ?>in nội dung ra mà không lọc HTML. Trong hầu hết trường hợp thì an toàn, vì các thông báo đều do lập trình viên viết cứng trong code.Nhưng không phải lúc nào cũng vậy. Trong
UserController::updateProfile()có dòng$_SESSION['error'] = implode(", ", $errors);, và các thông báo lỗi này được ghép từ nhiều nguồn. Nếu sau này ai đó thêm một thông báo lỗi có chứa dữ liệu người dùng nhập vào, lỗ hổng XSS sẽ mở ra ngay tại đây.Đây là lỗi số 10 trong Bài 22. Ở bài này bạn cứ đọc và gõ y hệt.
3.2. Đăng xuất
Mở Controllers/UserController.php và tìm phương thức logout(). Đây là phương thức ngắn nhất của cả project.
📁
Controllers/UserController.php— đọc trước
public function logout()
{
session_destroy();
header("Location: ?ctrl=page&act=home");
exit();
}
Ba dòng, mỗi dòng một nhiệm vụ:
session_destroy()— hủy phiên, xóa sạchuser_id,user_name,user_role.header("Location: ...")— bảo trình duyệt về trang chủ.exit()— dừng ngay lập tức, đúng quy tắc bạn học ở Bài 4.
Sau khi phương thức này chạy, menu trên đầu trang sẽ tự đổi từ "Xin chào, Hoàng Nguyễn / Đăng xuất" thành "Đăng ký / Đăng nhập". Lý do là Views/layout_header.php kiểm tra isset($_SESSION['user_id']) mỗi lần được nạp. Bài 15 sẽ làm phần đó.
Phương thức này được gọi qua địa chỉ ?ctrl=user&act=logout, chính là liên kết nút Đăng xuất trên menu.
4. Chạy thử
Thử nghiệm 1 — bộ đếm phiên
Tạo hoc/bai05.php:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
if (!isset($_SESSION['dem'])) {
$_SESSION['dem'] = 0;
}
$_SESSION['dem']++;
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Bài 5 - Session</title>
</head>
<body>
<h2>Bộ đếm phiên</h2>
<p>Bạn đã tải trang này <strong><?= $_SESSION['dem'] ?></strong> lần.</p>
<p>Mã phiên của bạn: <code><?= session_id() ?></code></p>
<p><a href="bai05.php">Tải lại</a> | <a href="bai05_xoa.php">Xóa phiên</a></p>
<hr>
<pre>$_SESSION = <?php print_r($_SESSION); ?></pre>
</body>
</html>
Và hoc/bai05_xoa.php:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
session_destroy();
header("Location: bai05.php");
exit();
?>
Các bước thử:
- Mở
http://localhost/EZCode_khoahoc/hoc/bai05.php. Hiện "1 lần". - Bấm "Tải lại" ba lần. Số đếm lên 2, 3, 4. Biến đã sống sót qua các lần tải trang.
- Ghi lại mã phiên hiển thị trên màn hình.
- Bấm "Xóa phiên". Số đếm về 1, và mã phiên đổi thành chuỗi khác.
- Bấm
F12, sang tab Application (Chrome) hoặc Storage (Firefox), mục Cookies. Bạn sẽ thấy một cookie tênPHPSESSIDchứa đúng mã phiên đó. Đây là cái thẻ gửi đồ. - Xóa tay cookie đó rồi tải lại trang. Số đếm về 1, vì bạn vừa vứt mất thẻ.
- Mở trang này trong cửa sổ ẩn danh. Số đếm bắt đầu lại từ 1, vì đó là một phiên hoàn toàn khác.
Bước 5 và 6 là quan trọng nhất — chúng cho bạn nhìn tận mắt cơ chế mà mục 2.1 mô tả.
5. Bài tập
Ba bài tập này ghép lại thành một hệ thống đăng nhập giả hoàn chỉnh, chưa cần database. Nó là bản thu nhỏ của đúng những gì bạn sẽ làm ở Bài 15.
Bài tập 1
Tạo hoc/bai05_dangnhap.php là form đăng nhập gồm hai ô: tên đăng nhập và mật khẩu.
Tài khoản đúng được viết cứng trong code: tên admin, mật khẩu 123.
- Nếu đúng: ghi
$_SESSION['user_name'] = 'admin';rồi chuyển hướng sanghoc/bai05_trangtrong.php. - Nếu sai: ghi
$_SESSION['error'] = "Tên đăng nhập hoặc mật khẩu không đúng";rồi hiện lại form.
Form phải hiển thị thông báo lỗi theo đúng mẫu flash ở mục 2.5, tức là có unset() sau khi in.
Bài tập 2
Tạo hoc/bai05_trangtrong.php. Trang này chỉ dành cho người đã đăng nhập.
- Nếu chưa có
$_SESSION['user_name']: chuyển hướng ngay về trang đăng nhập bằngheader()kèmexit(). - Nếu đã có: hiện dòng "Xin chào, admin" và một nút Đăng xuất.
Bài tập 3
Tạo hoc/bai05_dangxuat.php hủy phiên rồi quay về trang đăng nhập, kèm thông báo flash màu xanh "Đã đăng xuất thành công".
Lưu ý một cái bẫy: session_destroy() xóa sạch mọi thứ, kể cả thông báo bạn vừa ghi. Hãy tự tìm cách giải quyết trước khi xem đáp án.
6. Đáp án
Đáp án bài tập 1
📄 hoc/bai05_dangnhap.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if ($username === 'admin' && $password === '123') {
$_SESSION['user_name'] = 'admin';
header("Location: bai05_trangtrong.php");
exit();
} else {
$_SESSION['error'] = "Tên đăng nhập hoặc mật khẩu không đúng";
}
}
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Đăng nhập</title>
</head>
<body>
<h2>Đăng nhập</h2>
<?php if (isset($_SESSION['success'])): ?>
<div style="background-color: #d4edda; color: #155724; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<?= $_SESSION['success'] ?>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div style="background-color: #f8d7da; color: #721c24; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<?= $_SESSION['error'] ?>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<form method="POST">
<p>
<label>Tên đăng nhập</label><br>
<input type="text" name="username" value="<?= $_POST['username'] ?? '' ?>">
</p>
<p>
<label>Mật khẩu</label><br>
<input type="password" name="password">
</p>
<button type="submit">Đăng nhập</button>
</form>
</body>
</html>
Hãy so đoạn này với Views/user_login.php thật của EzCode — cấu trúc gần như trùng khớp. Khác biệt duy nhất là EzCode kiểm tra tài khoản trong database thay vì viết cứng, và dùng password_verify() thay vì so sánh chuỗi trực tiếp. Bài 15 sẽ bổ sung hai điều đó.
Đáp án bài tập 2
📄 hoc/bai05_trangtrong.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
if (!isset($_SESSION['user_name'])) {
header("Location: bai05_dangnhap.php");
exit();
}
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Trang trong</title>
</head>
<body>
<h2>Xin chào, <?= $_SESSION['user_name'] ?></h2>
<p>Đây là trang chỉ dành cho người đã đăng nhập.</p>
<p><a href="bai05_dangxuat.php">Đăng xuất</a></p>
</body>
</html>
Bốn dòng chặn quyền ở đầu file là mẫu quan trọng nhất của bài này. Bạn sẽ gõ lại nó gần như nguyên văn ở sáu chỗ khác nhau trong Controllers/UserController.php tại Bài 16:
if (!isset($_SESSION['user_id'])) {
header("Location: ?ctrl=user&act=login");
exit();
}
Hãy tự kiểm chứng: mở hoc/bai05_dangxuat.php để đăng xuất, rồi gõ thẳng địa chỉ hoc/bai05_trangtrong.php vào thanh địa chỉ. Bạn bị đá về trang đăng nhập ngay lập tức.
Đáp án bài tập 3
📄 hoc/bai05_dangxuat.php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
// Hủy phiên cũ TRƯỚC
session_destroy();
// Rồi mở phiên MỚI để đặt thông báo flash
session_start();
$_SESSION['success'] = "Đã đăng xuất thành công";
header("Location: bai05_dangnhap.php");
exit();
?>
Giải thích cái bẫy. Nếu bạn viết theo thứ tự tự nhiên:
$_SESSION['success'] = "Đã đăng xuất thành công";
session_destroy(); // xóa luôn thông báo vừa ghi
thì thông báo bị hủy cùng phiên, và trang đăng nhập không hiện gì cả.
Giải pháp là hủy phiên cũ trước, mở phiên mới, rồi mới ghi thông báo vào phiên mới đó. Phiên mới có mã phiên khác hẳn, không còn dấu vết user_name nào.
Một cách khác, gọn hơn, là không hủy phiên mà chỉ xóa những khóa liên quan tới đăng nhập:
<?php
session_start();
unset($_SESSION['user_name']);
$_SESSION['success'] = "Đã đăng xuất thành công";
header("Location: bai05_dangnhap.php");
exit();
?>
Cách này giữ nguyên phiên nên thông báo sống sót tự nhiên. Nhược điểm là bạn phải nhớ liệt kê đủ mọi khóa cần xóa; sót một khóa là còn sót dữ liệu của người dùng cũ.
EzCode chọn cách nào? Nhìn lại UserController::logout() ở mục 3.2: nó gọi session_destroy() và không đặt thông báo nào cả. Người dùng bị đưa về trang chủ trong im lặng. Đây là lựa chọn hợp lệ, tránh được hoàn toàn cái bẫy trên. Nếu bạn muốn thêm thông báo "Đã đăng xuất" cho EzCode, hãy dùng đúng kỹ thuật hủy rồi mở lại ở trên.
➡️ Bài tiếp theo: Bài 6 — Lập trình hướng đối tượng
⬅️ Về mục lục · Bài trước
All rights reserved