Сидирование (seeding) -- это процесс заполнения базы данных тестовыми или начальными данными. Laravel предоставляет два основных инструмента: Seeders (классы для запуска) и Factories (шаблоны для генерации данных).
Создание сидеров
# Create a seeder
php artisan make:seeder UserSeeder
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
final class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Method 1: Direct creation
User::create([
'name' => 'Admin User',
'email' => '[email protected]',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
// Method 2: Using factory
User::factory()->count(50)->create();
// Method 3: Using factory with overrides
User::factory()->count(10)->create([
'is_admin' => true,
]);
}
}
DatabaseSeeder -- главный сидер
<?php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
final class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// Call seeders in a specific order (respect FK constraints)
$this->call([
RoleSeeder::class,
UserSeeder::class,
CategorySeeder::class,
ProductSeeder::class,
OrderSeeder::class,
]);
}
}
Фабрики моделей (Model Factories)
Фабрики -- это классы, определяющие шаблоны для генерации данных Eloquent моделей.
# Create a factory
php artisan make:factory PostFactory
# Create model with factory
php artisan make:model Post -f
Определение фабрики
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Category;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<\App\Models\Post>
*/
final class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'category_id' => Category::factory(),
'title' => fake()->sentence(),
'slug' => fake()->unique()->slug(),
'excerpt' => fake()->paragraph(),
'content' => fake()->paragraphs(5, asText: true),
'is_published' => fake()->boolean(80), // 80% chance true
'views_count' => fake()->numberBetween(0, 10000),
'published_at' => fake()->optional(0.8)->dateTimeBetween('-1 year'),
'metadata' => [
'reading_time' => fake()->numberBetween(1, 15),
'featured' => fake()->boolean(20),
],
];
}
/**
* Indicate that the post is published.
*/
public function published(): static
{
return $this->state(fn (array $attributes) => [
'is_published' => true,
'published_at' => fake()->dateTimeBetween('-6 months'),
]);
}
/**
* Indicate that the post is a draft.
*/
public function draft(): static
{
return $this->state(fn (array $attributes) => [
'is_published' => false,
'published_at' => null,
]);
}
/**
* Indicate that the post is featured.
*/
public function featured(): static
{
return $this->state(fn (array $attributes) => [
'metadata' => array_merge($attributes['metadata'] ?? [], [
'featured' => true,
]),
]);
}
}
Fake() -- Faker helper
// Common Faker methods
fake()->name(); // 'Dr. John Smith'
fake()->firstName(); // 'John'
fake()->lastName(); // 'Smith'
fake()->email(); // '[email protected]'
fake()->unique()->safeEmail(); // '[email protected]'
fake()->phoneNumber(); // '+1-555-123-4567'
fake()->address(); // '123 Main St, City, ST 12345'
fake()->city(); // 'New York'
fake()->country(); // 'United States'
fake()->sentence(); // 'Lorem ipsum dolor sit amet.'
fake()->paragraph(); // Multi-sentence string
fake()->text(200); // Random text, max 200 chars
fake()->realText(200); // More realistic text
fake()->numberBetween(1, 100); // Random integer
fake()->randomFloat(2, 0, 1000); // Random float with 2 decimals
fake()->boolean(70); // 70% true
fake()->randomElement(['a', 'b']); // Random array element
fake()->dateTimeBetween('-1 year', 'now');
fake()->date(); // '2024-03-15'
fake()->time(); // '14:30:00'
fake()->url(); // 'https://example.com'
fake()->imageUrl(640, 480); // Placeholder image URL
fake()->ipv4(); // '192.168.1.1'
fake()->uuid(); // UUID v4
fake()->hexColor(); // '#ff5733'
// Conditional data
fake()->optional(0.9)->sentence(); // 90% chance of value, 10% null
// Unique within factory run
fake()->unique()->email();
// Locale-specific
fake('ru_RU')->name(); // Russian name
Использование фабрик
use App\Models\Post;
use App\Models\User;
// Create a single instance (persisted to DB)
$post = Post::factory()->create();
// Create without persisting (in memory only)
$post = Post::factory()->make();
// Create multiple instances
$posts = Post::factory()->count(10)->create();
// With state
$post = Post::factory()->published()->create();
$post = Post::factory()->draft()->featured()->create();
// With attribute overrides
$post = Post::factory()->create([
'title' => 'My Custom Title',
'user_id' => $specificUser->id,
]);
// With relationships
$user = User::factory()
->has(Post::factory()->count(3)->published())
->create();
// Shorthand for has()
$user = User::factory()
->hasPosts(3, ['is_published' => true])
->create();
Фабрики с отношениями
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
final class UserFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => 'password', // 'hashed' cast handles hashing
'remember_token' => \Illuminate\Support\Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
/**
* Indicate that the user is an admin.
*/
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
]);
}
}
// BelongsTo - parent is created automatically
$post = Post::factory()->create();
// User is auto-created via User::factory() in definition
// Has Many
$user = User::factory()
->has(Post::factory()->count(5))
->create();
// Belongs To Many (many-to-many)
$user = User::factory()
->hasAttached(
Role::factory()->count(3),
['assigned_at' => now()] // Pivot data
)
->create();
// Has Many Through
$user = User::factory()
->has(
Post::factory()
->count(3)
->has(Comment::factory()->count(5))
)
->create();
// For existing parent
$user = User::factory()->create();
$posts = Post::factory()
->count(5)
->for($user)
->create();
// Shorthand for for()
$posts = Post::factory()
->count(5)
->forUser(['name' => 'John'])
->create();
Sequences
use Illuminate\Database\Eloquent\Factories\Sequence;
// Cycle through values
$users = User::factory()
->count(10)
->sequence(
['is_admin' => true],
['is_admin' => false],
)
->create();
// Result: admin, not admin, admin, not admin, ...
// Using Sequence class with index
$users = User::factory()
->count(5)
->sequence(fn (Sequence $sequence) => [
'name' => 'User ' . ($sequence->index + 1),
])
->create();
// Result: User 1, User 2, User 3, User 4, User 5
afterCreating / afterMaking callbacks
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Order;
use App\Models\OrderItem;
use Illuminate\Database\Eloquent\Factories\Factory;
final class OrderFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => \App\Models\User::factory(),
'status' => 'pending',
'total' => 0,
];
}
/**
* Configure the model factory.
*/
public function configure(): static
{
return $this->afterCreating(function (Order $order) {
// Create order items after order is created
$items = OrderItem::factory()
->count(fake()->numberBetween(1, 5))
->for($order)
->create();
// Update order total
$order->update([
'total' => $items->sum('price'),
]);
});
}
}
Вызов сидеров
# Run all seeders (DatabaseSeeder)
php artisan db:seed
# Run a specific seeder
php artisan db:seed --class=UserSeeder
# With migration
php artisan migrate --seed
php artisan migrate:fresh --seed
# Force in production
php artisan db:seed --force
Вызов из кода
final class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Call a single seeder
$this->call(UserSeeder::class);
// Call multiple seeders
$this->call([
RoleSeeder::class,
UserSeeder::class,
]);
// Call with silence (no output)
$this->callSilent(UserSeeder::class);
// Call once (skip if already ran)
$this->callOnce([
RoleSeeder::class,
PermissionSeeder::class,
]);
}
}
Практический пример: полная система сидирования
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Comment;
use App\Models\Post;
use App\Models\Role;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Database\Seeder;
final class DatabaseSeeder extends Seeder
{
public function run(): void
{
// 1. Roles (static data)
$this->call(RoleSeeder::class);
// 2. Categories (static data)
$categories = Category::factory()
->count(10)
->create();
// 3. Tags
$tags = Tag::factory()->count(20)->create();
// 4. Admin user
$admin = User::factory()->admin()->create([
'name' => 'Admin',
'email' => '[email protected]',
]);
$admin->roles()->attach(Role::where('name', 'admin')->first());
// 5. Regular users with posts
User::factory()
->count(20)
->has(
Post::factory()
->count(5)
->published()
->hasAttached($tags->random(3))
->has(Comment::factory()->count(10))
->state(fn () => [
'category_id' => $categories->random()->id,
])
)
->create()
->each(function (User $user) {
$user->roles()->attach(
Role::where('name', 'user')->first()
);
});
// 6. Additional draft posts
Post::factory()
->count(15)
->draft()
->recycle(User::all()) // Reuse existing users
->recycle($categories) // Reuse existing categories
->create();
$this->command->info('Database seeded successfully!');
$this->command->info("Users: " . User::count());
$this->command->info("Posts: " . Post::count());
$this->command->info("Comments: " . Comment::count());
}
}
recycle() -- повторное использование моделей
// Without recycle: each post creates a NEW user
Post::factory()->count(100)->create(); // 100 new users created
// With recycle: reuse existing users
$users = User::factory()->count(10)->create();
Post::factory()
->count(100)
->recycle($users) // Randomly assigns from these 10 users
->create();
Лучшие практики
Идемпотентность
final class RoleSeeder extends Seeder
{
public function run(): void
{
// Idempotent: safe to run multiple times
$roles = ['admin', 'editor', 'user', 'moderator'];
foreach ($roles as $role) {
Role::firstOrCreate(
['name' => $role],
['description' => ucfirst($role) . ' role']
);
}
}
}
Разделение тестовых и продакшен данных
final class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Always run (required data)
$this->call([
RoleSeeder::class,
PermissionSeeder::class,
SettingsSeeder::class,
]);
// Only in non-production
if (! app()->isProduction()) {
$this->call([
TestUserSeeder::class,
TestDataSeeder::class,
]);
}
}
}
Использование транзакций
use Illuminate\Support\Facades\DB;
final class LargeDataSeeder extends Seeder
{
public function run(): void
{
DB::transaction(function () {
// All or nothing
User::factory()->count(1000)->create();
Post::factory()->count(5000)->create();
});
}
}
Тестирование с фабриками
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Tests\TestCase;
final class PostTest extends TestCase
{
public function test_user_can_view_own_posts(): void
{
$user = User::factory()
->has(Post::factory()->count(3)->published())
->create();
$response = $this->actingAs($user)
->getJson('/api/my-posts');
$response->assertOk()
->assertJsonCount(3, 'data');
}
public function test_draft_posts_not_visible_to_others(): void
{
$author = User::factory()->create();
Post::factory()->draft()->for($author)->create();
$viewer = User::factory()->create();
$response = $this->actingAs($viewer)
->getJson('/api/posts');
$response->assertOk()
->assertJsonCount(0, 'data');
}
}