Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 13: Seeder và Factory trong Laravel 13

Ở bài trước chúng ta đã có hai bảng projects, tasks nhưng chúng vẫn trống. Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về seeder và factory trong Laravel 13, để tạo dữ liệu mẫu chỉ với một câu lệnh.

1. Factory là gì?

Factory là class mô tả dữ liệu mặc định của một model, dùng để tạo nhanh các bản ghi giả khi seed database hoặc viết test.

Các factory nằm trong thư mục database/factories. Project mới đã có sẵn UserFactory cho model User (bỏ bớt comment):

- database/factories/UserFactory.php

class UserFactory extends Factory
{
    protected static ?string $password;

    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => static::$password ??= Hash::make('password'),
            'remember_token' => Str::random(10),
        ];
    }

    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

Trong đó:

  • definition() trả về mảng giá trị mặc định cho từng cột.
  • fake() là helper trả về object của thư viện Faker, sinh tên, email, địa chỉ... ngẫu nhiên. unique() đảm bảo không trùng.
  • Mật khẩu của mọi user là password, chỉ hash một lần rồi lưu vào static::$password cho nhanh.
  • unverified() là một state, phần 2 mình sẽ nói rõ.

Model (Bài 15) muốn dùng factory phải có trait HasFactory, model User có sẵn trait này. Factory cho Project, Task mình sẽ tạo bằng php artisan make:factory ở Bài 15, khi đã có model.

Faker tiếng Việt.

Mặc định Faker sinh tên tiếng Anh. Ngôn ngữ lấy từ option faker_locale trong config/app.php, đọc từ biến APP_FAKER_LOCALE trong .env (sửa cả .env.example):

APP_FAKER_LOCALE=vi_VN
fake()->name();        // Chị. Lều Giao Quế
fake('en_US')->name(); // Ardella Stroman

Truyền locale cho fake() để dùng ngôn ngữ khác cho riêng lần gọi đó. Tên tiếng Việt đôi khi kèm danh xưng như "Chị.", "Cụ.".

2. Tạo dữ liệu với factory.

Gọi User::factory() để lấy factory của model, rồi dùng make() hoặc create(). Mình chạy thử trong php artisan tinker, công cụ cho phép gõ code PHP trực tiếp với ứng dụng:

$user = User::factory()->make();
$user->name;   // Chị. Khúc Hoài
$user->exists; // false

$user = User::factory()->create();
$user->id;     // 1
$user->exists; // true

Trong đó:

  • make() chỉ tạo object, không lưu vào database. create() tạo và lưu luôn.
  • count($n) tạo $n bản ghi, VD User::factory()->count(3)->create(), kết quả là một collection (Bài 16).

Muốn đặt giá trị cụ thể cho vài cột, các bạn truyền mảng vào make()/create(), các cột còn lại vẫn lấy từ definition() (VD ở phần 4).

State.

State là method của factory sửa một vài giá trị mặc định, viết bằng $this->state() như unverified() ở trên. Closure nhận mảng giá trị đã sinh, trả về các cột cần đổi:

$user = User::factory()->unverified()->make(['name' => 'Trần Thị Bình']);
$user->email_verified_at; // null

Sequence.

Khi muốn mỗi bản ghi nhận một giá trị khác nhau theo thứ tự, các bạn dùng sequence(). Truyền nhiều mảng thì các giá trị được dùng xoay vòng: sequence(['name' => 'An'], ['name' => 'Bình']) với 4 bản ghi cho ra An, Bình, An, Bình. Hoặc truyền closure:

use Illuminate\Database\Eloquent\Factories\Sequence;

User::factory()->count(3)
    ->sequence(fn (Sequence $sequence) => ['email' => "user{$sequence->index}@taskflow.test"])
    ->make(); // [email protected], [email protected], [email protected]

Với closure, $sequence->index là số thứ tự của bản ghi, bắt đầu từ 0.

3. Seeder là gì?

Seeder là class chứa code đổ dữ liệu vào database, đặt trong database/seeders. DatabaseSeeder có sẵn là seeder chạy mặc định, từ đó gọi các seeder khác. Các bạn tạo seeder với cú pháp:

php artisan make:seeder ProjectSeeder
#  INFO  Seeder [database/seeders/ProjectSeeder.php] created successfully.

php artisan make:seeder TaskSeeder

Seeder chỉ có một method run(), chạy khi gọi lệnh db:seed. Trong run() các bạn dùng factory, hoặc Query Builder khi chưa có model. Ở đây mình dùng DB::table('projects')->insert() với mảng các bản ghi (tên cột => giá trị), Query Builder sẽ nói kỹ ở Bài 14.

- database/seeders/ProjectSeeder.php

use Illuminate\Support\Facades\DB;

