Глава 9.5: Seeders и Factories — тестовые данные, фейковые записи для разработки
Введение: Зачем нам нужны тестовые данные?
Представь: ты создал мессенджер. Есть миграции, модели, контроллеры. Запускаешь приложение — пустота. Ни пользователей, ни чатов, ни сообщений. Чтобы протестировать функционал, тебе нужно вручную:
- Зарегистрировать 10 пользователей
- Создать между ними чаты
- Отправить сотни сообщений
- Загрузить файлы, аватарки...
На это уйдут часы. И всё это придётся повторять каждый раз, когда ты сбросишь базу данных.
Решение: автоматизировать создание тестовых данных. В Laravel для этого есть два инструмента:
- Factories — шаблоны для создания моделей с фейковыми данными
- Seeders — скрипты, которые наполняют базу данных
1. Factories — фабрики моделей
1.1 Что такое Factory?
Factory — это класс, который описывает, как создать экземпляр модели с реалистичными фейковыми данными.
Создание Factory:
php artisan make:factory UserFactoryФайл появится в database/factories/UserFactory.php:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
/**
* Определяем дефолтные значения для модели
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => Hash::make('password'), // Все тестовые пользователи с паролем "password"
'remember_token' => Str::random(10),
];
}
/**
* Состояние: непроверенный email
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}Ключевые моменты:
fake()— глобальный хелпер Laravel, возвращает объект Faker (библиотека для генерации фейковых данных)definition()— основной метод, возвращает массив атрибутов моделиstate()— позволяет создавать вариации (например, "пользователь без подтверждённого email")
1.2 Использование Factory
Создать одну модель:
use App\Models\User;
$user = User::factory()->create();
// Создаёт пользователя и СОХРАНЯЕТ в БДСоздать несколько:
$users = User::factory()->count(50)->create();
// 50 пользователей в БДСоздать без сохранения в БД (для тестов):
$user = User::factory()->make();
// Создаёт объект User, но НЕ сохраняет в базуПереопределить атрибуты:
$admin = User::factory()->create([
'name' => 'Admin',
'email' => 'admin@example.com',
'role' => 'admin'
]);Использовать состояния (states):
$unverified = User::factory()->unverified()->create();
// email_verified_at будет null1.3 Faker — генератор фейковых данных
Laravel использует библиотеку Faker для генерации реалистичных данных.
Примеры методов Faker:
// Личные данные
fake()->name() // "John Doe"
fake()->firstName() // "Jane"
fake()->lastName() // "Smith"
fake()->email() // "email@example.com"
fake()->safeEmail() // "email@example.org" (безопасный домен)
fake()->phoneNumber() // "+1-555-123-4567"
// Адреса
fake()->address() // "123 Main St, Apt 4B, New York, NY 10001"
fake()->city() // "Los Angeles"
fake()->country() // "United States"
fake()->postcode() // "90210"
// Даты
fake()->dateTimeBetween('-1 year', 'now') // Случайная дата за последний год
fake()->dateTime() // Случайная дата
fake()->date() // "2024-03-15"
// Текст
fake()->sentence() // "Lorem ipsum dolor sit amet."
fake()->paragraph() // Целый абзац текста
fake()->text(200) // Текст до 200 символов
fake()->words(5, true) // "hello world foo bar baz"
// Числа
fake()->numberBetween(1, 100) // 42
fake()->randomFloat(2, 0, 100) // 45.67
fake()->boolean() // true/false
// Интернет
fake()->url() // "https://example.com"
fake()->domainName() // "example.com"
fake()->ipv4() // "192.168.1.1"
fake()->userAgent() // "Mozilla/5.0..."
// Картинки (URL с placeholder-сервиса)
fake()->imageUrl(640, 480, 'cats') // "https://via.placeholder.com/640x480.png/cats"
// Случайные элементы
fake()->randomElement(['cat', 'dog', 'bird']) // "dog"
fake()->randomElements(['a', 'b', 'c'], 2) // ['a', 'c']Локализация (русский язык):
// В config/app.php
'faker_locale' => 'ru_RU',
// Теперь:
fake()->name() // "Иван Петров"
fake()->city() // "Москва"1.4 Связи между моделями в Factories
Пример: Post принадлежит User
// database/factories/PostFactory.php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(), // Автоматически создаст User
'title' => fake()->sentence(),
'content' => fake()->paragraphs(3, true),
'published_at' => fake()->dateTimeBetween('-6 months', 'now'),
];
}
}Использование:
// Создаст User И Post
$post = Post::factory()->create();
// Создать Post для конкретного User
$user = User::find(1);
$post = Post::factory()->create(['user_id' => $user->id]);
// Или через связь (удобнее):
$post = $user->posts()->create(Post::factory()->raw());Пример: User has many Posts
$user = User::factory()
->has(Post::factory()->count(10))
->create();
// Создаст пользователя с 10 постамиАльтернативный синтаксис:
$user = User::factory()
->hasPosts(10) // Короче!
->create();Many-to-Many (например, роли):
$user = User::factory()
->hasAttached(
Role::factory()->count(2),
['expires_at' => now()->addYear()] // Доп. поля pivot-таблицы
)
->create();1.5 Состояния (States) — вариации Factory
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'role' => 'user',
'is_active' => true,
];
}
// Состояние: администратор
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'role' => 'admin',
]);
}
// Состояние: неактивный
public function inactive(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => false,
]);
}
// Состояние: с подтверждённым email
public function verified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => now(),
]);
}
}Использование:
$admin = User::factory()->admin()->create();
$inactive = User::factory()->inactive()->create();
// Можно комбинировать:
$inactiveAdmin = User::factory()->admin()->inactive()->create();1.6 Callbacks — действия после создания
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
];
}
public function configure(): static
{
return $this->afterCreating(function (User $user) {
// Создать профиль для пользователя
$user->profile()->create([
'bio' => fake()->paragraph(),
'avatar' => fake()->imageUrl(200, 200, 'people'),
]);
// Добавить в команду по умолчанию
$user->teams()->attach(Team::first());
});
}
}2. Seeders — наполнение базы данных
2.1 Что такое Seeder?
Seeder — это класс, который запускает процесс наполнения БД тестовыми данными.
Создание Seeder:
php artisan make:seeder UserSeederФайл появится в database/seeders/UserSeeder.php:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
class UserSeeder extends Seeder
{
public function run(): void
{
// Создать 50 пользователей
User::factory()->count(50)->create();
// Создать конкретного админа
User::factory()->create([
'name' => 'Admin',
'email' => 'admin@example.com',
'role' => 'admin',
]);
}
}2.2 DatabaseSeeder — главный оркестратор
Файл database/seeders/DatabaseSeeder.php — точка входа для всех seeders:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Вызываем другие seeders
$this->call([
UserSeeder::class,
PostSeeder::class,
CommentSeeder::class,
]);
}
}Запуск:
# Запустить все seeders
php artisan db:seed
# Запустить конкретный seeder
php artisan db:seed --class=UserSeeder
# Сбросить БД и запустить seeders (свежий старт)
php artisan migrate:fresh --seed2.3 Пример: полноценный Seeder для мессенджера
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Chat;
use App\Models\Message;
class MessengerSeeder extends Seeder
{
public function run(): void
{
// 1. Создать пользователей
$users = User::factory()->count(20)->create();
// 2. Создать админа
$admin = User::factory()->admin()->create([
'name' => 'Admin',
'email' => 'admin@test.com',
]);
// 3. Создать чаты
$chats = Chat::factory()->count(15)->create();
// 4. Привязать пользователей к чатам (многие-ко-многим)
$chats->each(function ($chat) use ($users) {
// Каждый чат — 2-5 случайных участников
$chat->users()->attach(
$users->random(rand(2, 5))->pluck('id')
);
});
// 5. Создать сообщения в каждом чате
$chats->each(function ($chat) {
$participants = $chat->users;
// 10-50 сообщений в чате
Message::factory()->count(rand(10, 50))->create([
'chat_id' => $chat->id,
'user_id' => $participants->random()->id,
]);
});
}
}2.4 Условные seeders (для разных окружений)
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Только для локальной разработки
if (app()->environment('local')) {
$this->call([
TestUserSeeder::class,
FakeDataSeeder::class,
]);
}
// Для продакшена — только базовые данные
$this->call([
RoleSeeder::class,
SettingSeeder::class,
]);
}
}2.5 Seeders с существующими данными
class RoleSeeder extends Seeder
{
public function run(): void
{
$roles = ['admin', 'moderator', 'user'];
foreach ($roles as $role) {
Role::firstOrCreate(['name' => $role]);
// Создаст только если роли ещё нет
}
}
}3. Продвинутые техники
3.1 Sequence — последовательные значения
$users = User::factory()
->count(10)
->sequence(
['role' => 'admin'],
['role' => 'moderator'],
['role' => 'user'],
)
->create();
// Роли будут чередоваться: admin, moderator, user, admin, moderator...Или с функцией:
$posts = Post::factory()
->count(10)
->sequence(fn (Sequence $sequence) => [
'order' => $sequence->index + 1, // 1, 2, 3, 4...
])
->create();3.2 Recycle — переиспользовать модели
Без recycle() — каждый раз создаётся новый User:
Post::factory()->count(100)->create();
// Создаст 100 постов и 100 пользователейС recycle() — используются существующие User:
$users = User::factory()->count(5)->create();
Post::factory()
->count(100)
->recycle($users)
->create();
// 100 постов распределятся между 5 пользователями3.3 Cross — связь многие-ко-многим
$users = User::factory()->count(10)->create();
$roles = Role::factory()->count(3)->create();
// Связать каждого пользователя с 1-2 ролями
$users->each(function ($user) use ($roles) {
$user->roles()->attach(
$roles->random(rand(1, 2))->pluck('id')
);
});3.4 Производительность — массовая вставка
Медленно (100 INSERT запросов):
User::factory()->count(100)->create();Быстрее (1 INSERT запрос):
$users = User::factory()->count(100)->make()->toArray();
User::insert($users); // Массовая вставка⚠️ Минус: не сработают события модели (creating, created), автоинкремент может не вернуть ID.
4. Практические примеры
4.1 Блог с комментариями
// PostFactory.php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(),
'slug' => fake()->slug(),
'content' => fake()->paragraphs(5, true),
'published_at' => fake()->boolean(80)
? fake()->dateTimeBetween('-1 year', 'now')
: null,
];
}
public function published(): static
{
return $this->state(fn () => [
'published_at' => fake()->dateTimeBetween('-1 year', 'now'),
]);
}
public function draft(): static
{
return $this->state(fn () => [
'published_at' => null,
]);
}
}
// CommentFactory.php
class CommentFactory extends Factory
{
public function definition(): array
{
return [
'post_id' => Post::factory(),
'user_id' => User::factory(),
'content' => fake()->paragraph(),
'approved' => fake()->boolean(90),
];
}
}
// BlogSeeder.php
class BlogSeeder extends Seeder
{
public function run(): void
{
$users = User::factory()->count(10)->create();
$posts = Post::factory()
->count(30)
->recycle($users)
->create();
// Комментарии для каждого поста
$posts->each(function ($post) use ($users) {
Comment::factory()
->count(rand(0, 15))
->recycle($users)
->create([
'post_id' => $post->id,
]);
});
}
}4.2 Интернет-магазин
// ProductFactory.php
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->words(3, true),
'description' => fake()->paragraph(),
'price' => fake()->randomFloat(2, 10, 1000),
'stock' => fake()->numberBetween(0, 100),
'category_id' => Category::factory(),
'image_url' => fake()->imageUrl(400, 400, 'tech'),
];
}
public function outOfStock(): static
{
return $this->state(fn () => ['stock' => 0]);
}
}
// OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'status' => fake()->randomElement(['pending', 'paid', 'shipped', 'delivered']),
'total' => 0, // Вычислим позже
'created_at' => fake()->dateTimeBetween('-6 months', 'now'),
];
}
}
// ShopSeeder.php
class ShopSeeder extends Seeder
{
public function run(): void
{
$categories = Category::factory()->count(5)->create();
$products = Product::factory()
->count(50)
->recycle($categories)
->create();
$users = User::factory()->count(20)->create();
// Заказы
$users->each(function ($user) use ($products) {
$orderCount = rand(0, 5);
for ($i = 0; $i < $orderCount; $i++) {
$order = Order::factory()->create([
'user_id' => $user->id,
]);
// Товары в заказе
$orderProducts = $products->random(rand(1, 5));
$total = 0;
foreach ($orderProducts as $product) {
$quantity = rand(1, 3);
$price = $product->price;
$order->products()->attach($product->id, [
'quantity' => $quantity,
'price' => $price,
]);
$total += $price * $quantity;
}
$order->update(['total' => $total]);
}
});
}
}5. Частые ошибки и их решения
❌ Ошибка 1: Дублирование уникальных полей
// ПЛОХО
User::factory()->count(100)->create([
'email' => 'same@email.com' // Все 100 пользователей с одним email — ошибка!
]);
// ХОРОШО
User::factory()->count(100)->create();
// Faker автоматически создаст уникальные email через unique()❌ Ошибка 2: Забыли про foreign keys
// ПЛОХО — упадёт, если user_id не существует
Post::factory()->create(['user_id' => 9999]);
// ХОРОШО
Post::factory()->create(['user_id' => User::factory()]);❌ Ошибка 3: Слишком медленный seeder
// ПЛОХО — 1000 запросов
for ($i = 0; $i < 1000; $i++) {
User::factory()->create();
}
// ХОРОШО
User::factory()->count(1000)->create();
// Или ещё быстрее:
User::insert(User::factory()->count(1000)->make()->toArray());❌ Ошибка 4: Не очищаете БД перед seeding
# ПЛОХО
php artisan db:seed # Данные добавляются к существующим
# ХОРОШО
php artisan migrate:fresh --seed # Чистая БД + seeders6. Best Practices
✅ 1. Один Seeder = одна ответственность
// ПЛОХО
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()->count(100)->create();
Post::factory()->count(500)->create();
Comment::factory()->count(2000)->create();
// Всё в одной куче
}
}
// ХОРОШО
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
UserSeeder::class,
PostSeeder::class,
CommentSeeder::class,
]);
}
}✅ 2. Используйте состояния вместо дублирования
// ПЛОХО
User::factory()->create(['role' => 'admin']);
User::factory()->create(['role' => 'moderator']);
// ХОРОШО
User::factory()->admin()->create();
User::factory()->moderator()->create();✅ 3. Создавайте реалистичные данные
// ПЛОХО
'created_at' => now() // Все записи с одной датой
// ХОРОШО
'created_at' => fake()->dateTimeBetween('-1 year', 'now')✅ 4. Комментируйте сложную логику
public function run(): void
{
// Создаём 10 пользователей, каждый с 5-10 постами
$users = User::factory()
->count(10)
->has(Post::factory()->count(rand(5, 10)))
->create();
// Для каждого поста создаём 0-5 комментариев от случайных пользователей
Post::all()->each(function ($post) use ($users) {
Comment::factory()
->count(rand(0, 5))
->create([
'post_id' => $post->id,
'user_id' => $users->random()->id,
]);
});
}7. Упражнения
Задание 1: Базовая фабрика
Создай фабрику для модели Product с полями:
name(3-5 слов)description(абзац текста)price(от 100 до 10000)in_stock(boolean, 80% true)
Задание 2: Связи
Создай фабрики для Author и Book. У автора может быть много книг. Создай сидер, который сгенерирует:
- 10 авторов
- У каждого автора 2-5 книг
Задание 3: Состояния
Расширь фабрику Product состояниями:
discount()— цена со скидкой 20%featured()—is_featured = trueoutOfStock()—in_stock = false
Задание 4: Сложный сценарий
Создай систему для библиотеки:
- Users (читатели)
- Books (книги)
- Borrows (записи о выдаче книг)
Сидер должен:
- Создать 20 читателей
- Создать 100 книг
- Создать 50 записей о выдаче (случайные пользователи + книги)
- 70% записей — книга уже возвращена (
returned_at!= null) - 30% — ещё на руках (
returned_at= null)
Заключение
Factories и Seeders — это не просто удобство, это необходимость для продуктивной разработки. Они позволяют:
✅ Быстро наполнять БД реалистичными данными
✅ Тестировать функционал на больших объёмах
✅ Легко сбрасывать и пересоздавать базу
✅ Делиться тестовым окружением с командой
✅ Писать автоматические тесты с изолированными данными
Следующий шаг: Глава 10.1 — Аутентификация в Laravel. Научимся защищать приложение, создавать системы входа и управлять правами доступа.
Удачи! 🚀