Back to blog

Kết Nối Database Trong Yii2: Active Record, Query Builder & Migrations

yii2phpactive-recorddatabasebackend
Kết Nối Database Trong Yii2: Active Record, Query Builder & Migrations

Phase 2 cho bạn thấy Controllers, Views và Models hoạt động với mảng in-memory. Tốt để hiểu pattern, nhưng app thực tế phải nói chuyện với database.

Phase 3 lấp đầy bức tranh: cấu hình kết nối MySQL thực, viết migration để version hóa schema, dùng Active Record biến database row thành PHP object, định nghĩa relation giữa các bảng, và bật Query Builder khi cần nhiều hơn CRUD đơn giản.

Cuối bài bạn sẽ có task manager chạy với database thật — cùng URL như Phase 2, cùng controller action, nhưng giờ mọi thứ đều được lưu lại.

Sẽ Xây Gì

Task manager có database thật:

tasks table
  id          INT AUTO_INCREMENT
  title       VARCHAR(255)
  description TEXT
  status      ENUM('pending','in_progress','done')
  user_id     INT (FK → users.id)
  created_at  DATETIME
 
users table
  id          INT AUTO_INCREMENT
  username    VARCHAR(50)
  email       VARCHAR(255)

Bạn có thể list, view, create, update và delete task — và load user của mỗi task bằng một query bổ sung qua Active Record relation.

Cấu Hình Component db

Kết nối database của Yii2 nằm trong config/db.php (basic app) hoặc common/config/main-local.php (advanced app).

Basic app:

// config/db.php
return [
    'class' => 'yii\db\Connection',
    'dsn'   => 'mysql:host=127.0.0.1;dbname=yii2_tasks',
    'username' => 'root',
    'password' => '',
    'charset'  => 'utf8mb4',
];

File này được reference từ config/web.php:

// config/web.php
$db = require __DIR__ . '/db.php';
 
return [
    // ...
    'components' => [
        'db' => $db,
        // ...
    ],
];

Driver được hỗ trợ:

DriverDSN prefixExtension
MySQL / MariaDBmysql:pdo_mysql
PostgreSQLpgsql:pdo_pgsql
SQLitesqlite:pdo_sqlite
MS SQL Serversqlsrv:pdo_sqlsrv

Truy cập kết nối ở bất cứ đâu qua Yii::$app->db.

Test kết nối từ terminal:

php yii migrate/create test_connection
# Nếu chạy không lỗi thì db config đúng rồi

Migrations — Version Hóa Schema

Migration là PHP class mô tả các thay đổi schema. Được commit vào git, chạy trên mọi môi trường, và có thể rollback.

Tạo Migration

php yii migrate/create create_users_table
php yii migrate/create create_tasks_table

Yii sinh file trong migrations/ (hoặc console/migrations/ với advanced app):

// migrations/m260312_000001_create_users_table.php
use yii\db\Migration;
 
class m260312_000001_create_users_table extends Migration
{
    public function up(): void
    {
        $this->createTable('users', [
            'id'         => $this->primaryKey(),
            'username'   => $this->string(50)->notNull()->unique(),
            'email'      => $this->string(255)->notNull()->unique(),
            'created_at' => $this->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
        ]);
    }
 
    public function down(): void
    {
        $this->dropTable('users');
    }
}
// migrations/m260312_000002_create_tasks_table.php
use yii\db\Migration;
 
class m260312_000002_create_tasks_table extends Migration
{
    public function up(): void
    {
        $this->createTable('tasks', [
            'id'          => $this->primaryKey(),
            'title'       => $this->string(255)->notNull(),
            'description' => $this->text()->null(),
            'status'      => "ENUM('pending','in_progress','done') NOT NULL DEFAULT 'pending'",
            'user_id'     => $this->integer()->notNull(),
            'created_at'  => $this->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
        ]);
 
        $this->addForeignKey(
            'fk_tasks_user_id',
            'tasks', 'user_id',
            'users', 'id',
            'CASCADE', 'CASCADE'
        );
 
        $this->createIndex('idx_tasks_user_id', 'tasks', 'user_id');
        $this->createIndex('idx_tasks_status',  'tasks', 'status');
    }
 
