MidПрактика8 min

Model Factories

Определение фабрик, states, sequences, отношения в фабриках, afterCreating/afterMaking, factory callbacks

Фабрики позволяют генерировать тестовые данные для моделей Eloquent. Они незаменимы для тестирования и seeding базы данных.

Определение фабрик

Базовая фабрика

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;

/**
 * @extends Factory<User>
 */
class UserFactory extends Factory
{
    // Model associated with this factory
    protected $model = User::class;
    // Note: if factory follows naming convention
    // (UserFactory -> User model), $model is optional

    /**
     * Define the model's default state.
     */
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => Hash::make('password'),
            'role' => UserRole::User,
            'bio' => fake()->paragraph(),
            'avatar_url' => fake()->imageUrl(200, 200),
            'is_active' => true,
            'remember_token' => \Illuminate\Support\Str::random(10),
        ];
    }
}

Конвенция: Фабрика UserFactory автоматически ассоциируется с моделью User. Свойство $model нужно указывать только если имя фабрики не соответствует конвенции.

Использование фабрик

<?php

use App\Models\User;

// Create single model (persisted to database)
$user = User::factory()->create();

// Create with custom attributes
$user = User::factory()->create([
    'name' => 'John Doe',
    'email' => '[email protected]',
]);

// Create multiple models
$users = User::factory()->count(5)->create();
// Or shorthand:
$users = User::factory(5)->create();

// Make (create in memory, NOT saved to database)
$user = User::factory()->make();
$users = User::factory(3)->make();

// Make with custom attributes
$user = User::factory()->make([
    'name' => 'Test User',
]);

Подвох на экзамене: create() сохраняет модель в базу данных. make() только создаёт экземпляр в памяти без сохранения. В тестах используйте make() когда БД не нужна.

States

States позволяют определять дискретные модификации, которые можно применять к фабрике:

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'password' => 'password',
            'role' => UserRole::User,
            'is_active' => true,
            'email_verified_at' => now(),
        ];
    }

    // State: admin user
    public function admin(): static
    {
        return $this->state(fn (array $attributes) => [
            'role' => UserRole::Admin,
            'is_admin' => true,
        ]);
    }

    // State: unverified email
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }

    // State: suspended user
    public function suspended(): static
    {
        return $this->state(fn (array $attributes) => [
            'is_active' => false,
            'suspended_at' => now(),
            'suspension_reason' => fake()->sentence(),
        ]);
    }

    // State: premium user with expiration
    public function premium(int $months = 12): static
    {
        return $this->state(fn (array $attributes) => [
            'is_premium' => true,
            'premium_until' => now()->addMonths($months),
        ]);
    }

    // State: user with specific creation date
    public function createdDaysAgo(int $days): static
    {
        return $this->state(fn (array $attributes) => [
            'created_at' => now()->subDays($days),
            'updated_at' => now()->subDays($days),
        ]);
    }
}

Использование:

<?php

// Single state
$admin = User::factory()->admin()->create();

// Multiple states chained
$suspendedAdmin = User::factory()
    ->admin()
    ->suspended()
    ->create();

// State with parameters
$premiumUser = User::factory()->premium(6)->create();

// Combined with count
$unverifiedUsers = User::factory(10)->unverified()->create();

// Combined with custom attributes
$user = User::factory()
    ->admin()
    ->premium()
    ->create(['name' => 'Super Admin']);

Подвох на экзамене: States возвращают static (новый экземпляр фабрики), что позволяет цепочечные вызовы. Порядок применения states имеет значение -- последующие states могут перезаписать предыдущие.

Sequences

Sequences позволяют чередовать значения для массового создания:

<?php

use Illuminate\Database\Eloquent\Factories\Sequence;

// Alternate between values
$users = User::factory()
    ->count(6)
    ->sequence(
        ['role' => 'admin'],
        ['role' => 'editor'],
        ['role' => 'viewer'],
    )
    ->create();
// Results: admin, editor, viewer, admin, editor, viewer

// Sequence with callback
$users = User::factory()
    ->count(10)
    ->sequence(fn (Sequence $sequence) => [
        'name' => "User #{$sequence->index}",
        'sort_order' => $sequence->index * 10,
    ])
    ->create();

// Multiple sequences
$users = User::factory()
    ->count(4)
    ->sequence(
        ['is_active' => true],
        ['is_active' => false],
    )
    ->sequence(
        ['role' => 'admin'],
        ['role' => 'user'],
    )
    ->create();
