Фабрики позволяют генерировать тестовые данные для моделей 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]']);
}
}