    public function down(): void
    {
        $this->dropForeignKey('fk_tasks_user_id', 'tasks');
        $this->dropTable('tasks');
    }
}

Chạy Migration

# Áp dụng tất cả migration chưa chạy
php yii migrate
 
# Rollback migration cuối cùng
php yii migrate/down
 
# Rollback 3 migration cuối
php yii migrate/down 3
 
# Xem lịch sử migration
php yii migrate/history
 
# Xem migration chưa chạy
php yii migrate/new

Các Method Của Migration Builder

// Kiểu cột
$this->primaryKey()        // INT AUTO_INCREMENT PRIMARY KEY
$this->bigPrimaryKey()     // BIGINT AUTO_INCREMENT PRIMARY KEY
$this->string(255)         // VARCHAR(255)
$this->text()              // TEXT
$this->integer()           // INT
$this->bigInteger()        // BIGINT
$this->boolean()           // TINYINT(1)
$this->decimal(10, 2)      // DECIMAL(10,2)
$this->dateTime()          // DATETIME
$this->timestamp()         // TIMESTAMP
$this->json()              // JSON (MySQL 5.7+)
 
// Modifier cho cột (chainable)
->notNull()
->null()
->unique()
->defaultValue('pending')
->defaultExpression('CURRENT_TIMESTAMP')
->unsigned()
->comment('User sở hữu task này')
 
// Thao tác schema
$this->addColumn('tasks', 'priority', $this->integer()->defaultValue(0));
$this->dropColumn('tasks', 'priority');
$this->renameColumn('tasks', 'description', 'body');
$this->alterColumn('tasks', 'title', $this->string(500)->notNull());
$this->addForeignKey(...);
$this->dropForeignKey(...);
$this->createIndex(...);
$this->dropIndex(...);

Active Record — Database Row Thành PHP Object

Active Record là ORM của Yii2. Mỗi model class map vào một bảng database. Mỗi instance map vào một row.

Tạo Model Class

# Dùng Gii (sẽ nói ở YII-9) hoặc viết tay
php yii gii/model --tableName=users --modelClass=User

Viết tay:

// models/User.php
namespace app\models;
 
use yii\db\ActiveRecord;
 
class User extends ActiveRecord
{
    // Tên bảng (tùy chọn — mặc định là snake_case của tên class)
    public static function tableName(): string
    {
        return 'users';
    }
 
    // Validation rules (giống yii\base\Model ở Phase 2)
    public function rules(): array
    {
        return [
            [['username', 'email'], 'required'],
            ['email', 'email'],
            ['username', 'string', 'min' => 3, 'max' => 50],
            ['username', 'unique'],
            ['email', 'unique'],
        ];
    }
 
    // Label cho form/error
    public function attributeLabels(): array
    {
        return [
            'id'         => 'ID',
            'username'   => 'Tên đăng nhập',
            'email'      => 'Email',
            'created_at' => 'Ngày tham gia',
        ];
    }
}
// models/Task.php
namespace app\models;
 
use yii\db\ActiveRecord;
 
class Task extends ActiveRecord
{
    const STATUS_PENDING     = 'pending';
    const STATUS_IN_PROGRESS = 'in_progress';
    const STATUS_DONE        = 'done';
 
    public static function tableName(): string
    {
        return 'tasks';
    }
 
    public function rules(): array
    {
        return [
            [['title', 'user_id'], 'required'],
            ['title', 'string', 'max' => 255],
            ['description', 'string'],
            ['status', 'in', 'range' => [self::STATUS_PENDING, self::STATUS_IN_PROGRESS, self::STATUS_DONE]],
            ['user_id', 'integer'],
            ['user_id', 'exist', 'targetClass' => User::class, 'targetAttribute' => 'id'],
        ];
    }
 
    public function attributeLabels(): array
    {
        return [
            'id'          => 'ID',
            'title'       => 'Tiêu đề task',
            'description' => 'Mô tả',
            'status'      => 'Trạng thái',
            'user_id'     => 'Người phụ trách',
            'created_at'  => 'Ngày tạo',
        ];
    }
 
