Yii2 Forms, Validation & Data Input

Phase 4 đã khóa chặt task manager với authentication và RBAC. Nhưng mọi tương tác với user đều bắt đầu từ form — registration, login, tạo task, cập nhật profile. Form làm sai thì bạn nhận được dữ liệu rác, lỗ hổng security, và user bực bội.
Hệ thống form của Yii2 là một trong những feature mạnh nhất. ActiveForm generate HTML, kết nối client-side validation, xử lý AJAX validation, hiển thị error — tất cả từ cùng rules() mà bạn đã viết trên model. Một nguồn duy nhất cho validation logic, không duplicate gì giữa server và client.
Kết thúc bài này, bạn sẽ build được form validate trên cả client lẫn server, xử lý file uploads, lưu nhiều model cùng lúc với tabular input, và điều chỉnh validation rules theo context bằng scenarios.
Bạn Sẽ Build Gì
✅ ActiveForm widget với automatic client-side validation
✅ Tất cả built-in validators chính (required, string, email, unique, compare, v.v.)
✅ Custom inline validators và standalone Validator classes
✅ Model scenarios cho context-dependent validation
✅ File và image upload với validation
✅ Tabular input — lưu nhiều models trong một form
✅ AJAX validation cho real-time server-side checks
1. ActiveForm Cơ Bản
ActiveForm Hoạt Động Như Thế Nào
ActiveForm là widget generate <form> tags, input fields, labels, error messages, và client-side validation — tất cả driven bởi rules() của model. Khi bạn gọi $form->field($model, 'attribute'), Yii2:
- Đọc
rules()của model để xác định validators nào áp dụng - Generate HTML input với đúng
name,id, vàvalue - Thêm
ariaattributes cho accessibility - Gắn JavaScript validation handlers
- Hiển thị label của attribute từ
attributeLabels() - Dành chỗ cho error messages
Cấu Trúc Form Cơ Bản
<?php
// views/task/create.php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
$this->title = 'Create Task';
?>
<h1><?= Html::encode($this->title) ?></h1>
<?php $form = ActiveForm::begin([
'id' => 'task-form',
'options' => ['class' => 'form-horizontal'],
]) ?>
<?= $form->field($model, 'title') ?>
<?= $form->field($model, 'description')->textarea(['rows' => 4]) ?>
<?= $form->field($model, 'status')->dropDownList([
1 => 'To Do',
2 => 'In Progress',
3 => 'Done',
], ['prompt' => '-- Select Status --']) ?>
<?= $form->field($model, 'due_date')->input('date') ?>
<div class="form-group">
<?= Html::submitButton('Create', ['class' => 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end() ?>HTML Được Generate Ra
Code đơn giản đó generate ra:
<form id="task-form" class="form-horizontal" action="/task/create" method="post">
<input type="hidden" name="_csrf" value="abc123...">
<div class="form-group field-task-title required">
<label class="control-label" for="task-title">Title</label>
<input type="text" id="task-title" class="form-control"
name="Task[title]" aria-required="true">
<div class="help-block"></div>
</div>
<!-- ... more fields ... -->
</form>Những điểm cần chú ý:
- CSRF token tự động được thêm vào
name="Task[title]"— tên model class trở thành array key (đây là lý do$model->load(Yii::$app->request->post())hoạt động)requiredCSS class tự động thêm từrules()aria-requiredcho accessibilityhelp-blockdiv dành cho error messages
Phía Controller
// controllers/TaskController.php
public function actionCreate()
{
$model = new Task();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
Yii::$app->session->setFlash('success', 'Task created.');
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', ['model' => $model]);
}Pattern luôn giống nhau:
- Tạo model
- Thử
load()POST data vàsave()(tự gọivalidate()bên trong) - Nếu validation fail, re-render form — errors đã gắn sẵn trên model
2. Built-in Validators
Yii2 đi kèm ~25 built-in validators. Đây là những cái bạn dùng nhiều nhất, tổ chức theo category.
Required & Type Validators
public function rules()
{
return [
// Required — field không được trống
[['title', 'email'], 'required'],
[['title'], 'required', 'message' => 'Please enter a title.'],
// String — ràng buộc độ dài
[['title'], 'string', 'max' => 255],
[['description'], 'string', 'max' => 5000],
[['username'], 'string', 'min' => 3, 'max' => 32],
[['code'], 'string', 'length' => 6], // độ dài chính xác
// Integer
[['status', 'priority'], 'integer'],
[['age'], 'integer', 'min' => 0, 'max' => 150],
// Number (float/decimal)
[['price'], 'number', 'min' => 0],
[['latitude'], 'number', 'min' => -90, 'max' => 90],
// Boolean
[['is_active', 'agree_terms'], 'boolean'],
];
}Format Validators
public function rules()
{
return [
// Email
[['email'], 'email'],
[['email'], 'email', 'checkDNS' => true], // verify domain có MX record
// URL
[['website'], 'url', 'defaultScheme' => 'https'],
// IP address
[['server_ip'], 'ip'],
[['server_ip'], 'ip', 'ipv4' => true, 'ipv6' => false],
// Date
[['birth_date'], 'date', 'format' => 'php:Y-m-d'],
[['start_time'], 'datetime', 'format' => 'php:Y-m-d H:i:s'],
// Regular expression
[['phone'], 'match', 'pattern' => '/^\+?[0-9]{10,15}$/'],
[['slug'], 'match', 'pattern' => '/^[a-z0-9-]+$/'],
];
}Comparison & Uniqueness Validators
public function rules()
{
return [
// So sánh hai attributes
[['password_repeat'], 'compare', 'compareAttribute' => 'password'],
[['end_date'], 'compare', 'compareAttribute' => 'start_date',
'operator' => '>=', 'type' => 'datetime'],
// Unique trong database
[['email'], 'unique'],
[['email'], 'unique', 'targetClass' => User::class,
'message' => 'This email is already taken.'],
// Composite unique (tổ hợp phải unique)
[['user_id'], 'unique',
'targetAttribute' => ['user_id', 'project_id'],
'message' => 'User already assigned to this project.'],
// Exist — foreign key phải tồn tại trong bảng khác
[['category_id'], 'exist',
'targetClass' => Category::class,
'targetAttribute' => 'id'],
];
}Filter Validators
Những cái này thay đổi giá trị thay vì reject:
public function rules()
{
return [
// Trim whitespace
[['title', 'email'], 'trim'],
// Set default value
[['status'], 'default', 'value' => self::STATUS_DRAFT],
[['created_at'], 'default',
'value' => function () { return date('Y-m-d H:i:s'); }],
// Custom filter
[['email'], 'filter', 'filter' => 'strtolower'],
[['tags'], 'filter', 'filter' => function ($value) {
return array_map('trim', explode(',', $value));
}],
];
}Validator safe
safe không validate — nó đánh dấu attributes là safe cho mass assignment:
public function rules()
{
return [
// Những attributes này có thể set qua load()
[['notes', 'metadata'], 'safe'],
];
}Warning: Chỉ dùng
safecho attributes thật sự không cần validation. Nếu attribute có bất kỳ constraint nào, dùng validator thật thay thế.
Bảng Tham Khảo Nhanh Validators
| Validator | Mục đích | Key Options |
|---|---|---|
required | Không được trống | when, message |
string | Ràng buộc độ dài | min, max, length |
integer | Phải là integer | min, max |
number | Phải là số | min, max |
boolean | Phải là 0/1 | trueValue, falseValue |
email | Format email hợp lệ | checkDNS |
url | URL hợp lệ | defaultScheme |
match | Regex pattern | pattern, not |
compare | So sánh với field khác | compareAttribute, operator |
unique | Unique trong DB table | targetClass, targetAttribute |
exist | Phải tồn tại trong DB | targetClass, targetAttribute |
date | Format date hợp lệ | format, min, max |
file | Validation file upload | extensions, maxSize |
image | Validation image file | minWidth, maxWidth |
in | Giá trị trong danh sách cho phép | range, strict |
trim | Bỏ whitespace | — |
default | Set giá trị mặc định | value |
filter | Transform giá trị | filter |
safe | Cho phép mass assignment | — |
3. Custom Validators
Khi built-in validators không đủ, Yii2 cho bạn hai lựa chọn: inline validators cho rules dùng một lần và standalone validator classes cho logic tái sử dụng.
Inline Validators
Inline validator là method trên model của bạn:
class Task extends ActiveRecord
{
public function rules()
{
return [
[['due_date'], 'validateFutureDate'],
[['title'], 'validateNoProfanity'],
];
}
/**
* Due date phải ở tương lai (chỉ cho task mới).
*/
public function validateFutureDate($attribute, $params)
{
if (!$this->hasErrors()) {
if ($this->$attribute && strtotime($this->$attribute) < time()) {
$this->addError($attribute, 'Due date must be in the future.');
}
}
}
/**
* Chặn từ ngữ không phù hợp trong task titles.
*/
public function validateNoProfanity($attribute, $params)
{
$blocked = ['badword1', 'badword2'];
foreach ($blocked as $word) {
if (stripos($this->$attribute, $word) !== false) {
$this->addError($attribute, 'Title contains inappropriate language.');
return;
}
}
}
}Những điểm chính:
- Method signature:
function validateXxx($attribute, $params) - Dùng
$this->$attributeđể truy cập giá trị - Gọi
$this->addError($attribute, $message)để đánh dấu invalid - Check
$this->hasErrors()trước để skip nếu validators khác đã fail
Inline Validator với when (Conditional Validation)
public function rules()
{
return [
// Chỉ require reason khi status là 'rejected'
[['rejection_reason'], 'required', 'when' => function ($model) {
return $model->status === self::STATUS_REJECTED;
}, 'whenClient' => "function (attribute, value) {
return $('#task-status').val() == '3';
}"],
];
}Callback when chạy trên server. whenClient là JavaScript function cho client-side validation — phải là string JS code.
Standalone Validator Class
Cho validation logic tái sử dụng, tạo standalone validator:
// validators/PhoneNumberValidator.php
namespace app\validators;
use yii\validators\Validator;
class PhoneNumberValidator extends Validator
{
public $country = 'US';
private $patterns = [
'US' => '/^(\+1)?[2-9]\d{2}[2-9]\d{6}$/',
'VN' => '/^(\+84|0)(3|5|7|8|9)\d{8}$/',
'UK' => '/^(\+44|0)7\d{9}$/',
];
public function init()
{
parent::init();
if ($this->message === null) {
$this->message = 'Invalid phone number for {country}.';
}
}
protected function validateValue($value)
{
$pattern = $this->patterns[$this->country] ?? $this->patterns['US'];
if (!preg_match($pattern, $value)) {
return [$this->message, ['country' => $this->country]];
}
return null; // hợp lệ
}
}Sử dụng trong model:
use app\validators\PhoneNumberValidator;
public function rules()
{
return [
[['phone'], PhoneNumberValidator::class, 'country' => 'VN'],
];
}Standalone Validator với validateAttribute
Khi bạn cần truy cập model (không chỉ giá trị), override validateAttribute:
// validators/DateRangeValidator.php
namespace app\validators;
use yii\validators\Validator;
class DateRangeValidator extends Validator
{
public $startAttribute;
public $maxDays = 30;
public function validateAttribute($model, $attribute)
{
$start = strtotime($model->{$this->startAttribute});
$end = strtotime($model->$attribute);
if ($end < $start) {
$this->addError($model, $attribute, 'End date must be after start date.');
}
$days = ($end - $start) / 86400;
if ($days > $this->maxDays) {
$this->addError($model, $attribute,
"Date range cannot exceed {$this->maxDays} days.");
}
}
}// Trong model:
public function rules()
{
return [
[['end_date'], DateRangeValidator::class,
'startAttribute' => 'start_date',
'maxDays' => 90],
];
}4. Model Scenarios
Scenario cho phép bạn áp dụng validation rules khác nhau cho cùng model tùy theo context. Model User cần rules khác nhau cho registration vs profile update vs admin edit.
Định Nghĩa Scenarios
class User extends ActiveRecord
{
const SCENARIO_REGISTER = 'register';
const SCENARIO_UPDATE_PROFILE = 'update-profile';
const SCENARIO_ADMIN_EDIT = 'admin-edit';
public $password_repeat;
public function rules()
{
return [
// Áp dụng trong TẤT CẢ scenarios (không có 'on')
[['username', 'email'], 'required'],
[['username'], 'string', 'min' => 3, 'max' => 32],
[['email'], 'email'],
[['email'], 'unique'],
// Chỉ khi registration
[['password', 'password_repeat'], 'required',
'on' => self::SCENARIO_REGISTER],
[['password'], 'string', 'min' => 8,
'on' => self::SCENARIO_REGISTER],
[['password_repeat'], 'compare', 'compareAttribute' => 'password',
'on' => self::SCENARIO_REGISTER],
// Khi update profile — password là optional
[['password'], 'string', 'min' => 8,
'on' => self::SCENARIO_UPDATE_PROFILE],
// Admin có thể set role và status
[['role', 'status'], 'required',
'on' => self::SCENARIO_ADMIN_EDIT],
[['role'], 'in', 'range' => ['user', 'editor', 'admin'],
'on' => self::SCENARIO_ADMIN_EDIT],
];
}
}Sử Dụng Scenarios Trong Controllers
// Registration
public function actionRegister()
{
$model = new User();
$model->scenario = User::SCENARIO_REGISTER;
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
$model->setPassword($model->password);
$model->save(false); // skip validation (đã validate rồi)
return $this->redirect(['login']);
}
return $this->render('register', ['model' => $model]);
}
// Profile update
public function actionUpdateProfile()
{
$model = Yii::$app->user->identity;
$model->scenario = User::SCENARIO_UPDATE_PROFILE;
if ($model->load(Yii::$app->request->post()) && $model->save()) {
Yii::$app->session->setFlash('success', 'Profile updated.');
return $this->redirect(['profile']);
}
return $this->render('update-profile', ['model' => $model]);
}
// Admin chỉnh sửa user
public function actionAdminEdit($id)
{
$model = User::findOne($id);
$model->scenario = User::SCENARIO_ADMIN_EDIT;
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['admin/users']);
}
return $this->render('admin-edit', ['model' => $model]);
}Scenarios và Safe Attributes
Một chi tiết quan trọng: chỉ attributes xuất hiện trong active rules cho scenario hiện tại mới safe cho mass assignment. Nghĩa là load() sẽ âm thầm bỏ qua attributes không có rules trong scenario hiện tại.
// Trong SCENARIO_REGISTER, những attributes này safe:
// username, email, password, password_repeat
// Trong SCENARIO_UPDATE_PROFILE, những attributes này safe:
// username, email, password
// Trong SCENARIO_ADMIN_EDIT, những attributes này safe:
// username, email, role, statusĐây là security feature — users không thể inject giá trị cho fields mà họ không được phép thay đổi.
Override scenarios() Để Kiểm Soát Chi Tiết
Nếu bạn cần kiểm soát rõ ràng attributes nào safe cho mỗi scenario:
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios[self::SCENARIO_REGISTER] = [
'username', 'email', 'password', 'password_repeat',
];
$scenarios[self::SCENARIO_UPDATE_PROFILE] = [
'username', 'email', 'bio', 'avatar',
'!status', // prefix với ! = không safe (không thể mass-assign)
];
return $scenarios;
}5. ActiveForm Features Nâng Cao
Các Loại Field
ActiveForm::field() trả về object ActiveField với fluent methods để customize rendering:
<?php $form = ActiveForm::begin() ?>
<!-- Text input (mặc định) -->
<?= $form->field($model, 'title') ?>
<!-- Textarea -->
<?= $form->field($model, 'description')->textarea(['rows' => 6]) ?>
<!-- Password -->
<?= $form->field($model, 'password')->passwordInput() ?>
<!-- Dropdown -->
<?= $form->field($model, 'category_id')->dropDownList(
Category::find()
->select(['name', 'id'])
->indexBy('id')
->column(),
['prompt' => '-- Select Category --']
) ?>
<!-- Radio list -->
<?= $form->field($model, 'priority')->radioList([
1 => 'Low',
2 => 'Medium',
3 => 'High',
]) ?>
<!-- Checkbox list (cho many-to-many) -->
<?= $form->field($model, 'tag_ids')->checkboxList(
Tag::find()->select(['name', 'id'])->indexBy('id')->column()
) ?>
<!-- Single checkbox -->
<?= $form->field($model, 'agree_terms')->checkbox() ?>
<!-- Hidden input -->
<?= $form->field($model, 'user_id')->hiddenInput()->label(false) ?>
<!-- Date picker (HTML5) -->
<?= $form->field($model, 'due_date')->input('date') ?>
<!-- Number input với step -->
<?= $form->field($model, 'price')->input('number', [
'step' => '0.01',
'min' => '0',
]) ?>
<?php ActiveForm::end() ?>Custom Labels, Hints, và Placeholders
<?= $form->field($model, 'email')
->label('Email Address')
->hint('We will never share your email.')
->textInput(['placeholder' => '[email protected]']) ?>
<!-- Bỏ label hoàn toàn -->
<?= $form->field($model, 'search')
->textInput(['placeholder' => 'Search...'])
->label(false) ?>Custom Error Messages
Bạn có thể customize error messages ở rule level:
public function rules()
{
return [
[['email'], 'required',
'message' => 'We need your email to send the confirmation.'],
[['password'], 'string', 'min' => 8,
'tooShort' => 'Password must be at least {min} characters.'],
[['username'], 'unique',
'message' => '"{value}" is already taken. Try another.'],
];
}Form Layout Options
// Horizontal form (Bootstrap-style)
<?php $form = ActiveForm::begin([
'id' => 'contact-form',
'fieldConfig' => [
'template' => "{label}\n<div class=\"col-lg-6\">{input}</div>\n"
. "<div class=\"col-lg-6\">{error}\n{hint}</div>",
'labelOptions' => ['class' => 'col-lg-2 control-label'],
],
]) ?>
// Inline form
<?php $form = ActiveForm::begin([
'options' => ['class' => 'form-inline'],
'fieldConfig' => [
'template' => '{input}',
],
]) ?>6. AJAX Validation
Client-side validation bắt lỗi format, nhưng một số checks cần server — như kiểm tra email có unique không. AJAX validation gửi giá trị field riêng lẻ đến server khi user gõ, mà không submit toàn bộ form.
Bật AJAX Validation
<?php $form = ActiveForm::begin([
'id' => 'registration-form',
'enableAjaxValidation' => true, // bật cho tất cả fields
]) ?>
<!-- Hoặc bật cho từng field -->
<?= $form->field($model, 'email', ['enableAjaxValidation' => true]) ?>
<?= $form->field($model, 'username', ['enableAjaxValidation' => true]) ?>
<?= $form->field($model, 'password') ?> <!-- không AJAX validation cho field này -->Xử Lý AJAX Validation Trong Controller
use yii\web\Response;
use yii\widgets\ActiveForm;
public function actionRegister()
{
$model = new User();
$model->scenario = User::SCENARIO_REGISTER;
// Xử lý AJAX validation request
if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
Yii::$app->response->format = Response::FORMAT_JSON;
return ActiveForm::validate($model);
}
// Xử lý form submission bình thường
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
$model->setPassword($model->password);
$model->save(false);
Yii::$app->user->login($model);
return $this->redirect(['site/index']);
}
return $this->render('register', ['model' => $model]);
}AJAX Validation Hoạt Động Thế Nào
Validate Chỉ Specific Attributes
Để tránh chạy tất cả validators khi AJAX (có thể gây side effects):
if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
Yii::$app->response->format = Response::FORMAT_JSON;
// Chỉ validate attribute cụ thể đang được check
return ActiveForm::validate($model, ['email', 'username']);
}Validation Timing Options
<?php $form = ActiveForm::begin([
'enableAjaxValidation' => true,
'validationDelay' => 500, // ms trước khi trigger validation
'validateOnChange' => true, // validate khi giá trị field thay đổi
'validateOnBlur' => true, // validate khi field mất focus
'validateOnType' => false, // validate mỗi keystroke (tốn kém)
]) ?>7. File Uploads
File uploads cần xử lý đặc biệt: form phải dùng multipart/form-data encoding, model cần file hoặc image validators, và controller phải xử lý uploaded file.
Thiết Lập Model
class Document extends ActiveRecord
{
/**
* @var UploadedFile
*/
public $uploadedFile;
public function rules()
{
return [
[['title'], 'required'],
[['title'], 'string', 'max' => 255],
// File validation
[['uploadedFile'], 'file',
'extensions' => ['pdf', 'doc', 'docx', 'txt'],
'maxSize' => 5 * 1024 * 1024, // 5 MB
'tooBig' => 'File cannot be larger than 5 MB.',
],
// Required khi create, optional khi update
[['uploadedFile'], 'required', 'on' => 'create'],
];
}
public static function tableName()
{
return '{{%documents}}';
}
public function attributeLabels()
{
return [
'title' => 'Document Title',
'uploadedFile' => 'Attachment',
];
}
}Image Validation
Cho images, dùng image validator — nó mở rộng file với dimension checks:
class UserProfile extends ActiveRecord
{
public $avatarFile;
public function rules()
{
return [
[['avatarFile'], 'image',
'extensions' => ['png', 'jpg', 'jpeg', 'gif', 'webp'],
'maxSize' => 2 * 1024 * 1024, // 2 MB
'minWidth' => 100, 'minHeight' => 100,
'maxWidth' => 2000, 'maxHeight' => 2000,
],
];
}
}Upload Form View
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
$form = ActiveForm::begin([
'options' => ['enctype' => 'multipart/form-data'], // BẮT BUỘC cho file upload
]);
?>
<?= $form->field($model, 'title') ?>
<?= $form->field($model, 'uploadedFile')->fileInput([
'accept' => '.pdf,.doc,.docx,.txt',
]) ?>
<?= Html::submitButton('Upload', ['class' => 'btn btn-primary']) ?>
<?php ActiveForm::end() ?>Xử Lý Upload Trong Controller
use yii\web\UploadedFile;
public function actionCreate()
{
$model = new Document();
$model->scenario = 'create';
if ($model->load(Yii::$app->request->post())) {
$model->uploadedFile = UploadedFile::getInstance($model, 'uploadedFile');
if ($model->validate()) {
// Generate tên file unique
$filename = Yii::$app->security->generateRandomString(16)
. '.' . $model->uploadedFile->extension;
$uploadPath = Yii::getAlias('@webroot/uploads/' . $filename);
if ($model->uploadedFile->saveAs($uploadPath)) {
$model->file_path = '/uploads/' . $filename;
$model->file_name = $model->uploadedFile->baseName
. '.' . $model->uploadedFile->extension;
$model->file_size = $model->uploadedFile->size;
$model->save(false);
Yii::$app->session->setFlash('success', 'Document uploaded.');
return $this->redirect(['view', 'id' => $model->id]);
}
}
}
return $this->render('create', ['model' => $model]);
}Multiple File Uploads
Để upload nhiều files cùng lúc:
// Model
class Gallery extends Model
{
/**
* @var UploadedFile[]
*/
public $imageFiles;
public function rules()
{
return [
[['imageFiles'], 'file',
'extensions' => ['png', 'jpg', 'jpeg'],
'maxSize' => 2 * 1024 * 1024,
'maxFiles' => 10,
],
];
}
}// View — chú ý `[]` trong name attribute và option `multiple`
<?= $form->field($model, 'imageFiles[]')->fileInput([
'multiple' => true,
'accept' => 'image/*',
]) ?>// Controller
$model->imageFiles = UploadedFile::getInstances($model, 'imageFiles');
if ($model->validate()) {
foreach ($model->imageFiles as $file) {
$filename = Yii::$app->security->generateRandomString(16)
. '.' . $file->extension;
$file->saveAs(Yii::getAlias('@webroot/uploads/' . $filename));
}
}Checklist Security Upload
Đừng bao giờ tin client. Luôn validate trên server.
- Validate cả file extension VÀ MIME type —
filevalidator check extensions; thêmcheckExtensionByMimeType => true(mặc định) - Generate random filenames — đừng bao giờ dùng tên file gốc trực tiếp (rủi ro path traversal)
- Lưu ngoài webroot nếu có thể — serve files qua controller action với access checks
- Set giới hạn file size — cả trong PHP (
upload_max_filesize,post_max_size) và trong validator - Giới hạn permissions thư mục upload — không execute permission
8. Tabular Input
Tabular input cho phép bạn chỉnh sửa nhiều models trong một form. Các use cases phổ biến: bulk editing items, thêm nhiều records cùng lúc, hoặc chỉnh sửa parent record cùng children.
Form — Chỉnh Sửa Nhiều Tasks
<?php
// views/task/bulk-edit.php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
$form = ActiveForm::begin();
?>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
<?php foreach ($models as $i => $model): ?>
<tr>
<td>
<?= $form->field($model, "[$i]title")
->textInput()
->label(false) ?>
</td>
<td>
<?= $form->field($model, "[$i]status")
->dropDownList([
1 => 'To Do',
2 => 'In Progress',
3 => 'Done',
])
->label(false) ?>
</td>
<td>
<?= $form->field($model, "[$i]priority")
->dropDownList([
1 => 'Low',
2 => 'Medium',
3 => 'High',
])
->label(false) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?= Html::submitButton('Save All', ['class' => 'btn btn-primary']) ?>
<?php ActiveForm::end() ?>Chìa khóa là prefix [$i] trong attribute name. Nó generate HTML kiểu:
<input name="Task[0][title]" value="First task">
<input name="Task[1][title]" value="Second task">Controller — Batch Saving
public function actionBulkEdit()
{
$models = Task::find()
->where(['user_id' => Yii::$app->user->id])
->orderBy('id')
->all();
if (Yii::$app->request->isPost) {
$post = Yii::$app->request->post();
$valid = true;
// Load data vào mỗi model
foreach ($models as $i => $model) {
if ($model->load($post['Task'][$i] ?? [], '')) {
if (!$model->validate()) {
$valid = false;
}
}
}
if ($valid) {
$transaction = Yii::$app->db->beginTransaction();
try {
foreach ($models as $model) {
$model->save(false);
}
$transaction->commit();
Yii::$app->session->setFlash('success', 'Tasks updated.');
return $this->redirect(['index']);
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
}
}
return $this->render('bulk-edit', ['models' => $models]);
}Dynamic Rows — Thêm Models Mới
Cho forms mà users có thể thêm rows động:
// Controller
public function actionBulkCreate()
{
$count = Yii::$app->request->post('count', 3);
$models = [];
if (Yii::$app->request->isPost && isset($_POST['Task'])) {
foreach ($_POST['Task'] as $i => $data) {
$model = new Task();
$model->load($data, '');
$models[] = $model;
}
// Validate tất cả
$valid = true;
foreach ($models as $model) {
if (!$model->validate()) {
$valid = false;
}
}
if ($valid) {
$transaction = Yii::$app->db->beginTransaction();
try {
foreach ($models as $model) {
$model->user_id = Yii::$app->user->id;
$model->save(false);
}
$transaction->commit();
Yii::$app->session->setFlash('success',
count($models) . ' tasks created.');
return $this->redirect(['index']);
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
}
} else {
// Khởi tạo empty models
for ($i = 0; $i < $count; $i++) {
$models[] = new Task();
}
}
return $this->render('bulk-create', ['models' => $models]);
}Thêm/Xóa Rows Bằng JavaScript
// View với dynamic row management
<?php
$this->registerJs(<<<JS
let rowIndex = <?= count($models) ?>;
$('#add-row').on('click', function () {
const template = $('#row-template').html().replace(/__INDEX__/g, rowIndex);
$('tbody').append(template);
rowIndex++;
});
$(document).on('click', '.remove-row', function () {
$(this).closest('tr').remove();
});
JS);
?>
<!-- Hidden template cho rows mới -->
<script type="text/template" id="row-template">
<tr>
<td><input type="text" name="Task[__INDEX__][title]" class="form-control"></td>
<td>
<select name="Task[__INDEX__][status]" class="form-control">
<option value="1">To Do</option>
<option value="2">In Progress</option>
<option value="3">Done</option>
</select>
</td>
<td><button type="button" class="btn btn-danger remove-row">×</button></td>
</tr>
</script>
<button type="button" id="add-row" class="btn btn-success">+ Add Task</button>9. Ghép Tất Cả Lại — Complete Task Form
Hãy build một complete task form kết hợp mọi thứ: ActiveForm, nhiều validators, file attachment, AJAX validation cho title, và xử lý scenario đúng cách.
Task Model (Nâng Cao)
// models/Task.php
namespace app\models;
use yii\db\ActiveRecord;
use yii\web\UploadedFile;
class Task extends ActiveRecord
{
const STATUS_TODO = 1;
const STATUS_IN_PROGRESS = 2;
const STATUS_DONE = 3;
const SCENARIO_CREATE = 'create';
const SCENARIO_UPDATE = 'update';
/**
* @var UploadedFile
*/
public $attachmentFile;
public static function tableName()
{
return '{{%tasks}}';
}
public function rules()
{
return [
// Filters (chạy trước)
[['title', 'description'], 'trim'],
[['status'], 'default', 'value' => self::STATUS_TODO],
[['priority'], 'default', 'value' => 2],
// Required fields
[['title'], 'required'],
[['title'], 'string', 'min' => 3, 'max' => 255],
[['title'], 'unique',
'targetAttribute' => ['title', 'user_id'],
'message' => 'You already have a task with this title.'],
// Optional fields
[['description'], 'string', 'max' => 5000],
[['status'], 'in', 'range' => [
self::STATUS_TODO,
self::STATUS_IN_PROGRESS,
self::STATUS_DONE,
]],
[['priority'], 'integer', 'min' => 1, 'max' => 3],
// Date validation
[['due_date'], 'date', 'format' => 'php:Y-m-d'],
[['due_date'], 'validateFutureDate', 'on' => self::SCENARIO_CREATE],
// File attachment
[['attachmentFile'], 'file',
'extensions' => ['pdf', 'doc', 'docx', 'png', 'jpg'],
'maxSize' => 5 * 1024 * 1024,
],
// Foreign key
[['user_id'], 'exist',
'targetClass' => User::class,
'targetAttribute' => 'id'],
];
}
public function validateFutureDate($attribute)
{
if ($this->$attribute && strtotime($this->$attribute) < strtotime('today')) {
$this->addError($attribute, 'Due date must be today or later.');
}
}
public function attributeLabels()
{
return [
'title' => 'Task Title',
'description' => 'Description',
'status' => 'Status',
'priority' => 'Priority',
'due_date' => 'Due Date',
'attachmentFile' => 'Attachment',
];
}
public function getStatusLabel()
{
$labels = [
self::STATUS_TODO => 'To Do',
self::STATUS_IN_PROGRESS => 'In Progress',
self::STATUS_DONE => 'Done',
];
return $labels[$this->status] ?? 'Unknown';
}
public function getUser()
{
return $this->hasOne(User::class, ['id' => 'user_id']);
}
}View — Form Đầy Đủ Feature
<?php
// views/task/_form.php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
$form = ActiveForm::begin([
'id' => 'task-form',
'enableAjaxValidation' => true,
'options' => ['enctype' => 'multipart/form-data'],
'fieldConfig' => [
'options' => ['class' => 'form-group'],
],
]);
?>
<?= $form->field($model, 'title', ['enableAjaxValidation' => true])
->textInput(['maxlength' => true, 'autofocus' => true])
->hint('Must be unique among your tasks.') ?>
<?= $form->field($model, 'description')
->textarea(['rows' => 5, 'placeholder' => 'Describe the task...']) ?>
<div class="row">
<div class="col-md-4">
<?= $form->field($model, 'status')->dropDownList([
1 => 'To Do',
2 => 'In Progress',
3 => 'Done',
]) ?>
</div>
<div class="col-md-4">
<?= $form->field($model, 'priority')->radioList([
1 => 'Low',
2 => 'Medium',
3 => 'High',
]) ?>
</div>
<div class="col-md-4">
<?= $form->field($model, 'due_date')->input('date') ?>
</div>
</div>
<?= $form->field($model, 'attachmentFile')->fileInput([
'accept' => '.pdf,.doc,.docx,.png,.jpg',
]) ?>
<?php if ($model->attachment_path): ?>
<p class="text-muted">
File hiện tại: <?= Html::encode($model->attachment_name) ?>
(<?= Yii::$app->formatter->asShortSize($model->attachment_size) ?>)
</p>
<?php endif; ?>
<div class="form-group">
<?= Html::submitButton(
$model->isNewRecord ? 'Create Task' : 'Update Task',
['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']
) ?>
<?= Html::a('Cancel', ['index'], ['class' => 'btn btn-default']) ?>
</div>
<?php ActiveForm::end() ?>Controller — Create và Update
// controllers/TaskController.php
use yii\web\Response;
use yii\web\UploadedFile;
use yii\widgets\ActiveForm;
public function actionCreate()
{
$model = new Task();
$model->scenario = Task::SCENARIO_CREATE;
$model->user_id = Yii::$app->user->id;
// AJAX validation
if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
Yii::$app->response->format = Response::FORMAT_JSON;
return ActiveForm::validate($model);
}
if ($model->load(Yii::$app->request->post())) {
$model->attachmentFile = UploadedFile::getInstance($model, 'attachmentFile');
if ($model->validate()) {
// Xử lý file upload
if ($model->attachmentFile) {
$this->saveAttachment($model);
}
$model->save(false);
Yii::$app->session->setFlash('success', 'Task created.');
return $this->redirect(['view', 'id' => $model->id]);
}
}
return $this->render('create', ['model' => $model]);
}
public function actionUpdate($id)
{
$model = $this->findTask($id);
$model->scenario = Task::SCENARIO_UPDATE;
// AJAX validation
if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
Yii::$app->response->format = Response::FORMAT_JSON;
return ActiveForm::validate($model);
}
if ($model->load(Yii::$app->request->post())) {
$model->attachmentFile = UploadedFile::getInstance($model, 'attachmentFile');
if ($model->validate()) {
if ($model->attachmentFile) {
// Xóa file cũ nếu thay thế
if ($model->attachment_path) {
$oldPath = Yii::getAlias('@webroot' . $model->attachment_path);
if (file_exists($oldPath)) {
unlink($oldPath);
}
}
$this->saveAttachment($model);
}
$model->save(false);
Yii::$app->session->setFlash('success', 'Task updated.');
return $this->redirect(['view', 'id' => $model->id]);
}
}
return $this->render('update', ['model' => $model]);
}
private function saveAttachment(Task $model)
{
$filename = Yii::$app->security->generateRandomString(16)
. '.' . $model->attachmentFile->extension;
$path = '/uploads/tasks/' . $filename;
$model->attachmentFile->saveAs(Yii::getAlias('@webroot' . $path));
$model->attachment_path = $path;
$model->attachment_name = $model->attachmentFile->baseName
. '.' . $model->attachmentFile->extension;
$model->attachment_size = $model->attachmentFile->size;
}
private function findTask($id)
{
$model = Task::findOne([
'id' => $id,
'user_id' => Yii::$app->user->id,
]);
if ($model === null) {
throw new \yii\web\NotFoundHttpException('Task not found.');
}
return $model;
}Request Flow
10. Common Patterns & Tips
Pattern 1: Form Model Cho Complex Forms
Khi form không map trực tiếp tới một database table, dùng form model:
// models/ContactForm.php
namespace app\models;
use yii\base\Model;
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
public function rules()
{
return [
[['name', 'email', 'subject', 'body'], 'required'],
[['email'], 'email'],
[['subject'], 'string', 'max' => 128],
[['body'], 'string', 'max' => 5000],
];
}
public function sendEmail()
{
if (!$this->validate()) {
return false;
}
return Yii::$app->mailer->compose()
->setTo(Yii::$app->params['adminEmail'])
->setFrom([Yii::$app->params['senderEmail'] => $this->name])
->setReplyTo([$this->email => $this->name])
->setSubject($this->subject)
->setTextBody($this->body)
->send();
}
}Pattern 2: Dependent Dropdowns
Khi dropdown phụ thuộc vào dropdown khác (ví dụ: country → city):
// View
<?= $form->field($model, 'country_id')->dropDownList(
Country::find()->select(['name', 'id'])->indexBy('id')->column(),
[
'prompt' => '-- Select Country --',
'id' => 'country-id',
]
) ?>
<?= $form->field($model, 'city_id')->dropDownList([], [
'prompt' => '-- Select City --',
'id' => 'city-id',
]) ?>
<?php
$this->registerJs(<<<JS
$('#country-id').on('change', function () {
var countryId = $(this).val();
if (!countryId) {
$('#city-id').html('<option value="">-- Select City --</option>');
return;
}
$.get('/api/cities', { country_id: countryId }, function (data) {
var options = '<option value="">-- Select City --</option>';
$.each(data, function (id, name) {
options += '<option value="' + id + '">' + name + '</option>';
});
$('#city-id').html(options);
});
});
JS);
?>// Controller action cho API
public function actionCities($country_id)
{
Yii::$app->response->format = Response::FORMAT_JSON;
return City::find()
->select(['name', 'id'])
->where(['country_id' => $country_id])
->indexBy('id')
->column();
}Pattern 3: Xác Nhận Trước Destructive Actions
<?= Html::a('Delete', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Are you sure you want to delete this task?',
'method' => 'post',
],
]) ?>JavaScript của Yii2 tự động chuyển thành confirmation dialog + POST request.
Pattern 4: Cross-Attribute Validation
Khi validation phụ thuộc vào nhiều fields:
public function rules()
{
return [
[['discount_percent'], 'number', 'min' => 0, 'max' => 100],
[['discount_code'], 'required',
'when' => function ($model) {
return $model->discount_percent > 0;
},
'whenClient' => "function (attribute, value) {
return parseFloat($('#order-discount_percent').val()) > 0;
}",
'message' => 'Discount code is required when offering a discount.',
],
];
}Debug Validation
Khi validation fail mà bạn không chắc tại sao:
$model->load(Yii::$app->request->post());
if (!$model->validate()) {
// Xem tất cả validation errors
var_dump($model->errors);
// Output: ['title' => ['Title cannot be blank.'], 'email' => [...]]
// Check attributes nào đã được load (safe attributes)
var_dump($model->safeAttributes());
// Check scenario hiện tại
var_dump($model->scenario);
// Check validators nào đang active
var_dump($model->getActiveValidators());
}Performance Tip: Skip Validation Khi Đã An Toàn
Khi bạn đã validate và chỉ cần save:
// save(false) = skip validation
$model->save(false);
// updateAll không trigger validation hay events
Task::updateAll(['status' => Task::STATUS_DONE], ['due_date' => '<' . date('Y-m-d')]);Chỉ dùng save(false) khi bạn đã gọi validate() rõ ràng trước đó trong cùng request. Đừng bao giờ dùng trên user input chưa validate.
Tiếp Theo
Task manager của bạn giờ đã có complete form system:
- ActiveForm generate fields với client-side validation từ
rules() - Built-in validators xử lý strings, emails, dates, unique checks, và nhiều hơn
- Custom validators cover domain-specific logic
- Scenarios cho phép cùng model enforce rules khác nhau theo context
- File uploads được validate và lưu trữ an toàn
- Tabular input xử lý bulk operations
- AJAX validation cho real-time feedback không cần reload trang
Deep Dive → RESTful API Development với Yii2 — build complete REST API với ActiveController, token authentication, rate limiting, serialization, và CORS.
📬 Đă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.