MVC trong Yii2: Models, Views, Controllers & Routing

Phase 1 cho bạn một app Yii2 đang chạy. Phase 2 dạy bạn cách thực sự xây dựng với nó.
Yii2 là một MVC framework — mỗi request đều đi qua một Controller, mỗi trang được render đều đi qua một View, và mỗi dữ liệu đều sống trong một Model. Hiểu cách ba tầng này giao tiếp với nhau, và cách routing quyết định controller nào xử lý URL nào, là nền tảng của mọi thứ khác trong Yii2.
Post này thiên về thực hành. Cuối cùng bạn sẽ xây dựng xong một task manager: liệt kê task, xem một task, tạo, chỉnh sửa, và xóa — toàn bộ CRUD cycle, routing và tất cả.
Bạn Sẽ Xây Dựng Gì
Một simple task manager với:
GET /task → liệt kê tất cả task
GET /task/view?id=1 → xem một task
GET /task/create → hiển thị form tạo
POST /task/create → xử lý form submit
GET /task/update?id=1 → hiển thị form chỉnh sửa
POST /task/update?id=1 → xử lý form chỉnh sửa
POST /task/delete?id=1 → xóa một taskChưa có database (đó là Phase 3) — chúng ta sẽ dùng in-memory array để tập trung vào MVC mechanics.
Request Lifecycle trong Yii2
Mỗi request trong Yii2 đều đi theo cùng một con đường:
Điểm quan trọng:
- Entry script (
web/index.php) tạoApplicationobject và chạy nó - URL Manager parse URL thành
controller/action+ parameter - Yii2 khởi tạo Controller và gọi method
action*()tương ứng - Action fetch data (từ Model), rồi render View
- View trả về HTML, Controller gửi nó như response
Controllers
Controller trong Yii2 là một class extend yii\web\Controller. Mỗi public method tên actionSomething() tự động map với URL segment something.
Controller Đầu Tiên Của Bạn
Tạo controllers/TaskController.php:
<?php
namespace app\controllers;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
class TaskController extends Controller
{
// "Database" in-memory — thay bằng Active Record trong Phase 3
private static array $tasks = [
1 => ['id' => 1, 'title' => 'Mua đồ ăn', 'done' => false],
2 => ['id' => 2, 'title' => 'Viết bài Phase 2', 'done' => true],
3 => ['id' => 3, 'title' => 'Review pull request', 'done' => false],
];
private static int $nextId = 4;
// GET /task (Yii2 map "index" thành action mặc định)
public function actionIndex(): string
{
return $this->render('index', [
'tasks' => self::$tasks,
]);
}
// GET /task/view?id=1
public function actionView(int $id): string
{
$task = self::$tasks[$id] ?? null;
if ($task === null) {
throw new NotFoundHttpException("Không tìm thấy task #$id.");
}
return $this->render('view', ['task' => $task]);
}
// GET /task/create → hiển thị form
// POST /task/create → xử lý form
public function actionCreate(): string|\yii\web\Response
{
if (\Yii::$app->request->isPost) {
$title = \Yii::$app->request->post('title', '');
if (trim($title) !== '') {
$id = self::$nextId++;
self::$tasks[$id] = ['id' => $id, 'title' => $title, 'done' => false];
return $this->redirect(['task/view', 'id' => $id]);
}
}
return $this->render('create');
}
// GET/POST /task/update?id=1
public function actionUpdate(int $id): string|\yii\web\Response
{
$task = self::$tasks[$id] ?? null;
if ($task === null) {
throw new NotFoundHttpException("Không tìm thấy task #$id.");
}
if (\Yii::$app->request->isPost) {
$task['title'] = \Yii::$app->request->post('title', $task['title']);
$task['done'] = (bool)\Yii::$app->request->post('done', false);
self::$tasks[$id] = $task;
return $this->redirect(['task/index']);
}
return $this->render('update', ['task' => $task]);
}
// POST /task/delete?id=1
public function actionDelete(int $id): \yii\web\Response
{
unset(self::$tasks[$id]);
return $this->redirect(['task/index']);
}
}$this->render() Làm Gì
$this->render('index', $data) thực hiện ba việc:
- Tìm file view tại
views/task/index.php(tên controller xác định thư mục) - Extract
$datathành local variable (nên['tasks' => $tasks]thành$tasksbên trong view) - Wrap output của view trong layout (mặc định là
views/layouts/main.php)
Để render không có layout (cho AJAX response):
return $this->renderPartial('_form', ['task' => $task]);
// hoặc
return $this->renderAjax('_form', ['task' => $task]);Property $defaultAction
Mặc định, actionIndex() chạy khi không có action nào được chỉ định trong URL. Override nó:
class TaskController extends Controller
{
public $defaultAction = 'list'; // giờ GET /task → actionList()
}Filter và behaviors()
Controller khai báo filter (access control, rate limiting, logging) qua behaviors():
public function behaviors(): array
{
return [
'access' => [
'class' => \yii\filters\AccessControl::class,
'rules' => [
['allow' => true, 'roles' => ['@']], // chỉ user đã đăng nhập
['allow' => true, 'actions' => ['index', 'view'], 'roles' => ['?']], // guest được đọc
],
],
'verbs' => [
'class' => \yii\filters\VerbFilter::class,
'actions' => [
'delete' => ['POST'], // chỉ cho phép POST với delete
],
],
];
}Views
View trong Yii2 là file PHP thuần output HTML. Chúng nằm trong views/<tên-controller>/.
Tạo Các View
Tạo thư mục views/task/ và các file sau:
views/task/index.php — danh sách task:
<?php
/** @var yii\web\View $this */
/** @var array[] $tasks */
use yii\helpers\Html;
$this->title = 'Danh sách Task';
?>
<div class="task-index">
<h1><?= Html::encode($this->title) ?></h1>
<p><?= Html::a('Tạo Task', ['task/create'], ['class' => 'btn btn-success']) ?></p>
<table class="table table-bordered">
<thead>
<tr>
<th>#</th>
<th>Tiêu đề</th>
<th>Trạng thái</th>
<th>Thao tác</th>
</tr>
</thead>
<tbody>
<?php foreach ($tasks as $task): ?>
<tr>
<td><?= $task['id'] ?></td>
<td><?= Html::encode($task['title']) ?></td>
<td><?= $task['done'] ? '✅ Xong' : '⏳ Chờ' ?></td>
<td>
<?= Html::a('Xem', ['task/view', 'id' => $task['id']]) ?>
<?= Html::a('Sửa', ['task/update', 'id' => $task['id']]) ?>
<?= Html::a('Xóa', ['task/delete', 'id' => $task['id']], [
'data-method' => 'post',
'data-confirm' => 'Bạn chắc chắn muốn xóa?',
]) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>views/task/view.php — một task:
<?php
/** @var yii\web\View $this */
/** @var array $task */
use yii\helpers\Html;
$this->title = $task['title'];
?>
<div class="task-view">
<h1><?= Html::encode($this->title) ?></h1>
<p><strong>Trạng thái:</strong> <?= $task['done'] ? 'Đã xong' : 'Chờ xử lý' ?></p>
<p>
<?= Html::a('Sửa', ['task/update', 'id' => $task['id']], ['class' => 'btn btn-primary']) ?>
<?= Html::a('Quay lại danh sách', ['task/index'], ['class' => 'btn btn-default']) ?>
</p>
</div>views/task/create.php — form tạo:
<?php
/** @var yii\web\View $this */
use yii\helpers\Html;
$this->title = 'Tạo Task';
?>
<div class="task-create">
<h1><?= Html::encode($this->title) ?></h1>
<form method="post" action="">
<?= Html::hiddenInput(\Yii::$app->request->csrfParam, \Yii::$app->request->csrfToken) ?>
<div class="form-group">
<label for="title">Tiêu đề task</label>
<input type="text" id="title" name="title" class="form-control" required>
</div>
<button type="submit" class="btn btn-success">Tạo</button>
<?= Html::a('Hủy', ['task/index']) ?>
</form>
</div>views/task/update.php — form sửa:
<?php
/** @var yii\web\View $this */
/** @var array $task */
use yii\helpers\Html;
$this->title = 'Cập nhật: ' . $task['title'];
?>
<div class="task-update">
<h1><?= Html::encode($this->title) ?></h1>
<form method="post" action="">
<?= Html::hiddenInput(\Yii::$app->request->csrfParam, \Yii::$app->request->csrfToken) ?>
<div class="form-group">
<label for="title">Tiêu đề task</label>
<input type="text" id="title" name="title" class="form-control"
value="<?= Html::encode($task['title']) ?>" required>
</div>
<div class="form-check">
<input type="checkbox" id="done" name="done" value="1" class="form-check-input"
<?= $task['done'] ? 'checked' : '' ?>>
<label class="form-check-label" for="done">Đánh dấu đã xong</label>
</div>
<br>
<button type="submit" class="btn btn-primary">Lưu</button>
<?= Html::a('Hủy', ['task/index']) ?>
</form>
</div>Tại Sao Html::encode() Bắt Buộc?
Không bao giờ output dữ liệu do user cung cấp trực tiếp. Html::encode() chuyển đổi <, >, ", ', & thành HTML entity tương ứng, ngăn chặn XSS. Đây là thói quen quan trọng nhất khi làm việc với bất kỳ templating system nào.
// KHÔNG AN TOÀN — lỗ hổng XSS:
echo $task['title'];
// AN TOÀN:
echo Html::encode($task['title']);Layout
View render bên trong một layout. Layout mặc định là views/layouts/main.php. Nó wrap mỗi trang với header, navigation, và footer chung. Nội dung view thay thế $content:
<?php
/** @var yii\web\View $this */
/** @var string $content */
use app\assets\AppAsset;
AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<nav class="navbar navbar-default navbar-fixed-top"><!-- nav ở đây --></nav>
<div class="container"><?= $content ?></div>
<footer><!-- footer ở đây --></footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>Để dùng layout khác cho một controller:
class TaskController extends Controller
{
public $layout = 'minimal'; // dùng views/layouts/minimal.php
}Hoặc tắt layout cho một action:
public function actionApi(): string
{
$this->layout = false;
return $this->render('api-response');
}Partial View và $this->render() Trong View
Snippet có thể tái sử dụng nằm trong file có prefix _ (convention, không bắt buộc):
// views/task/_form.php — shared form partial
<?php
/** @var array $task */
/** @var string $action */
use yii\helpers\Html;
?>
<form method="post" action="<?= $action ?>">
<?= Html::hiddenInput(\Yii::$app->request->csrfParam, \Yii::$app->request->csrfToken) ?>
<input type="text" name="title" value="<?= Html::encode($task['title'] ?? '') ?>">
<button type="submit">Lưu</button>
</form>Render partial từ view khác:
// Trong create.php hoặc update.php:
<?= $this->render('_form', ['task' => $task ?? [], 'action' => '']) ?>Models
Model trong Yii2 đại diện cho data và business rule. Để validate form data (không có database), extend yii\base\Model. Để data được lưu trong database, extend yii\db\ActiveRecord (sẽ cover trong Phase 3).
Tạo Model Cho Validation
Hãy thay thế các lệnh request->post() thuần trong controller bằng Model đúng nghĩa:
Tạo models/TaskForm.php:
<?php
namespace app\models;
use yii\base\Model;
class TaskForm extends Model
{
public string $title = '';
public bool $done = false;
public function rules(): array
{
return [
// title bắt buộc, string, tối đa 200 ký tự
[['title'], 'required'],
[['title'], 'string', 'max' => 200],
[['title'], 'trim'], // xóa khoảng trắng đầu/cuối
// done là boolean
[['done'], 'boolean'],
];
}
public function attributeLabels(): array
{
return [
'title' => 'Tiêu đề task',
'done' => 'Đã hoàn thành',
];
}
}Sử Dụng Model Trong Controller
Cập nhật actionCreate() để dùng model:
public function actionCreate(): string|\yii\web\Response
{
$model = new \app\models\TaskForm();
if ($model->load(\Yii::$app->request->post()) && $model->validate()) {
// model hợp lệ — lưu vào
$id = self::$nextId++;
self::$tasks[$id] = [
'id' => $id,
'title' => $model->title,
'done' => $model->done,
];
return $this->redirect(['task/view', 'id' => $id]);
}
// GET request hoặc validation thất bại — hiển thị form
return $this->render('create', ['model' => $model]);
}$model->load($post) đọc POST data và map vào attribute của model dựa trên tên class (nó tìm $_POST['TaskForm']['title']). $model->validate() chạy rules() và điền $model->errors nếu validation thất bại.
Hiển Thị Lỗi Validation Trong View
Cập nhật views/task/create.php để dùng model:
<?php
/** @var yii\web\View $this */
/** @var app\models\TaskForm $model */
use yii\helpers\Html;
use yii\widgets\ActiveForm;
$this->title = 'Tạo Task';
?>
<div class="task-create">
<h1><?= Html::encode($this->title) ?></h1>
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'title')->textInput(['placeholder' => 'Cần làm gì?']) ?>
<?= $form->field($model, 'done')->checkbox() ?>
<div class="form-group">
<?= Html::submitButton('Tạo', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>ActiveForm là Yii2 widget làm:
- Tạo tag
<form>đúng với CSRF token - Tự động render label, input, và message lỗi validation
- Kết nối với client-side validation (hiển thị lỗi khi blur, không cần reload trang)
URL Routing
URL routing của Yii2 được xử lý bởi component URL Manager. Nó map URL thành route controller/action và generate URL trong code của bạn.
Format URL Mặc Định
Khi không có cấu hình gì, Yii2 dùng query-string URL:
/index.php?r=task/view&id=1Tham số r là route. task → TaskController, view → actionView.
Pretty URL
Bật clean URL bằng cách cấu hình urlManager trong config/web.php:
'components' => [
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false, // ẩn index.php khỏi URL
'rules' => [], // rule tùy chỉnh ở đây
],
],Bạn cũng cần .htaccess (Apache) hoặc nginx rewrite rule để route tất cả request qua index.php:
# web/.htaccess
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.phpKhi bật pretty URL, format route mặc định trở thành:
/task → TaskController::actionIndex()
/task/view/1 → TaskController::actionView(1) ← id như path segment
/task/create → TaskController::actionCreate()Khoan — làm sao /task/view/1 map số 1 vào $id? Không có rule tùy chỉnh, extra path segment được xử lý như GET parameter. Bạn vẫn dùng /task/view?id=1. Để có /task/view/1, thêm URL rule.
URL Rule
URL rule định nghĩa pattern tùy chỉnh. Thêm vào mảng rules trong urlManager:
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// Pattern → Route
'tasks' => 'task/index',
'tasks/create' => 'task/create',
'tasks/<id:\d+>' => 'task/view',
'tasks/<id:\d+>/edit' => 'task/update',
'tasks/<id:\d+>/delete' => 'task/delete',
],
],Giờ thì:
GET /tasks → TaskController::actionIndex()
GET /tasks/create → TaskController::actionCreate()
GET /tasks/42 → TaskController::actionView($id=42)
GET /tasks/42/edit → TaskController::actionUpdate($id=42)
POST /tasks/42/delete → TaskController::actionDelete($id=42)Cú pháp <id:\d+> là named pattern: id là tên parameter, \d+ là regex. Yii2 tự động truyền giá trị matched vào action method.
Generate URL Trong Code
Không bao giờ hardcode URL. Dùng URL helper của Yii2:
use yii\helpers\Url;
// Trong controller hoặc model:
$url = Url::to(['task/view', 'id' => 42]); // → /tasks/42
$url = Url::to(['task/index']); // → /tasks
$url = Url::toRoute(['task/create']); // tương tự, API khác
// URL tuyệt đối (bao gồm http://hostname):
$url = Url::to(['task/view', 'id' => 42], true);
// Trong view:
$this->redirect(['task/index']); // redirect bằng route array
// Html::a() chấp nhận route array trực tiếp:
Html::a('Xem', ['task/view', 'id' => $task->id])Khi bạn thay đổi URL rule, tất cả lệnh Url::to() tự động tạo ra URL mới. Không cần tìm và thay thế qua các view.
URL Rule Cho REST-style Route
'rules' => [
// GET /api/tasks → api/task/index
// POST /api/tasks → api/task/create
// GET /api/tasks/42 → api/task/view
// PUT /api/tasks/42 → api/task/update
// DELETE /api/tasks/42 → api/task/delete
[
'class' => 'yii\rest\UrlRule',
'controller' => 'api/task',
'pluralize' => false,
],
],Gắn Kết Lại: Flow Đầy Đủ
Hãy trace một request tạo task hoàn chỉnh:
Pattern Controller Phổ Biến
Flash Message
Flash message tồn tại qua một lần redirect — hoàn hảo cho feedback thành công/lỗi:
// Trong controller sau khi lưu thành công:
\Yii::$app->session->setFlash('success', 'Task đã được tạo thành công!');
return $this->redirect(['task/index']);
// Trong view (hoặc layout):
foreach (\Yii::$app->session->getAllFlashes() as $type => $message) {
echo "<div class='alert alert-{$type}'>" . Html::encode($message) . "</div>";
}Giới Hạn HTTP Method
// Trong behaviors():
'verbs' => [
'class' => \yii\filters\VerbFilter::class,
'actions' => [
'create' => ['GET', 'POST'],
'update' => ['GET', 'POST'],
'delete' => ['POST'],
],
],JSON Response
public function actionApiList(): \yii\web\Response
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return $this->asJson(array_values(self::$tasks));
}Cấu Trúc Thư Mục Sau Phase 2
your-app/
├── controllers/
│ ├── SiteController.php (mặc định, từ template)
│ └── TaskController.php ← mới
├── models/
│ └── TaskForm.php ← mới
├── views/
│ ├── layouts/
│ │ └── main.php (không đổi)
│ ├── site/ (view mặc định từ template)
│ └── task/ ← mới
│ ├── index.php
│ ├── view.php
│ ├── create.php
│ └── update.php
└── config/
└── web.php (cập nhật urlManager)Những Gì Bạn Đã Học
✅ Request lifecycle của Yii2 chảy từ URL đến response như thế nào
✅ Cách viết controller và action method
✅ Cách view tìm template file và merge vào layout
✅ Tại sao Html::encode() không thể bỏ qua vì lý do security
✅ Cách dùng ActiveForm với Model để xử lý form với validation
✅ Cách urlManager biến query thành pretty URL
✅ Cách generate URL bằng Url::to() — không bao giờ hardcode
✅ Cách dùng flash message, verb filter, và JSON response
Tiếp Theo Là Gì
Task manager hoạt động được, nhưng task biến mất sau mỗi request vì chúng sống trong static array. Phase 3 kết nối cái này với database thật dùng Active Record ORM và Query Builder của Yii2 — model Task trở thành class đầy đủ được database hỗ trợ với Task::find(), $task->save(), và relational query.
Điều hướng series:
← Post 2: Getting Started with Yii2
→ Post 4: Database & Active Record
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.