    // Helper trả về label trạng thái
    public function getStatusLabel(): string
    {
        return [
            self::STATUS_PENDING     => 'Chờ xử lý',
            self::STATUS_IN_PROGRESS => 'Đang làm',
            self::STATUS_DONE        => 'Hoàn thành',
        ][$this->status] ?? $this->status;
    }
}

Kiến Trúc Active Record


Active Record CRUD

Create — Insert Row Mới

// Cách 1: new + save()
$task = new Task();
$task->title       = 'Viết unit test';
$task->description = 'Cover tất cả controller action';
$task->status      = Task::STATUS_PENDING;
$task->user_id     = 1;
 
if ($task->save()) {
    // INSERT thành công — $task->id đã được gán
    echo $task->id; // 1
} else {
    print_r($task->errors); // lỗi validation
}
 
// Cách 2: load từ POST + save()
$task = new Task();
if ($task->load(Yii::$app->request->post()) && $task->save()) {
    Yii::$app->session->setFlash('success', 'Đã tạo task!');
    return $this->redirect(['task/view', 'id' => $task->id]);
}
 
// Cách 3: batch insert (không qua validation — raw SQL)
Yii::$app->db->createCommand()->batchInsert(
    'tasks',
    ['title', 'status', 'user_id'],
    [
        ['Task A', 'pending', 1],
        ['Task B', 'pending', 2],
    ]
)->execute();

Read — Tìm Row

// Tìm theo primary key
$task = Task::findOne(42);          // trả về Task|null
$task = Task::findOne(['id' => 42]); // tương đương
 
// Tìm nhiều theo primary key
$tasks = Task::findAll([1, 2, 3]);  // trả về Task[]
 
// Tìm có điều kiện
$tasks = Task::find()
    ->where(['status' => 'pending'])
    ->all();                         // trả về Task[]
 
$task = Task::find()
    ->where(['id' => 42])
    ->one();                         // trả về Task|null
 
// Đếm
$count = Task::find()
    ->where(['user_id' => 1])
    ->count();
 
// Kiểm tra tồn tại
$exists = Task::find()
    ->where(['email' => '[email protected]'])
    ->exists();
 
// Giá trị scalar
$maxId = Task::find()->max('id');
$minId = Task::find()->min('id');
 
// Tất cả task của user, mới nhất trước
$tasks = Task::find()
    ->where(['user_id' => 1])
    ->orderBy(['created_at' => SORT_DESC])
    ->limit(10)
    ->offset(20)
    ->all();

Update — Sửa Row

// Tìm rồi sửa
$task = Task::findOne(42);
if ($task !== null) {
    $task->status = Task::STATUS_DONE;
    $task->save(); // chạy validation + UPDATE
}
 
// Load từ POST + save
$task = Task::findOne(42);
if ($task !== null && $task->load(Yii::$app->request->post()) && $task->save()) {
    $this->redirect(['task/view', 'id' => $task->id]);
}
 
// Mass update (không qua validation — SQL trực tiếp)
Task::updateAll(
    ['status' => 'done'],                           // SET
    ['user_id' => 1, 'status' => 'pending'] // WHERE
);
 
// Chỉ update một số attribute (bỏ qua validation khác)
$task->updateAttributes(['status' => Task::STATUS_DONE]);

Delete — Xóa Row

// Tìm rồi xóa
$task = Task::findOne(42);
if ($task !== null) {
    $task->delete(); // kích hoạt beforeDelete / afterDelete event
}
 
// Mass delete (không kích hoạt event)
Task::deleteAll(['status' => 'done', 'user_id' => 5]);

So Sánh save() vs. insert() vs. update()

MethodKhi record mớiKhi record đã cóValidation
save()INSERTUPDATECó (mặc định)
save(false)INSERTUPDATEKhông
insert()INSERTN/ACó
update()N/AUPDATECó
updateAll()N/ABatch UPDATEKhông
deleteAll()N/ABatch DELETEKhông

Relations — Kết Nối Các Model

