| name | symfony:doctrine-fixtures-foundry |
| description | Create test data with Foundry factories; define states, sequences, and relationships for realistic fixtures |
Doctrine Fixtures with Foundry
Foundry provides modern, type-safe factories for creating test data.
Installation
composer require zenstruck/foundry --dev
composer require doctrine/doctrine-fixtures-bundle --dev
Creating Factories
bin/console make:factory User
<?php
namespace App\Tests\Factory;
use App\Entity\User;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
final class UserFactory extends PersistentProxyObjectFactory
{
public static function class(): string
{
return User::class;
}
protected function defaults(): array
{
return [
'email' => self::faker()->unique()->safeEmail(),
'password' => 'hashed_password',
'roles' => ['ROLE_USER'],
'createdAt' => \DateTimeImmutable::createFromMutable(
self::faker()->dateTimeBetween('-1 year')
),
];
}
public function admin(): self
{
return $this->with(['roles' => ['ROLE_ADMIN']]);
}
public function verified(): self
{
return $this->with(['verifiedAt' => new \DateTimeImmutable()]);
}
public function withPosts(int $count = 3): self
{
return $this->afterPersist(function (User $user) use ($count) {
PostFactory::createMany($count, ['author' => $user]);
});
}
}
Using Factories
In Tests
<?php
use App\Tests\Factory\UserFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class UserTest extends WebTestCase
{
use Factories;
use ResetDatabase;
public function testSomething(): void
{
$user = UserFactory::createOne();
$admin = UserFactory::createOne([
'email' => 'admin@example.com',
'roles' => ['ROLE_ADMIN'],
]);
$users = UserFactory::createMany(5);
$verifiedAdmin = UserFactory::new()
->()
->()
->();
= ::()
->()
->();
= ->();
= ->();
}
}
In Fixtures
<?php
namespace App\DataFixtures;
use App\Tests\Factory\UserFactory;
use App\Tests\Factory\PostFactory;
use App\Tests\Factory\TagFactory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$admin = UserFactory::createOne([
'email' => 'admin@example.com',
])->admin();
UserFactory::createMany(10, function () {
return [
=> ::((, )),
];
});
= ::([
[ => ],
[ => ],
[ => ],
[ => ],
]);
::(, function () ($) {
[
'' => $->((1, 3))->(),
];
});
}
}
Load fixtures:
bin/console doctrine:fixtures:load
Factory Features
Sequences
$users = UserFactory::createSequence([
['email' => 'user1@example.com'],
['email' => 'user2@example.com'],
['email' => 'user3@example.com'],
]);
$users = UserFactory::createSequence(
fn(int $i) => ['email' => "user{$i}@example.com"]
)->count(10);
Relationships
<?php
final class PostFactory extends PersistentProxyObjectFactory
{
protected function defaults(): array
{
return [
'title' => self::faker()->sentence(),
'content' => self::faker()->paragraphs(3, true),
'author' => UserFactory::new(),
'status' => PostStatus::DRAFT,
];
}
public function published(): self
{
return $this->with([
'status' => PostStatus::PUBLISHED,
'publishedAt' => new \DateTimeImmutable(),
]);
}
public function withTags( = []):
{
->(function (Post ) ($) {
($ $) {
$->($ ? $ : ::(['' => $])->());
}
});
}
}
Usage:
$user = UserFactory::createOne();
$post = PostFactory::createOne(['author' => $user]);
$post = PostFactory::createOne();
$post = PostFactory::new()
->published()
->withTags(['PHP', 'Symfony'])
->create();
afterPersist Hook
protected function initialize(): static
{
return $this->afterPersist(function (User $user): void {
ProfileFactory::createOne(['user' => $user]);
});
}
Lazy Values
protected function defaults(): array
{
return [
'slug' => fn() => self::faker()->slug(),
'author' => lazy(fn() => UserFactory::createOne()),
];
}
Test Database Reset
use Zenstruck\Foundry\Test\ResetDatabase;
class MyTest extends WebTestCase
{
use ResetDatabase;
}
Configuration:
zenstruck_foundry:
database_resetter:
enabled: true
strategy: schema
Factory Best Practices
- Minimal defaults: Only set required fields
- Use states: Create named states for common variations
- Relationships: Let factories create related entities by default
- Realistic data: Use Faker for realistic test data
- Don't over-factory: Simple data can be created inline
protected function defaults(): array
{
return [
'email' => self::faker()->unique()->safeEmail(),
'roles' => ['ROLE_USER'],
];
}
protected function defaults(): array
{
return [
'email' => 'test@test.com',
'firstName' => 'Test',
'lastName' => 'User',
'phone' => '123456789',
];
}