Laravel Data Seeding and Model Factories
Seeder and Factory are Laravel's "data generators"—with just one command, you can generate thousands of realistic test data records, instantly transforming your development environment into a real-world scenario.
1. What You'll Learn
- Model Factory Definition: Faker Data Generation and State Modification
- Seeder code and call chain: $this->call()
- Related Data Factory: When a user is created, an order and a product are created at the same time
- Multi-tenant data isolation: TenantSeeder populates data on a per-tenant basis
- Workflow for
php artisan db:seedandmigrate:fresh --seed
2. A True Story from a Tester
(1) Pain Point: Manually generating data is even slower than writing code
Every time Alice tests ShopMetrics, she has to manually create tenants, add stores, enter products, and place orders—it takes 30 minutes to generate 10 test records. Bob has it even worse; he needed to test the pagination feature, but after manually entering 100 records, his browser crashed and he lost all the data. Charlie tried writing an SQL script to populate the data, but the relationships were too complex (tenant → store → product → order → order line). He wrote 500 lines of SQL and still missed a foreign key.
(2) The Factory + Seeder Approach
Laravel Factories use Faker to automatically generate realistic data; Seeders orchestrate the population order, and relating data is done with just one line of code.
// Factory generates realistic data
$shop = Shop::factory()->create();
$shop->products()->createMany(
Product::factory()->count(10)->make()->toArray()
);
// 1 shop + 10 products in 2 lines
(3) Revenue
After using Factory, Alice generated 1,000 test records (including all relationships) in 30 seconds, and testing pagination took just migrate:fresh --seed.
3. Model Factory
(1) Create a Factory
php artisan make:factory ShopFactory
# Or with model
php artisan make:factory ShopFactory --model=Shop
(2) Defining a Factory
// database/factories/ShopFactory.php
class ShopFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'description' => fake()->paragraph(),
'status' => fake()->randomElement(['active', 'suspended', 'closed']),
'revenue' => fake()->randomFloat(2, 100, 100000),
];
}
}
(3) Faker's Common Strategies
| Method | Sample Output | Description |
|---|---|---|
name() |
"Alice Johnson" | Name |
email() |
"alice@example.com" | |
company() |
"Acme Corp" | Company Name |
slug() |
"acme-corp" | URL slug |
paragraph() |
"Lorem ipsum..." | Paragraph text |
randomFloat(2, 0, 100) |
45.67 | Decimal |
numberBetween(1, 100) |
42 | Integer range |
randomElement([...]) |
Select a random element from an array | Enumerate values |
dateTimeThisYear() |
"June 15, 2024" | Date |
imageUrl() |
"https://via.placeholder.com/640x480" | Image URL |
unique() |
Ensure Uniqueness | Modifier |
(1) ▶ Example: ShopMetrics Core Factory Definition
// database/factories/TenantFactory.php
class TenantFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'domain' => fake()->unique()->domainName(),
'status' => 'active',
];
}
}
// database/factories/ProductFactory.php
class ProductFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->words(3, true),
'sku' => strtoupper(fake()->unique()->lexify('???-????')),
'price' => fake()->randomFloat(2, 9.99, 999.99),
'stock' => fake()->numberBetween(0, 500),
'is_active' => true,
];
}
}
// database/factories/OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => fake()->unique()->numerify('ORD-########'),
'subtotal' => fake()->randomFloat(2, 10, 5000),
'discount' => 0,
'total' => fake()->randomFloat(2, 10, 5000),
'status' => fake()->randomElement(['pending', 'processing', 'completed', 'cancelled']),
];
}
}
Output:
// Execution Successful
4. Factory State Modifiers
(1) Defining States
// database/factories/ShopFactory.php
class ShopFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->company(),
'slug' => fake()->unique()->slug(),
'status' => 'active',
'revenue' => fake()->randomFloat(2, 100, 50000),
];
}
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'suspended',
'revenue' => 0,
]);
}
public function highRevenue(): static
{
return $this->state(fn (array $attributes) => [
'revenue' => fake()->randomFloat(2, 50000, 500000),
]);
}
}
(2) Usage Status
// Active shop (default)
$shop = Shop::factory()->create();
// Suspended shop
$shop = Shop::factory()->suspended()->create();
// High revenue shop
$shop = Shop::factory()->highRevenue()->create();
// Combine states
$shop = Shop::factory()->highRevenue()->suspended()->create();
| Usage | Description |
|---|---|
factory()->create() |
Default state, stored in the DB |
factory()->make() |
Default state, not stored in the DB |
factory()->suspended()->create() |
Using the "suspended" status |
factory()->count(10)->create() |
Created 10 entries |
factory()->for(Tenant::factory()) |
Associate with Parent Model |
(1) ▶ Example: ShopMetrics Factory Status Combinations
// database/factories/OrderFactory.php
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'order_number' => fake()->unique()->numerify('ORD-########'),
'subtotal' => fake()->randomFloat(2, 10, 5000),
'discount' => 0,
'total' => fake()->randomFloat(2, 10, 5000),
'status' => 'pending',
];
}
public function completed(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'completed',
]);
}
public function cancelled(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'cancelled',
'total' => 0,
'subtotal' => 0,
]);
}
public function highValue(): static
{
return $this->state(fn (array $attributes) => [
'subtotal' => fake()->randomFloat(2, 1000, 10000),
'total' => fake()->randomFloat(2, 1000, 10000),
]);
}
}
// Usage
$order = Order::factory()->completed()->highValue()->create();
Output:
// Execution Successful
5. Linked Data Factory
(1) Create a relationship
// Create shop with products
$shop = Shop::factory()
->has(Product::factory()->count(10), 'products')
->create();
// Or using magic method
$shop = Shop::factory()
->hasProducts(10)
->create();
// Create order for a specific shop
$order = Order::factory()
->for($shop)
->create();
// Create user with tenant
$user = User::factory()
->for(Tenant::factory())
->create();
| Method | Description | Example |
|---|---|---|
has() |
Create Sub-Association | has(Product::factory()->count(5)) |
hasProducts(5) |
Shorthand for magic methods | Equivalent to has(Product::factory()->count(5), 'products') |
for() |
Associated Parent Model | for($tenant) or for(Tenant::factory()) |
(1) ▶ Example: ShopMetrics Data Integration Factory
// Create a complete tenant ecosystem
$tenant = Tenant::factory()
->has(User::factory()->count(3))
->has(Shop::factory()->count(5)->hasProducts(10))
->create();
// Create an order with items for a shop
$shop = Shop::factory()->create();
$products = Product::factory()->count(5)->for($shop)->create();
$order = Order::factory()
->for($shop->tenant)
->for($shop)
->for(User::factory()->for($shop->tenant))
->has(OrderItem::factory()->count(3)->state([
'product_id' => $products->random()->id,
]), 'items')
->create();
Output:
// Execution Successful
6. Seeder Orchestration
(1) Create a Seeder
php artisan make:seeder TenantSeeder
php artisan make:seeder ProductSeeder
(2) Writing a Seeder
// database/seeders/TenantSeeder.php
class TenantSeeder extends Seeder
{
public function run(): void
{
Tenant::factory()
->count(10)
->has(User::factory()->count(3))
->has(Shop::factory()->count(5)->hasProducts(10))
->create();
}
}
(3) Call Chain
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PlanSeeder::class,
TenantSeeder::class,
ProductSeeder::class,
OrderSeeder::class,
]);
}
}
(1) ▶ Example: ShopMetrics Complete DatabaseSeeder
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// 1. Create plans
Plan::factory()->createMany([
['name' => 'Starter', 'slug' => 'starter', 'price' => 29, 'shop_limit' => 5],
['name' => 'Pro', 'slug' => 'pro', 'price' => 79, 'shop_limit' => 25],
['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null],
]);
// 2. Create tenants with full data
Tenant::factory()
->count(20)
->has(User::factory()->count(5))
->has(Shop::factory()->count(3)->hasProducts(15))
->create();
// 3. Create orders for each shop
Shop::all()->each(function ($shop) {
Order::factory()
->count(50)
->for($shop->tenant)
->for($shop)
->for($shop->tenant->users->random())
->create();
});
}
}
Output:
// Execution Successful
7. Multi-tenant Data Isolation Population
(1) Subtenant Allocation
// database/seeders/TenantIsolatedSeeder.php
class TenantIsolatedSeeder extends Seeder
{
public function run(): void
{
$tenants = Tenant::factory()->count(10)->create();
foreach ($tenants as $tenant) {
// Each tenant gets isolated data
$users = User::factory()->count(5)->for($tenant)->create();
$shops = Shop::factory()->count(3)->for($tenant)->create();
foreach ($shops as $shop) {
$products = Product::factory()->count(20)->for($shop)->create();
// Create orders with items
Order::factory()->count(30)->for($tenant)->for($shop)
->for($users->random())
->create()
->each(function ($order) use ($products) {
$orderItems = $products->random(rand(1, 5));
foreach ($orderItems as $product) {
$order->items()->create([
'product_id' => $product->id,
'quantity' => rand(1, 3),
'price' => $product->price,
]);
}
$order->update([
'subtotal' => $order->items->sum(fn ($i) => $i->price * $i->quantity),
'total' => $order->items->sum(fn ($i) => $i->price * $i->quantity),
]);
});
}
}
}
}
(2) Data Volume Planning
| Entity | Per Tenant | Total for 10 Tenants | Notes |
|---|---|---|---|
| Users | 5 | 50 | including 1 owner |
| Shops | 3 | 30 | All active |
| Products | 20/shop | 600 | Random active/inactive |
| Orders | 30/shop | 900 | Various statuses |
| OrderItems | 1-5 per order | ~2,700 | Random quantity |
(1) ▶ Example: Running a Populate Workflow
# Fresh migrate + seed (development)
php artisan migrate:fresh --seed
# Run specific seeder
php artisan db:seed --class=TenantSeeder
# Run with custom amount
php artisan db:seed --class=OrderSeeder
# Production — never use migrate:fresh!
php artisan migrate --force
php artisan db:seed --force
Output:
# Command executed successfully
8. Comprehensive Example: Complete Data Population for ShopMetrics
// ============================================
// Comprehensive: ShopMetrics Complete Seeder
// Covers: factories, states, relationships, tenant isolation
// ============================================
// database/seeders/ShopMetricsSeeder.php
class ShopMetricsSeeder extends Seeder
{
public function run(): void
{
// Step 1: Plans
$starter = Plan::create(['name' => 'Starter', 'slug' => 'starter', 'price' => 29, 'shop_limit' => 5, 'features' => ['basic_analytics']]);
$pro = Plan::create(['name' => 'Pro', 'slug' => 'pro', 'price' => 79, 'shop_limit' => 25, 'features' => ['advanced_analytics', 'api_access']]);
$enterprise = Plan::create(['name' => 'Enterprise', 'slug' => 'enterprise', 'price' => 199, 'shop_limit' => null, 'features' => ['custom_analytics', 'api_access', 'sso']]);
// Step 2: Create super admin
User::factory()->create(['email' => 'admin@shopmetrics.io', 'role' => 'super_admin']);
// Step 3: Create tenants with isolated data
Tenant::factory()->count(20)->create()->each(function ($tenant) use ($starter, $pro, $enterprise) {
// Assign random plan
$plan = fake()->randomElement([$starter, $pro, $enterprise]);
Subscription::create(['tenant_id' => $tenant->id, 'plan_id' => $plan->id, 'status' => 'active']);
// Create tenant owner
$owner = User::factory()->for($tenant)->create(['role' => 'tenant_owner']);
// Create analysts
$analysts = User::factory()->count(3)->for($tenant)->create(['role' => 'analyst']);
// Create shops with products
$shopCount = min(fake()->numberBetween(2, 5), $plan->shop_limit ?? 99);
Shop::factory()->count($shopCount)->for($tenant)->create()->each(function ($shop) use ($tenant, $owner, $analysts) {
// Products per shop
Product::factory()->count(fake()->numberBetween(10, 30))->for($shop)->create();
// Orders per shop
Order::factory()->count(fake()->numberBetween(20, 80))
->for($tenant)
->for($shop)
->for(fake()->randomElement(array_merge([$owner], $analysts->all())))
->create()
->each(function ($order) use ($shop) {
$products = $shop->products()->inRandomOrder()->take(rand(1, 5))->get();
foreach ($products as $product) {
$qty = rand(1, 3);
$order->items()->create([
'product_id' => $product->id,
'quantity' => $qty,
'price' => $product->price,
]);
}
$subtotal = $order->items->sum(fn ($i) => $i->price * $i->quantity);
$order->update(['subtotal' => $subtotal, 'total' => $subtotal]);
});
});
});
}
}
❓ FAQ
db:seed command.make() and create()?make() creates a model instance but does not save it to the database; it is used in scenarios where persistence is not required. create() creates the model instance and saves it to the database; it is used in scenarios where an ID and join queries are required.fake('zh_CN')->name(), or use fake()->locale('zh_CN') in Factory. However, we recommend keeping the data in English for internationalization purposes.DB::table('products')->insert($data). Disable model events: Product::withoutEvents(fn () => Product::factory()->count(1000)->create()).plans table); do not use Factory to generate dummy data. Be sure to verify the command when adding the --force flag.$shop = Shop::factory()->create(). Laravel automatically rolls back the database transaction after each test, so tests do not affect one another.📖 Summary
- Factory defines data generation rules; Faker provides realistic random data
- State modification (state) defines different data scenarios for the same model
- Use
has()andfor()to create associated data for associated factories; thehasProducts()magic method simplifies the syntax - The Seeder organizes the population order, and the DatabaseSeeder calls its child Seeders
- For multi-tenant data, populate the data by tenant and ensure that the
tenant_idis correctly associated. - Use
migrate:fresh --seedin development; use onlydb:seed --forcein production
📝 Exercises
-
Basic Exercise (⭐): Create a ShopFactory and a ProductFactory for ShopMetrics, use Tinker to generate 3 stores with 5 products each, and verify that the associated data is correct.
-
Advanced Exercise (⭐⭐): Write a
TenantIsolatedSeederthat generates 5 tenants, each containing 2 stores, 10 products, and 20 orders, using factory association methods (has/for) to create the data. -
Challenge (⭐⭐⭐): Implement a complete data seeding solution that takes state into account—add three states (active, suspended, and highRevenue) to
ShopFactory, and three states (completed, canceled, and refunded) toOrderFactory, and distribute them proportionally in the Seeder (80% completed, 15% canceled, 5% refunded) .



