Сидеры и фабрики в Laravel: что советуют на практике
Как заполнять базу тестовыми данными в Laravel так, чтобы сиды не ломались при повторном запуске, а фабрики не тащили лишние связи. Разбираем подходы и цену каждого.

Классическая ошибка: разработчик пишет сид, который создаёт пользователя с фиксированным email, запускает php artisan db:seed дважды и получает SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry. Или другой сценарий — фабрика поста тянет за собой создание категории, категория тянет автора, и на 500 постов в базе оказывается 500 лишних категорий. Оба случая решаются, но по-разному, и выбор зависит от того, где данные используются: в тестах, на демо-стенде или при первичном развёртывании прода.
Материал по Laravel 11.x и 12.x. Синтаксис фабрик как классов актуален начиная с Laravel 8; если вы на более старой ветке с определением фабрик через $factory->define() — часть примеров не подойдёт, сверяйтесь с документацией своей версии.
Три задачи, которые путают
Сидеры и фабрики часто описывают вместе, но они закрывают разные потребности. Разделите их до того, как начнёте писать:
- Справочные данные для прода — роли, статусы заказов, страны. Их создаёт сид, они должны быть идемпотентными: повторный запуск не плодит дубли.
- Демо-данные для стенда — правдоподобный контент, чтобы менеджер или дизайнер видели заполненные экраны. Здесь работают фабрики через сид.
- Данные для теста — минимальный набор ровно под один тест-кейс. Создаётся фабрикой внутри теста, живёт в транзакции и откатывается.
Смешение этих трёх — источник большинства проблем. Демо-сид с сотней случайных заказов не должен запускаться на проде, а идемпотентный справочник ролей не нужен внутри теста.
Идемпотентные сиды: firstOrCreate вместо create
Для справочников, которые попадают в прод, замените create() на firstOrCreate() или updateOrCreate(). Первый ищет запись по переданным атрибутам и создаёт только при отсутствии, второй ещё и обновляет остальные поля.
<?php
namespace Database\Seeders;
use App\Models\Role;
use Illuminate\Database\Seeder;
class RoleSeeder extends Seeder
{
public function run(): void
{
$roles = [
['slug' => 'admin', 'title' => 'Администратор'],
['slug' => 'manager', 'title' => 'Менеджер'],
['slug' => 'client', 'title' => 'Клиент'],
];
foreach ($roles as $role) {
// ищем по slug, обновляем название — повторный запуск безопасен
Role::updateOrCreate(
['slug' => $role['slug']],
['title' => $role['title']]
);
}
}
}Ключевой момент: первый массив в updateOrCreate — это условие поиска, второй — данные для записи. Если положить slug во второй массив, идемпотентность сломается, потому что Laravel будет искать по пустому набору условий.
Если сид обязан запускаться на проде при каждом деплое — он должен пережить сотый запуск так же спокойно, как первый. create() в таком сиде — мина замедленного действия.Фабрики: не тяните лишние связи
Фабрика по умолчанию для связанной модели создаёт новую запись. Определение вроде такого на каждый пост создаёт отдельного пользователя:
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
// проблема: каждый пост порождает нового автора
'user_id' => User::factory(),
];
}
}Если вы создаёте 200 постов, получите 200 пользователей. Обычно это не то, что нужно. Есть два способа управлять связью извне.
Способ 1: передавать существующую модель в сиде
// сначала создаём фиксированное число авторов
$authors = User::factory()->count(10)->create();
// затем раздаём посты по существующим авторам
App\Models\Post::factory()
->count(200)
->recycle($authors) // переиспользует переданные модели вместо создания новых
->create();Метод recycle() появился в Laravel 9 и берёт случайную запись из переданной коллекции для каждой связи. Это самый чистый способ не плодить лишние строки. Проверьте наличие метода в документации, если у вас ветка старше 9.x.
Способ 2: явно указать связь через for и has
$author = User::factory()->create();
// все посты привязываются к одному автору
$author->posts()->saveMany(
Post::factory()->count(5)->make()
);
// или обратный порядок — автор с постами одним вызовом
User::factory()
->has(Post::factory()->count(5))
->create();Разница в направлении: has идёт от родителя к детям, for — от ребёнка к родителю. Выбирайте по тому, что создаёте первым.
Состояния вместо копипасты
Когда для теста нужен «опубликованный пост» и «черновик», не дублируйте фабрику. Опишите состояние через метод state():
public function published(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'published',
'published_at' => now(),
]);
}
// использование
Post::factory()->published()->create();Состояния комбинируются: Post::factory()->published()->featured()->create(). Это снимает соблазн держать пять почти одинаковых фабрик.
Что выбрать под задачу
| Задача | Инструмент | Метод создания |
|---|---|---|
| Справочник для прода (роли, статусы) | Сид, без фабрики | updateOrCreate |
| Демо-контент для стенда | Сид + фабрика | factory()->count()->recycle() |
| Данные под конкретный тест | Фабрика в тесте | factory()->state()->create() |
| Связанные модели без дублей | Фабрика | recycle или for/has |
Частые ошибки
- Демо-сид в списке DatabaseSeeder на проде. Разделите сиды:
DatabaseSeederвызывает только идемпотентные справочники, демо-данные — отдельным классом, который на проде не запускается. Указывайте нужный класс явно:php artisan db:seed --class=DemoSeeder. - fake() с одинаковым сидом рандома. Если тесты падают через раз из-за случайных совпадений email — используйте уникальный модификатор
fake()->unique()->safeEmail(). - Тяжёлые сиды в CI. Создание 10 000 записей через фабрику по одной вставке душит пайплайн. Для больших объёмов собирайте массив и вставляйте пачкой через
insert(), пожертвовав событиями модели. - RefreshDatabase не откатывает данные сида. Трейт
RefreshDatabaseмигрирует базу заново, но данные, созданные вsetUpтеста, живут в транзакции и откатываются автоматически — отдельно чистить не нужно.
Что делать дальше
Проверьте свой DatabaseSeeder: запустите php artisan migrate:fresh --seed дважды подряд. Если второй запуск падает на дублях — переведите справочные сиды на updateOrCreate. Затем найдите фабрики, где связанная модель создаётся через вложенный factory(), и оцените, не плодят ли они лишние строки на больших сидах. Инвалидация и обновление демо-стенда из свежего дампа прода — отдельная тема, здесь мы разбирали только генерацию данных.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Laravel Documentation — Database: Seeding, Laravel Documentation — Eloquent: Factories
Частые вопросы
А если у меня Laravel старше 9 и метода recycle нет?
Сид падает с ошибкой Duplicate entry при повторном запуске. Что делать?
Можно ли запускать демо-сиды на продакшене?
Нужно ли чистить данные фабрики после теста?
Фабрика создаёт 10 000 записей очень медленно в CI. Как ускорить?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.