// Result: active-admin, inactive-user, active-admin, inactive-user

Sequence с индексом

<?php

use Illuminate\Database\Eloquent\Factories\Sequence;

$posts = Post::factory()
    ->count(5)
    ->sequence(fn (Sequence $sequence) => [
        'title' => "Post #{$sequence->index}",
        'sort_order' => ($sequence->index + 1) * 100,
        'is_featured' => $sequence->index === 0, // Only first is featured
    ])
    ->create();

Relationships в фабриках

BelongsTo (автоматическое создание родителя)

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Post;
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),
            'published_at' => fake()->optional()->dateTimeBetween('-1 year'),

            // Automatically creates a User if not provided
            'user_id' => User::factory(),
        ];
    }

    // State with specific user type
    public function byAdmin(): static
    {
        return $this->state(fn () => [
            'user_id' => User::factory()->admin(),
        ]);
    }
}
<?php

// Creates post WITH new user automatically
$post = Post::factory()->create();
// $post->user exists!

// Create post for EXISTING user
$user = User::factory()->create();
$post = Post::factory()->create([
    'user_id' => $user->id,
]);

// Create post for specific user using for()
$post = Post::factory()
    ->for($user) // Sets user_id automatically
    ->create();

// for() with factory
$post = Post::factory()
    ->for(User::factory()->admin(), 'author') // Custom relationship name
    ->create();

HasMany (создание дочерних записей)

<?php

// Create user with 3 posts
$user = User::factory()
    ->has(Post::factory()->count(3))
    ->create();

// Shorthand (magic method)
$user = User::factory()
    ->hasPosts(3) // Magic: has + relationship name in StudlyCase
    ->create();

// With custom post attributes
$user = User::factory()
    ->has(
        Post::factory()
            ->count(3)
            ->state(['published_at' => now()])
    )
    ->create();

// Magic method with state
$user = User::factory()
    ->hasPosts(3, ['published_at' => now()])
    ->create();

// Nested: user -> posts -> comments
$user = User::factory()
    ->has(
        Post::factory()
            ->count(3)
            ->has(Comment::factory()->count(5))
    )
    ->create();

BelongsToMany (Many-to-Many)

<?php

// Create user with roles
$user = User::factory()
    ->hasAttached(
        Role::factory()->count(3),
        ['assigned_at' => now()], // Pivot attributes
    )
    ->create();

// Magic method
$user = User::factory()
    ->hasRoles(3) // Creates 3 roles and attaches them
    ->create();

// Attach existing models
$roles = Role::factory(3)->create();
$user = User::factory()
    ->hasAttached($roles, ['assigned_at' => now()])
    ->create();

// With pivot data per role
$user = User::factory()
    ->hasAttached(
        Role::factory()->count(2),
        fn () => ['assigned_at' => fake()->dateTimeThisYear()],
    )
    ->create();

Polymorphic relationships

<?php

// morphMany
$post = Post::factory()
    ->has(Comment::factory()->count(3), 'comments') // Specify relationship name
    ->create();

// morphToMany
$post = Post::factory()
    ->hasAttached(
        Tag::factory()->count(5),
        [],
        'tags', // Relationship name
    )
    ->create();

Recursive relationships

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;

class CategoryFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->word(),
            'parent_id' => null, // Root category by default
        ];
    }

    // State: subcategory
    public function child(): static
    {
        return $this->state(fn () => [
            'parent_id' => Category::factory(),
        ]);
    }

    // State: with children
    public function withChildren(int $count = 3): static
    {
        return $this->has(
            Category::factory()->count($count),
            'children'
        );
    }
}

// Usage
$rootCategory = Category::factory()->withChildren(5)->create();

Factory Callbacks

afterMaking и afterCreating

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'password' => 'password',
        ];
    }

    // Configure callbacks
    public function configure(): static
    {
        return $this
            ->afterMaking(function (User $user) {
                // Called after make() -- model NOT saved yet
                // Good for computed attributes
                $user->slug = \Illuminate\Support\Str::slug($user->name);
            })
            ->afterCreating(function (User $user) {
                // Called after create() -- model IS saved
                // Good for creating related data
                $user->profile()->create([
                    'bio' => fake()->paragraph(),
                    'website' => fake()->url(),
                ]);
            });
    }

    // Callbacks in states
    public function withDefaultTeam(): static
    {
        return $this->afterCreating(function (User $user) {
            $team = \App\Models\Team::factory()->create([
                'owner_id' => $user->id,
            ]);
            $user->update(['current_team_id' => $team->id]);
        });
    }
}

