MidПрактика8 min

Сидирование (Seeding)

Создание сидеров, фабрики моделей, вызов сидеров, лучшие практики заполнения базы данных

Сидирование (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');
    }
}

Проверь себя

Метод callOnce() в сидере гарантирует, что:

Что делает метод recycle() в фабрике?

Как создать пользователя с 3 постами, у каждого из которых 5 комментариев, используя фабрики?

Что делает Sequence в фабрике?

В чём разница между factory()->create() и factory()->make()?