Сидеры и фабрики в Laravel: живой пример на каталоге товаров
Как наполнить базу тестовыми данными в Laravel 11: фабрики со связями, состояния, sequence и когда сидер тормозит на десятках тысяч записей.

Вам нужно 50 категорий, 5000 товаров с картинками и отзывами, чтобы прогнать список с пагинацией и проверить фильтры. Руками это набивать нельзя. В Laravel для этого есть две связанные вещи: фабрики (Illuminate\Database\Eloquent\Factories\Factory) описывают, как выглядит одна фейковая запись, а сидеры (сиды) запускают их массово и раскладывают по таблицам. Разберём на каталоге товаров: категории, товары, отзывы — со связями, состояниями и без просадки на больших объёмах. Всё для Laravel 11.x и PHP 8.2+.
Задача и очевидное решение, которое подведёт
Есть три модели: Category, Product, Review. Товар принадлежит категории, у товара много отзывов. Первое, что приходит в голову — накидать в сидер циклы с DB::table()->insert() и захардкоженными значениями. Это работает ровно до момента, когда вам понадобится тот же набор данных в тесте, но с другим статусом товара или пустой категорией. Придётся копировать код.
Фабрика решает это иначе: она описывает форму записи один раз, а вы вызываете её с нужными вариациями. Один и тот же ProductFactory обслуживает и сид для локальной разработки, и десяток тестов с разными условиями.
Сидер отвечает на вопрос «сколько и в каком порядке», фабрика — на вопрос «как выглядит одна запись». Смешивать эти обязанности — верный способ получить сидер, который невозможно переиспользовать в тестах.
Фабрика: описываем одну запись
Фабрику генерируют командой php artisan make:factory ProductFactory. Ключевой метод — definition(), он возвращает массив атрибутов для одной записи. Значения берём у генератора fake() (библиотека FakerPHP, ставится вместе с Laravel в dev-зависимостях).
<?php
namespace Database\Factories;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class ProductFactory extends Factory
{
public function definition(): array
{
$name = fake()->words(3, true);
return [
// Связь: если категорию не передали при вызове, создаём новую
'category_id' => Category::factory(),
'name' => $name,
'slug' => Str::slug($name) . '-' . fake()->unique()->numberBetween(1, 100000),
'price' => fake()->numberBetween(100, 50000),
'in_stock' => fake()->boolean(80), // 80% товаров в наличии
'published' => true,
];
}
}
Важный момент про связь: Category::factory() внутри definition() создаёт новую категорию для каждого товара, только если вы не передадите готовую при вызове. Ниже покажу, как передать одну категорию на пачку товаров и не наплодить их лишнего.
Состояния: варианты одной сущности
Состояние (state) — это именованное отклонение от базовой формы. Товар не в наличии, снятый с публикации, дорогой — всё это состояния, а не отдельные фабрики.
public function outOfStock(): static
{
return $this->state(fn (array $attributes) => [
'in_stock' => false,
]);
}
public function draft(): static
{
return $this->state(fn (array $attributes) => [
'published' => false,
]);
}
Теперь в тесте читается как фраза: Product::factory()->outOfStock()->draft()->create(). Состояния комбинируются, порядок вызова значения не имеет.
Сидер: сколько и в каком порядке
Сидер вызывает фабрики и связывает модели между собой. Метод run() в классе сидера — точка входа. Здесь важно правильно построить связи, чтобы не получить 5000 товаров, каждый в своей уникальной категории.
<?php
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Product;
use App\Models\Review;
use Illuminate\Database\Seeder;
class CatalogSeeder extends Seeder
{
public function run(): void
{
// 10 категорий, к каждой — от 5 до 20 товаров,
// у каждого товара — от 0 до 5 отзывов
Category::factory(10)
->has(
Product::factory()
->count(fake()->numberBetween(5, 20))
->has(Review::factory()->count(fake()->numberBetween(0, 5)))
)
->create();
}
}
Метод has() создаёт дочерние записи и сам проставляет внешний ключ на родителя — category_id у товаров подставится автоматически, ту самую категорию из definition() Laravel не будет плодить. Обратная сторона — for(): Product::factory()->for($category), когда вы идёте от товара к готовой категории.
Чтобы CatalogSeeder запускался, зарегистрируйте его в DatabaseSeeder:
public function run(): void
{
$this->call([
CatalogSeeder::class,
]);
}
Запуск: php artisan db:seed или сразу с пересборкой базы — php artisan migrate:fresh --seed. После этого в таблице categories будет 10 строк, а в products — от 50 до 200.
Sequence: контролируемые вариации вместо рандома
Иногда нужен не случайный разброс, а точное распределение: ровно половина товаров опубликована, половина — черновики. Для этого есть sequence() — он циклически подставляет значения записям по очереди.
Product::factory()
->count(100)
->sequence(
['published' => true],
['published' => false],
)
->create();
// первая запись published=true, вторая false, третья true и так далее
Это удобнее fake()->boolean(), когда в тесте нужно точно знать, сколько записей в каждом состоянии, а не «примерно 80 процентов».
Где это ломается под объёмом
Наивный сидер на десятках тысяч записей упирается в то, что Eloquent для каждой модели через create() делает отдельный INSERT и прогоняет события модели. 50 000 товаров через фабрику — это 50 000 запросов плюс срабатывание наблюдателей и событий. На локальной SQLite это минуты.
- Отключайте события модели на время сидинга. Обёртка
Model::withoutEvents(fn () => ...)не даст сработать обзёрверам, которые в продакшене шлют письма или пишут в поисковый индекс — при массовой заливке это не нужно и опасно. - Не полагайтесь на
fake()->unique()на больших объёмах. Он держит уже выданные значения в памяти и на десятках тысяч уникальных вызовов замедляется, а при исчерпании диапазона бросаетOverflowException. Для слагов и артикулов лучше строить значение из индекса записи. - Массовую вставку без связей делайте через
insert(). Когда данные не нужны как Eloquent-модели (например, справочник),Model::insert($массивМассивов)одним запросом кладёт пачку строк. Но он не заполняетcreated_at/updated_atи не проходит через фабрики — это компромисс между скоростью и удобством.
Сравнение способов наполнения
| Способ | Запросов на N записей | События модели | Когда брать |
|---|---|---|---|
factory()->create() в цикле | N и больше (со связями) | срабатывают | тесты, локальные данные до тысяч записей |
factory()->count(N)->create() | оптимизирован, но события идут | срабатывают | основной способ для сидов |
withoutEvents() + фабрика | как выше, без обзёрверов | отключены | массовый сидинг демо-данных |
Model::insert() | 1 (или пачками) | не срабатывают | десятки тысяч строк, связи не нужны |
Фабрики в тестах: ради чего всё затевалось
Тот же ProductFactory в тесте даёт ровно те данные, что нужны проверяемому сценарию, без сида всей базы. Метод make() собирает модель в памяти без записи в базу, create() — сохраняет.
public function test_список_показывает_только_опубликованные_товары(): void
{
Product::factory()->count(3)->create(); // видны
Product::factory()->draft()->count(2)->create(); // скрыты
$response = $this->get('/catalog');
$response->assertOk();
// на странице ровно 3 товара
}
Здесь виден смысл разделения: сидер вам в этом тесте не нужен вовсе, вы поднимаете минимальный набор через фабрику с конкретным состоянием.
Что делать дальше
Разнесите сиды по окружениям: тяжёлый CatalogSeeder с тысячами записей запускайте только локально и в CI, а в отдельный сидер вынесите справочники, которые нужны и в продакшене (категории, статусы). Проверьте, что события модели во время массового сидинга либо отключены, либо не делают ничего вредного. Если сид растёт до сотен тысяч записей и начинает мешать — измеряйте время запуска через --verbose и переносите справочники на insert(). Точный список методов фабрик для вашей версии сверяйте в разделе Database Testing официальной документации Laravel — часть из них (например, recycle() для переиспользования моделей в связях) появилась в свежих релизах.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Laravel: Database Testing / Eloquent Factories, Laravel: Seeding
Частые вопросы
У меня Laravel 9 или 10 — код подойдёт?
Сидер падает с ошибкой уникальности slug — что делать?
Фабрика создаёт лишние категории — на каждый товар свою. Почему?
Как заполнить связь многие-ко-многим (например, товары и теги)?
Сид работает минуты на большом объёме — как ускорить?
Стоит ли запускать демо-сидер на продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.