Relation mô tả quan hệ giữa các bảng. Yii2 xử lý SQL JOIN (hoặc query thứ hai) cho bạn.

Định Nghĩa Relation

// models/Task.php
class Task extends ActiveRecord
{
    // Task thuộc về một user
    public function getUser(): \yii\db\ActiveQuery
    {
        return $this->hasOne(User::class, ['id' => 'user_id']);
    }
 
    // Task có nhiều comment (nếu thêm bảng comments)
    public function getComments(): \yii\db\ActiveQuery
    {
        return $this->hasMany(Comment::class, ['task_id' => 'id']);
    }
}
 
// models/User.php
class User extends ActiveRecord
{
    // User có nhiều task
    public function getTasks(): \yii\db\ActiveQuery
    {
        return $this->hasMany(Task::class, ['user_id' => 'id']);
    }
}

Convention: getter relation là getXxx() — truy cập qua $task->user (magic property qua __get).

Dùng Relation

$task = Task::findOne(1);
 
// Lazy loading — chạy SELECT * FROM users WHERE id = ? khi lần đầu truy cập
$user = $task->user;       // object User
echo $user->username;
 
// Truy cập lại — không query thêm (đã cache trong $task->_related)
echo $task->user->email;

Eager Loading — Tránh N+1

Vấn đề N+1 kinh điển: 10 task → 10 query riêng để lấy user.

// TỆ — N+1 query
$tasks = Task::find()->all();
foreach ($tasks as $task) {
    echo $task->user->username; // SELECT cho mỗi task!
}
 
// TỐT — eager loading với with()
// Chỉ 2 query: một cho tasks, một cho tất cả user của chúng
$tasks = Task::find()
    ->with('user')
    ->all();
 
foreach ($tasks as $task) {
    echo $task->user->username; // không query thêm
}
 
// Nhiều relation
$tasks = Task::find()
    ->with(['user', 'comments'])
    ->all();
 
// Nested relation
$users = User::find()
    ->with(['tasks.comments'])
    ->all();

Minh Họa N+1

via() — Junction Table (Many-to-Many)

Với junction table task_tags:

// models/Task.php
public function getTags(): \yii\db\ActiveQuery
{
    return $this->hasMany(Tag::class, ['id' => 'tag_id'])
                ->viaTable('task_tags', ['task_id' => 'id']);
}
 
// HOẶC với model trung gian
public function getTaskTags(): \yii\db\ActiveQuery
{
    return $this->hasMany(TaskTag::class, ['task_id' => 'id']);
}
 
public function getTags(): \yii\db\ActiveQuery
{
    return $this->hasMany(Tag::class, ['id' => 'tag_id'])
                ->via('taskTags');
}

Query Builder — SQL Phức Tạp Trong PHP

Khi fluent API của Active Record không đủ, Query Builder cho phép viết SQL có cấu trúc mà không cần raw string.

Dùng Query Builder Cơ Bản

use yii\db\Query;
 
// SELECT với WHERE, ORDER, LIMIT
$rows = (new Query())
    ->select(['id', 'title', 'status', 'created_at'])
    ->from('tasks')
    ->where(['status' => 'pending'])
    ->andWhere(['>', 'id', 100])
    ->orderBy(['created_at' => SORT_DESC])
    ->limit(20)
    ->all(); // trả về mảng associative (không phải ActiveRecord object)
 
// Một row
$row = (new Query())
    ->from('tasks')
    ->where(['id' => 42])
    ->one();
 
// Scalar / column
$count = (new Query())
    ->from('tasks')
    ->where(['status' => 'done'])
    ->count();
 
$titles = (new Query())
    ->select('title')
    ->from('tasks')
    ->column(); // trả về mảng phẳng các giá trị

Điều Kiện WHERE

// Hash format — AND equality
->where(['status' => 'pending', 'user_id' => 1])
 
