zenstruck/foundry
Expressive, autocompletable fixture factories for Symfony + Doctrine (ORM and/or MongoDB). Create random-but-valid entities on demand for fixtures and tests, with states, persistence helpers, and rich testing features via ZenstruckFoundryBundle.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require zenstruck/foundry --dev
For Symfony projects, also install the bundle:
composer require zenstruck/foundry-bundle --dev
Bootstrapping:
Add the FoundryServiceProvider to your config/app.php (Symfony) or run:
php artisan foundry:install
This generates a config/foundry.php with default settings.
First Factory:
Create a factory for a model (e.g., User) using the Artisan command:
php artisan make:factory UserFactory --model=User
This generates a factory class in database/factories/UserFactory.php:
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
final class UserFactory extends ModelFactory
{
protected function configure(): void
{
$this->sequence(['name' => 'User {n}']);
}
protected function modelBuilder(): ModelBuilder
{
return User::newModelInstance();
}
protected static function class(): string
{
return User::class;
}
}
First Usage: Use the factory in tests or fixtures:
$user = UserFactory::new()->create();
Or with custom attributes:
$user = UserFactory::new()->create(['name' => 'John Doe', 'email' => 'john@example.com']);
Replace hardcoded test data with dynamic factories:
public function test_user_can_login(): void
{
$user = UserFactory::new()->create(['password' => 'password123']);
$this->actingAs($user);
$response = $this->get('/dashboard');
$response->assertOk();
}
Define reusable factory methods for common states:
final class PostFactory extends ModelFactory
{
public function published(): static
{
return $this->state([
'published_at' => now(),
'status' => 'published',
]);
}
public function draft(): static
{
return $this->state(['status' => 'draft']);
}
}
Usage:
$post = PostFactory::new()->published()->create();
Define relationships between factories:
final class CommentFactory extends ModelFactory
{
protected function modelBuilder(): ModelBuilder
{
return Comment::newModelInstance(['post' => PostFactory::random()]);
}
}
Or use has()/hasMany():
final class PostFactory extends ModelFactory
{
public function withComments(int $count = 3): static
{
return $this->has(CommentFactory::new(), $count);
}
}
Usage:
$post = PostFactory::new()->withComments(5)->create();
Override states dynamically:
$post = PostFactory::new()
->state(function (array $attributes, PostFactory $factory) {
return ['slug' => Str::slug($attributes['title'])];
})
->create();
Use Foundry::cleanup() to reset the database after each test:
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
#[ResetDatabase]
class UserTest extends TestCase
{
use Factories;
public function test_user_creation(): void
{
$user = UserFactory::new()->create();
$this->assertDatabaseHas('users', ['id' => $user->id]);
}
}
Leverage FoundryTestCase for browser tests:
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\FoundryTestCase;
class PostTest extends FoundryTestCase
{
use Factories;
public function test_post_creation(): void
{
$post = PostFactory::new()->create();
$this->get('/posts/' . $post->slug)
->assertOk();
}
}
Load factories as fixtures:
# config/packages/doctrine.yaml
doctrine:
fixtures:
# ...
zenstruck_foundry:
factories:
- App\Database\Factories\UserFactory
- App\Database\Factories\PostFactory
Run fixtures:
php bin/console doctrine:fixtures:load --group=zenstruck_foundry
Override Faker providers globally:
// config/foundry.php
'faker' => [
'providers' => [
Faker\Provider\Lorem::class,
App\Faker\CustomProvider::class,
],
],
Or per factory:
final class UserFactory extends ModelFactory
{
protected function configure(): void
{
$this->faker->addProvider(new CustomProvider($this->faker));
}
}
Enable auto-refresh for Doctrine entities (no manual refresh() calls):
// config/foundry.php
'orm' => [
'auto_refresh' => true,
],
Use #[AsFoundryHook] for custom logic:
#[AsFoundryHook('PostFactory::afterInstantiate')]
public function logPostCreation(Post $post): void
{
\Log::info('Post created: ' . $post->title);
}
Dispatch events:
$post = PostFactory::new()
->afterInstantiate(function (Post $post) {
event(new PostCreated($post));
})
->create();
withoutDoctrineEvents()prePersist, preUpdate) may interfere with factory creation.withoutDoctrineEvents() to suppress them:
$user = UserFactory::new()
->withoutDoctrineEvents()
->create();
withoutDoctrineEvents() inside flush_after()—it throws an exception in v2.10+.final class UserFactory extends ModelFactory
{
public function __construct()
{
$this->autoRefresh = false;
}
}
refresh() manually for complex relationships:
$post = PostFactory::new()->create();
$this->entityManager->refresh($post);
permutations():
#[DataProvider('userProvider')]
public function test_user_permutations(User $user): void
{
// ...
}
public static function userProvider(): array
{
return UserFactory::new()
->permutations([
'role' => ['admin', 'user'],
'status' => ['active', 'suspended'],
])
->count(2)
->randomize()
->provide();
}
$user = UserFactory::new()
->skipFakerSeed()
->create();
.env:
FOUNDRY_FAKER_SEED=1234
ResetDatabase trait with migrate mode for faster resets:
#[ResetDatabase(mode: 'migrate')]
class UserTest extends TestCase
{
// ...
}
schema mode in CI—it’s slow. Use migrate or in-memory instead.$user = UserFactory::new()->create(['email' => 'temp@example.com']);
UserFactory::delete($user);
Foundry::cleanup() to purge all ghosts:
Foundry::clean
How can I help you explore Laravel packages today?