Подвох на экзамена: afterMaking() вызывается и для make(), и для create() (create включает make). afterCreating() вызывается ТОЛЬКО для create(). Порядок: definition -> afterMaking -> save -> afterCreating.

Множественные callbacks

<?php

public function configure(): static
{
    return $this
        ->afterCreating(function (User $user) {
            // First callback: create profile
            $user->profile()->create(['bio' => fake()->text()]);
        })
        ->afterCreating(function (User $user) {
            // Second callback: assign default role
            $user->roles()->attach(
                \App\Models\Role::firstOrCreate(['name' => 'user'])
            );
        });
}

Продвинутые паттерны

Factory с трейтом HasFactory

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;

    // HasFactory automatically resolves OrderFactory
    // Convention: App\Models\Order -> Database\Factories\OrderFactory
}

// If factory is in non-standard location:
class Order extends Model
{
    use HasFactory;

    protected static function newFactory(): \Custom\Namespace\OrderFactory
    {
        return \Custom\Namespace\OrderFactory::new();
    }
}

Recycle (переиспользование моделей)

<?php

// Without recycle: each post creates its own user
$posts = Post::factory(10)->create();
// 10 posts + 10 users created

// With recycle: all posts share the same user
$user = User::factory()->create();
$posts = Post::factory(10)
    ->recycle($user)
    ->create();
// 10 posts + 1 user (reused)

// Recycle collection
$users = User::factory(3)->create();
$posts = Post::factory(10)
    ->recycle($users) // Random user from collection for each post
    ->create();

// Recycle multiple types
$users = User::factory(3)->create();
$categories = Category::factory(5)->create();

$posts = Post::factory(20)
    ->recycle($users)
    ->recycle($categories)
    ->create();

Подвох на экзамене: recycle() -- важный метод в Laravel 11+ для оптимизации тестов. Без него каждый User::factory() в definition создаёт нового пользователя. С recycle() используются существующие модели.

Factory для сложных сценариев

<?php

// Complete e-commerce scenario
$user = User::factory()
    ->premium()
    ->has(
        Order::factory()
            ->count(5)
            ->has(
                OrderItem::factory()
                    ->count(3)
                    ->state(fn () => [
                        'product_id' => Product::factory()
                            ->has(ProductImage::factory()->count(2)),
                    ]),
                'items'
            )
            ->sequence(
                ['status' => 'completed'],
                ['status' => 'pending'],
                ['status' => 'processing'],
            ),
        'orders'
    )
    ->has(
        Address::factory()->count(2)->sequence(
            ['type' => 'shipping'],
            ['type' => 'billing'],
        ),
        'addresses'
    )
    ->create();

Тестирование с фабриками (Pest)

<?php

use App\Models\User;
use App\Models\Post;

test('user can create a post', function () {
    $user = User::factory()->create();
    $post = Post::factory()->for($user)->create();

    expect($post->user_id)->toBe($user->id)
        ->and($user->posts)->toHaveCount(1);
});

test('admin can see all posts', function () {
    $admin = User::factory()->admin()->create();
    Post::factory(10)->create();

    $this->actingAs($admin)
        ->getJson('/api/posts')
        ->assertOk()
        ->assertJsonCount(10, 'data');
});

test('premium user has access to features', function () {
    $user = User::factory()
        ->premium(months: 6)
        ->create();

    expect($user->is_premium)->toBeTrue()
        ->and($user->premium_until)->toBeGreaterThan(now());
});

Seeder с фабриками

<?php

declare(strict_types=1);

namespace Database\Seeders;

use App\Models\User;
use App\Models\Post;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Create admin
        $admin = User::factory()->admin()->create([
            'name' => 'Admin User',
            'email' => '[email protected]',
        ]);

        // Create regular users with posts
        User::factory(50)
            ->has(Post::factory()->count(rand(1, 10)))
            ->create();

        // Create specific test scenarios
        User::factory()
            ->suspended()
            ->createdDaysAgo(30)
            ->create(['email' => '[email protected]']);
    }
}

Проверь себя

Как создать пользователя с 3 постами используя магический метод фабрики?

В каком порядке вызываются callback-и фабрики?

Что произойдёт, если в definition фабрики PostFactory указать 'user_id' => User::factory() и вызвать Post::factory()->create()?

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

Чем отличается `User::factory()->create()` от `User::factory()->make()`?