// Operator format
->where(['>', 'id', 100])
->where(['<>', 'status', 'done'])
->where(['in', 'status', ['pending', 'in_progress']])
->where(['not in', 'user_id', [3, 5]])
->where(['like', 'title', 'write'])        // LIKE '%write%'
->where(['like', 'title', 'write%', false]) // LIKE 'write%' (raw)
->where(['between', 'id', 10, 50])
->where(['is', 'description', null])        // IS NULL
->where(['is not', 'description', null])    // IS NOT NULL
 
// Kết hợp
->where(['status' => 'pending'])
->andWhere(['>', 'created_at', '2026-01-01'])
->orWhere(['user_id' => 99])
 
// String format (KHÔNG AN TOÀN nếu dùng user input)
->where('status = "pending"') // ĐỪNG làm vậy với dữ liệu user
 
// String có parameter
->where('status = :s AND user_id = :u', [':s' => 'pending', ':u' => 1])

JOIN và Grouping

// JOIN
$rows = (new Query())
    ->select(['t.id', 't.title', 'u.username'])
    ->from(['t' => 'tasks'])
    ->innerJoin(['u' => 'users'], 'u.id = t.user_id')
    ->where(['t.status' => 'pending'])
    ->all();
 
// LEFT JOIN
->leftJoin(['c' => 'comments'], 'c.task_id = t.id')
 
// GROUP BY + HAVING + aggregation
$summary = (new Query())
    ->select(['user_id', 'COUNT(*) AS task_count', 'MAX(created_at) AS latest'])
    ->from('tasks')
    ->groupBy('user_id')
    ->having(['>', 'task_count', 5])
    ->orderBy(['task_count' => SORT_DESC])
    ->all();

Active Query vs. Query Builder

Active Record (Task::find())Query Builder (new Query())
Trả vềObject Task[]Mảng associative array[]
Validation/Event✅ có❌ không
Relation✅ ->with()❌ phải join tay
Dùng khiCRUD thông thườngBáo cáo, aggregation, multi-table
PerformanceChậm hơn chút (object hydration)Nhanh hơn với result set lớn

Chạy Raw SQL

// Đọc
$rows = Yii::$app->db->createCommand('SELECT * FROM tasks WHERE status = :s', [
    ':s' => 'pending',
])->queryAll();
 
$row  = Yii::$app->db->createCommand($sql, $params)->queryOne();
$col  = Yii::$app->db->createCommand($sql, $params)->queryColumn();
$val  = Yii::$app->db->createCommand($sql, $params)->queryScalar();
 
// Ghi
Yii::$app->db->createCommand('UPDATE tasks SET status = :s WHERE id = :id', [
    ':s'  => 'done',
    ':id' => 42,
])->execute();

Transactions — Đảm Bảo Toàn Vẹn Dữ Liệu

Khi nhiều thao tác ghi phải cùng thành công hoặc cùng thất bại, bọc chúng trong transaction.

$db = Yii::$app->db;
$transaction = $db->beginTransaction();
 
try {
    $user = new User();
    $user->username = 'alice';
    $user->email    = '[email protected]';
    if (!$user->save()) {
        throw new \RuntimeException('Lưu user thất bại: ' . json_encode($user->errors));
    }
 
    $task = new Task();
    $task->title   = 'Task onboarding';
    $task->user_id = $user->id;
    $task->status  = Task::STATUS_PENDING;
    if (!$task->save()) {
        throw new \RuntimeException('Lưu task thất bại: ' . json_encode($task->errors));
    }
 
    $transaction->commit();
} catch (\Throwable $e) {
    $transaction->rollBack();
    Yii::error($e->getMessage());
    throw $e; // re-throw để controller xử lý
}

Dạng callback gọn hơn:

Yii::$app->db->transaction(function ($db) {
    $user = new User([
        'username' => 'alice',
        'email'    => '[email protected]',
    ]);
    if (!$user->save()) {
        throw new \Exception('Lưu user thất bại');
    }
 
    $task = new Task([
        'title'   => 'Task onboarding',
        'user_id' => $user->id,
        'status'  => Task::STATUS_PENDING,
    ]);
    if (!$task->save()) {
        throw new \Exception('Lưu task thất bại');
    }
});
// Exception bất kỳ sẽ trigger rollback tự động

Isolation level:

$transaction = $db->beginTransaction(\yii\db\Transaction::READ_COMMITTED);
// Các level khác: SERIALIZABLE, REPEATABLE_READ, READ_UNCOMMITTED

Ráp Lại Tất Cả — TaskController Với Database

Đây là controllers/TaskController.php được viết lại để dùng database:

namespace app\controllers;
 
use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use app\models\Task;
 
class TaskController extends Controller
{
    public function behaviors(): array
    {
        return [
            'verbs' => [
                'class'   => VerbFilter::class,
                'actions' => [
                    'delete' => ['POST'],
                ],
            ],
        ];
    }
 
    // GET /task
    public function actionIndex(): string
    {
        $tasks = Task::find()
            ->with('user')
            ->orderBy(['created_at' => SORT_DESC])
            ->all();
 
        return $this->render('index', ['tasks' => $tasks]);
    }
 
    // GET /task/view?id=1
    public function actionView(int $id): string
    {
        $task = $this->findTask($id);
        return $this->render('view', ['task' => $task]);
    }
 
    // GET/POST /task/create
    public function actionCreate(): string|\yii\web\Response
    {
        $task = new Task();
        $task->status = Task::STATUS_PENDING;
 
        if ($task->load(Yii::$app->request->post()) && $task->save()) {
            Yii::$app->session->setFlash('success', 'Đã tạo task thành công.');
            return $this->redirect(['view', 'id' => $task->id]);
        }
 
        return $this->render('create', ['task' => $task]);
    }
 
    // GET/POST /task/update?id=1
    public function actionUpdate(int $id): string|\yii\web\Response
    {
        $task = $this->findTask($id);
 
        if ($task->load(Yii::$app->request->post()) && $task->save()) {
            Yii::$app->session->setFlash('success', 'Đã cập nhật task.');
            return $this->redirect(['view', 'id' => $task->id]);
        }
 
        return $this->render('update', ['task' => $task]);
    }
 
    // POST /task/delete?id=1
    public function actionDelete(int $id): \yii\web\Response
    {
        $this->findTask($id)->delete();
        Yii::$app->session->setFlash('success', 'Đã xóa task.');
        return $this->redirect(['index']);
    }
 
    private function findTask(int $id): Task
    {
        $task = Task::findOne($id);
        if ($task === null) {
            throw new NotFoundHttpException("Task #$id không tìm thấy.");
        }
        return $task;
    }
}

Điểm khác so với Phase 2:

  • Task::find()->with('user')->all() thay mảng in-memory
  • new Task() và $task->save() thay thao tác mảng thủ công
  • Task::findOne($id) thay tìm kiếm trong mảng
  • $task->delete() thay array splice
  • Relation (with('user')) tự động load user liên quan

Fixtures — Seed Dữ Liệu Test

Fixture cho phép load dữ liệu cố định để test và development.

// tests/fixtures/UserFixture.php
namespace tests\fixtures;
 
use yii\test\ActiveFixture;
 
class UserFixture extends ActiveFixture
{
    public $modelClass = 'app\models\User';
    public $dataFile   = '@tests/fixtures/data/users.php';
    public $depends    = []; // load trước TaskFixture
}
// tests/fixtures/data/users.php
return [
    'alice' => [
        'username'   => 'alice',
        'email'      => '[email protected]',
        'created_at' => '2026-01-01 00:00:00',
    ],
    'bob' => [
        'username'   => 'bob',
        'email'      => '[email protected]',
        'created_at' => '2026-01-02 00:00:00',
    ],
];
// tests/fixtures/TaskFixture.php
namespace tests\fixtures;
 
use yii\test\ActiveFixture;
 
class TaskFixture extends ActiveFixture
{
    public $modelClass = 'app\models\Task';
    public $dataFile   = '@tests/fixtures/data/tasks.php';
    public $depends    = [UserFixture::class];
}
// tests/fixtures/data/tasks.php
return [
    'task1' => [
        'title'      => 'Viết unit test',
        'status'     => 'pending',
        'user_id'    => 1, // alice
        'created_at' => '2026-01-10 09:00:00',
    ],
    'task2' => [
        'title'      => 'Deploy lên staging',
        'status'     => 'in_progress',
        'user_id'    => 2, // bob
        'created_at' => '2026-01-11 10:00:00',
    ],
];