public function run(): void
{
    $now = now();

    DB::table('projects')->insert([
        ['id' => 1, 'user_id' => 1, 'name' => 'Website toidicode.com', 'description' => 'Viết series Laravel 13 & làm mới giao diện', 'created_at' => $now, 'updated_at' => $now],
        ['id' => 2, 'user_id' => 1, 'name' => 'TaskFlow API', 'description' => null, 'created_at' => $now, 'updated_at' => $now],
        ['id' => 3, 'user_id' => 2, 'name' => 'Du lịch Đà Lạt', 'description' => 'Chuyến đi 3 ngày 2 đêm cùng nhóm bạn', 'created_at' => $now, 'updated_at' => $now],
    ]);
}

insert() không tự điền created_at, updated_at nên mình gán bằng now(). Mình ghi rõ id để TaskSeeder biết task thuộc project nào.

Với 10 task, mình khai báo mỗi task một dòng rồi dùng vòng lặp tạo các bản ghi. Chỉ task done mới có completed_at, task không có hạn thì due_date là null:

- database/seeders/TaskSeeder.php

use Illuminate\Support\Facades\DB;

public function run(): void
{
    // [project_id, title, status, hạn sau bao nhiêu ngày, assignee_id]
    $tasks = [
        [1, 'Viết bài Seeder và Factory', 'doing', 2, 1],
        [1, 'Viết bài Blade template', 'done', null, 1],
        [1, 'Thiết kế trang chủ', 'todo', 7, null],
        [1, 'Tối ưu ảnh trên trang', 'todo', null, null],
        [2, 'Thiết kế cấu trúc API', 'done', null, 2],
        [2, 'Viết API danh sách task', 'doing', 5, 2],
        [2, 'Viết tài liệu cho API', 'todo', 14, null],
        [3, 'Đặt vé xe khách', 'done', null, 3],
        [3, 'Đặt phòng homestay', 'doing', 3, 3],
        [3, 'Lên lịch trình tham quan', 'todo', null, null],
    ];

    $rows = [];

    foreach ($tasks as [$projectId, $title, $status, $days, $assigneeId]) {
        $rows[] = [
            'project_id' => $projectId,
            'title' => $title,
            'status' => $status,
            'due_date' => $days === null ? null : now()->addDays($days)->toDateString(),
            // chỉ task đã xong mới có thời điểm hoàn thành
            'completed_at' => $status === 'done' ? now() : null,
            'assignee_id' => $assigneeId,
            'created_at' => now(),
            'updated_at' => now(),
        ];
    }

    DB::table('tasks')->insert($rows);
}

4. DatabaseSeeder và chạy seeder.

Trong DatabaseSeeder, mình tạo 3 user bằng factory, trong đó user demo có email cố định để đăng nhập ở Bài 21, rồi dùng call() chạy lần lượt các seeder: project phải có trước task vì khóa ngoại.

- database/seeders/DatabaseSeeder.php

public function run(): void
{
    // user demo tạo đầu tiên nên có id 1, mật khẩu là "password"
    User::factory()->create([
        'name' => 'Nguyễn Văn An',
        'email' => '[email protected]',
    ]);

    User::factory()->count(2)->create();

    $this->call([
        ProjectSeeder::class,
        TaskSeeder::class,
    ]);
}

Trait WithoutModelEvents có sẵn trong class giữ nguyên, nó tắt model event (Bài 26) khi seed. Bảng users phải trống thì user demo mới có id 1, nên nếu đã tạo user trong tinker ở phần 2, các bạn chạy php artisan migrate:fresh trước. Giờ chạy seeder:

php artisan db:seed
#  INFO  Seeding database.
#
#  Database\Seeders\ProjectSeeder ..................................... RUNNING
#  Database\Seeders\ProjectSeeder ................................... 8 ms DONE
#
#  Database\Seeders\TaskSeeder ........................................ RUNNING
#  Database\Seeders\TaskSeeder ...................................... 9 ms DONE

db:seed chạy DatabaseSeeder. Muốn chạy riêng một seeder, các bạn thêm --class, VD php artisan db:seed --class=ProjectSeeder.

Seeder chỉ thêm dữ liệu, không xóa dữ liệu cũ. Vì mình đã ghi rõ id, chạy lại ProjectSeeder khi bảng đã có dữ liệu sẽ báo lỗi UNIQUE constraint failed: projects.id. Vì vậy hay dùng nhất là xóa bảng, migrate lại rồi seed trong một lệnh:

php artisan migrate:fresh --seed

Thêm --seeder=ProjectSeeder nếu muốn chạy seeder khác thay cho DatabaseSeeder. Lúc này database có 3 user, 3 project và 10 task: 4 todo, 3 doing, 3 done.

Chú ý: Khi APP_ENV=production, db:seed hỏi xác nhận trước khi chạy (--force để bỏ qua). Dữ liệu giả chỉ nên seed ở local.

5. Lời kết.

Như vậy chúng ta đã tìm hiểu xong seeder và factory trong Laravel 13: factory, state, sequence, seeder và migrate:fresh --seed. Các bạn xem thêm tài liệu về seeder và factory. Bài sau mình sẽ giới thiệu về Query Builder để truy vấn dữ liệu mẫu này, VD thống kê task theo trạng thái.

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha