Bài 26 xây dựng Model làm việc với database. Bài này đi sâu vào View — lớp hiển thị giao diện, nhận dữ liệu từ Controller và render HTML. View tốt phải tách biệt logic hiển thị khỏi business logic, dễ bảo trì, tái sử dụng. Bạn sẽ học cách tổ chức views với layouts, partials và helper functions.
1. Vai trò của View
| View LÀM | View KHÔNG LÀM |
|---|---|
| ✅ Hiển thị HTML với dữ liệu từ Controller | ❌ Truy vấn database |
| ✅ Loop, if/else đơn giản | ❌ Xử lý business logic |
| ✅ Format dữ liệu hiển thị | ❌ Validate input |
| ✅ Gọi helper functions | ❌ Tính toán nghiệp vụ |
| ✅ Include partials | ❌ Redirect, flash message |
💡 Logic-less Templates: View chỉ chứa logic hiển thị tối thiểu. Tính toán phức tạp phải xử lý ở Controller/Model rồi truyền kết quả cho View.
2. Cấu trúc Views
app/Views/
├── layouts/
│ ├── main.php
│ └── admin.php
├── partials/
│ ├── alerts.php
│ └── pagination.php
├── sinhvien/
│ ├── index.php
│ ├── form.php
│ └── detail.php
└── errors/
├── 404.php
└── 500.php3. Render View
<?php
// File: app/Helpers/functions.php
function view(string $view, array $data = [], ?string $layout = 'layouts.main'): string {
extract($data);
$viewPath = APP_PATH . '/Views/' . str_replace('.', '/', $view) . '.php';
if (!file_exists($viewPath)) {
throw new RuntimeException("View không tồn tại: {$view}");
}
ob_start();
require $viewPath;
$content = ob_get_clean();
if ($layout) {
$layoutPath = APP_PATH . '/Views/' . str_replace('.', '/', $layout) . '.php';
if (file_exists($layoutPath)) {
ob_start();
require $layoutPath;
return ob_get_clean();
}
}
return $content;
}
function partial(string $view, array $data = []): string {
return view($view, $data, null);
}
?>4. Layout chính
<?php
// File: app/Views/layouts/main.php
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $title ?? 'Quản Lý Sinh Viên'; ?></title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: #2c3e50;
color: white;
padding: 1rem 0;
margin-bottom: 2rem;
}
header h1 { font-size: 1.5rem; }
nav { margin-top: 1rem; }
nav a {
color: white;
text-decoration: none;
margin-right: 1.5rem;
padding: 0.5rem 1rem;
border-radius: 4px;
}
nav a:hover { background: rgba(255,255,255,0.1); }
.alert {
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
footer {
margin-top: 3rem;
padding: 2rem 0;
text-align: center;
color: #666;
border-top: 1px solid #ddd;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Hệ Thống Quản Lý Sinh Viên</h1>
<nav>
<a href="/">Trang chủ</a>
<a href="/sinhvien">Sinh viên</a>
<a href="/monhoc">Môn học</a>
<a href="/diem">Điểm</a>
</nav>
</div>
</header>
<main class="container">
<?php echo partial('partials.alerts', ['errors' => $errors ?? []]); ?>
<?php echo $content; ?>
</main>
<footer>
<div class="container">
<p>© <?php echo date('Y'); ?> Hệ Thống Quản Lý Sinh Viên</p>
</div>
</footer>
</body>
</html>5. Partials
<?php
// File: app/Views/partials/alerts.php
?>
<?php if (flash('success')): ?>
<div class="alert alert-success">
<?php echo e(flash('success')); ?>
</div>
<?php endif; ?>
<?php if (flash('error')): ?>
<div class="alert alert-error">
<?php echo e(flash('error')); ?>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="alert alert-error">
<ul style="margin-left: 1.5rem;">
<?php foreach ($errors as $error): ?>
<li><?php echo e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?><?php
// File: app/Views/partials/pagination.php
?>
<?php if ($pagination['last_page'] > 1): ?>
<nav style="margin-top: 2rem; text-align: center;">
<div style="display: inline-flex; gap: 0.5rem;">
<?php if ($pagination['current_page'] > 1): ?>
<a href="?page=<?php echo $pagination['current_page'] - 1; ?>"
style="padding: 0.5rem 1rem; background: #3498db; color: white; text-decoration: none; border-radius: 4px;">
« Trước
</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $pagination['last_page']; $i++): ?>
<?php if ($i == $pagination['current_page']): ?>
<strong style="padding: 0.5rem 1rem; background: #2c3e50; color: white; border-radius: 4px;">
<?php echo $i; ?>
</strong>
<?php else: ?>
<a href="?page=<?php echo $i; ?>"
style="padding: 0.5rem 1rem; background: #ecf0f1; color: #333; text-decoration: none; border-radius: 4px;">
<?php echo $i; ?>
</a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($pagination['current_page'] < $pagination['last_page']): ?>
<a href="?page=<?php echo $pagination['current_page'] + 1; ?>"
style="padding: 0.5rem 1rem; background: #3498db; color: white; text-decoration: none; border-radius: 4px;">
Sau »
</a>
<?php endif; ?>
</div>
<p style="margin-top: 1rem; color: #666;">
<?php echo $pagination['from']; ?> - <?php echo $pagination['to']; ?> / <?php echo $pagination['total']; ?> bản ghi
</p>
</nav>
<?php endif; ?>6. View danh sách
<?php
// File: app/Views/sinhvien/index.php
?>
<style>
.search-form { margin-bottom: 1.5rem; display: flex; gap: 0.5rem; }
.search-form input { flex: 1; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; }
.btn { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.btn-primary { background: #3498db; color: white; }
.btn-success { background: #2ecc71; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn-secondary { background: #95a5a6; color: white; }
.btn-sm { padding: 0.5rem 1rem; font-size: 0.9rem; }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
</style>
<div class="card">
<div style="display: flex; justify-content: space-between; margin-bottom: 1.5rem;">
<h2>Danh Sách Sinh Viên</h2>
<a href="/sinhvien/create" class="btn btn-success">+ Thêm</a>
</div>
<form method="GET" class="search-form">
<input type="text" name="q" value="<?php echo e($keyword ?? ''); ?>"
placeholder="Tìm kiếm...">
<button type="submit" class="btn btn-primary">Tìm</button>
<?php if (!empty($keyword)): ?>
<a href="/sinhvien" class="btn btn-secondary">Xóa</a>
<?php endif; ?>
</form>
<?php if (empty($students)): ?>
<p style="text-align: center; padding: 2rem; color: #666;">Không có dữ liệu.</p>
<?php else: ?>
<table>
<thead>
<tr>
<th>MSSV</th><th>Họ Tên</th><th>Email</th><th>Lớp</th><th>Điểm</th><th>Thao tác</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $sv): ?>
<tr>
<td><?php echo e($sv['mssv']); ?></td>
<td><?php echo e($sv['ho_ten']); ?></td>
<td><?php echo e($sv['email']); ?></td>
<td><?php echo e($sv['lop']); ?></td>
<td><?php echo $sv['diem_tb'] ?? '-'; ?></td>
<td>
<a href="/sinhvien/<?php echo $sv['id']; ?>" class="btn btn-primary btn-sm">Xem</a>
<a href="/sinhvien/<?php echo $sv['id']; ?>/edit" class="btn btn-secondary btn-sm">Sửa</a>
<form method="POST" action="/sinhvien/<?php echo $sv['id']; ?>"
style="display: inline;" onsubmit="return confirm('Xóa?')">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-danger btn-sm">Xóa</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (isset($pagination)): ?>
<?php echo partial('partials.pagination', ['pagination' => $pagination]); ?>
<?php endif; ?>
<?php endif; ?>
</div>7. View form
<?php
// File: app/Views/sinhvien/form.php
?>
<div class="card">
<h2><?php echo $title ?? 'Form'; ?></h2>
<form method="POST" action="<?php echo $action ?? ''; ?>" style="margin-top: 1.5rem;">
<?php if (isset($method) && $method !== 'POST'): ?>
<input type="hidden" name="_method" value="<?php echo $method; ?>">
<?php endif; ?>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">MSSV <span style="color: red;">*</span></label>
<input type="text" name="mssv" value="<?php echo e(old('mssv', $student['mssv'] ?? '')); ?>"
style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px;" required>
<?php if (isset($errors['mssv'])): ?>
<small style="color: red;"><?php echo e($errors['mssv']); ?></small>
<?php endif; ?>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Họ Tên <span style="color: red;">*</span></label>
<input type="text" name="ho_ten" value="<?php echo e(old('ho_ten', $student['ho_ten'] ?? '')); ?>"
style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px;" required>
<?php if (isset($errors['ho_ten'])): ?>
<small style="color: red;"><?php echo e($errors['ho_ten']); ?></small>
<?php endif; ?>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Email <span style="color: red;">*</span></label>
<input type="email" name="email" value="<?php echo e(old('email', $student['email'] ?? '')); ?>"
style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px;" required>
<?php if (isset($errors['email'])): ?>
<small style="color: red;"><?php echo e($errors['email']); ?></small>
<?php endif; ?>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Lớp <span style="color: red;">*</span></label>
<input type="text" name="lop" value="<?php echo e(old('lop', $student['lop'] ?? '')); ?>"
style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px;" required>
<?php if (isset($errors['lop'])): ?>
<small style="color: red;"><?php echo e($errors['lop']); ?></small>
<?php endif; ?>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Điểm TB</label>
<input type="number" name="diem_tb" step="0.01" min="0" max="10"
value="<?php echo old('diem_tb', $student['diem_tb'] ?? ''); ?>"
style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Ghi Chú</label>
<textarea name="ghi_chu" rows="3"
style="width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px;"><?php echo e(old('ghi_chu', $student['ghi_chu'] ?? '')); ?></textarea>
</div>
<div style="display: flex; gap: 1rem; margin-top: 1.5rem;">
<button type="submit" class="btn btn-success">
<?php echo isset($student) ? 'Cập Nhật' : 'Thêm'; ?>
</button>
<a href="/sinhvien" class="btn btn-secondary">Hủy</a>
</div>
</form>
</div>8. Helpers
<?php
// File: app/Helpers/functions.php (bổ sung)
function formatDate(?string $date, string $format = 'd/m/Y'): string {
if (!$date) return '-';
return date($format, strtotime($date));
}
function formatNumber($number, int $decimals = 0): string {
if ($number === null || $number === '') return '-';
return number_format($number, $decimals, '.', ',');
}
function formatCurrency($amount): string {
if ($amount === null || $amount === '') return '-';
return number_format($amount, 0, '.', ',') . ' đ';
}
function str_limit(string $str, int $limit = 100): string {
if (mb_strlen($str, 'UTF-8') <= $limit) return $str;
return mb_substr($str, 0, $limit, 'UTF-8') . '...';
}
function asset(string $path): string {
return rtrim(config('app.url'), '/') . '/assets/' . ltrim($path, '/');
}
?>9. Controller sử dụng
<?php
// File: app/Controllers/SinhVienController.php
namespace App\Controllers;
use App\Models\SinhVienModel;
class SinhVienController extends BaseController {
protected SinhVienModel $model;
public function __construct() {
parent::__construct();
$this->model = new SinhVienModel();
}
public function index(array $params): string {
$page = (int)($_GET['page'] ?? 1);
$keyword = trim($_GET['q'] ?? '');
if ($keyword) {
$result = $this->model->search($keyword, $page);
} else {
$result = $this->model->paginate($page);
}
return view('sinhvien.index', [
'title' => 'Danh Sách',
'students' => $result['items'],
'pagination' => $result,
'keyword' => $keyword,
]);
}
public function create(array $params): string {
return view('sinhvien.form', [
'title' => 'Thêm Mới',
'action' => '/sinhvien',
'method' => 'POST',
]);
}
}
?>Tóm tắt
- ✅ View nhận dữ liệu, render HTML
- ✅ Layout tự động wrap content
- ✅ Partials tái sử dụng
- ✅ Helpers format dữ liệu
- ✅ PHP thuần, đơn giản
- ✅ extract() biến data thành biến
- ✅ ob_start/ob_get_clean
- ✅ Logic ở Controller/Model
🎯 Bài tập
- Admin Layout: Tạo
layouts/admin.phpvới sidebar, views admin. - Components: Tạo
partials/table.phprender bảng từ array. - Multi-layout: Hỗ trợ nhiều layout:
view('x', $data, 'layouts.admin').