Load trong test class:

// tests/unit/TaskTest.php
use yii\test\FixtureTrait;
 
class TaskTest extends \Codeception\Test\Unit
{
    use FixtureTrait;
 
    public function fixtures(): array
    {
        return [
            'users' => UserFixture::class,
            'tasks' => TaskFixture::class,
        ];
    }
 
    public function testFindByStatus(): void
    {
        $pending = Task::find()->where(['status' => 'pending'])->all();
        $this->assertCount(1, $pending);
        $this->assertEquals('Viết unit test', $pending[0]->title);
    }
}

Toàn Cảnh Request — Tạo Task Với Database


Pattern Phổ Biến

Pagination Với ActiveDataProvider

use yii\data\ActiveDataProvider;
 
public function actionIndex(): string
{
    $dataProvider = new ActiveDataProvider([
        'query'      => Task::find()->with('user')->orderBy(['created_at' => SORT_DESC]),
        'pagination' => ['pageSize' => 10],
    ]);
 
    return $this->render('index', ['dataProvider' => $dataProvider]);
}

Trong view, dùng widget ListView hoặc GridView (sẽ nói ở YII-8).

Pattern Search / Filter Model

// models/TaskSearch.php
namespace app\models;
 
use yii\base\Model;
use yii\data\ActiveDataProvider;
 
class TaskSearch extends Task
{
    public function rules(): array
    {
        return [
            [['id', 'user_id'], 'integer'],
            [['title', 'status'], 'safe'],
        ];
    }
 
    public function scenarios(): array
    {
        return Model::scenarios();
    }
 
    public function search(array $params): ActiveDataProvider
    {
        $query = Task::find()->with('user');
 
        $dataProvider = new ActiveDataProvider([
            'query'      => $query,
            'pagination' => ['pageSize' => 10],
        ]);
 
        if (!$this->load($params) || !$this->validate()) {
            return $dataProvider;
        }
 
        $query->andFilterWhere(['id' => $this->id])
              ->andFilterWhere(['user_id' => $this->user_id])
              ->andFilterWhere(['status' => $this->status])
              ->andFilterWhere(['like', 'title', $this->title]);
 
        return $dataProvider;
    }
}

Trong controller:

public function actionIndex(): string
{
    $searchModel = new TaskSearch();
    $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
 
    return $this->render('index', [
        'searchModel'  => $searchModel,
        'dataProvider' => $dataProvider,
    ]);
}

Soft Delete Với beforeDelete Event

class Task extends ActiveRecord
{
    public function beforeDelete(): bool
    {
        if (!parent::beforeDelete()) {
            return false;
        }
        // Ghi log việc xóa
        Yii::info("Task #{$this->id} '{$this->title}' bị xóa bởi user " . Yii::$app->user->id);
        return true;
    }
}

Tự Động Timestamp Với TimestampBehavior

use yii\behaviors\TimestampBehavior;
use yii\db\Expression;
 
class Task extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            [
                'class'              => TimestampBehavior::class,
                'createdAtAttribute' => 'created_at',
                'updatedAtAttribute' => 'updated_at',
                'value'              => new Expression('NOW()'),
            ],
        ];
    }
}

Giờ created_at và updated_at tự được set khi save — không cần gán tay nữa.


Tiếp Theo

Giờ bạn đã có app Yii2 chạy với database thực sự:

  • Schema version hóa bằng migration
  • Bảng map thành Active Record model
  • Relation loại bỏ JOIN thủ công
  • Query Builder cho các query phức tạp
  • Transaction bảo vệ toàn vẹn dữ liệu

Phase 4 → Authentication & Authorization (RBAC) — kiểm soát ai được xem và làm gì: session, form login, behavior AccessControl, và RBAC phân cấp supported bằng database.

← Phase 2: MVC & Routing | → Tổng quan series Yii2

📬 Đă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.