zenstruck/foundry
Zenstruck Foundry supercharges Laravel/Symfony testing with fluent model factories, fixtures, and story-based data builders. Create, persist, and customize entities easily, manage relations, and write cleaner, faster tests with powerful helpers and states.
Installation Run:
composer require --dev zenstruck/foundry
Add to composer.json under require-dev to ensure it’s excluded from production.
Basic Setup Publish the config (optional but recommended for customization):
php artisan vendor:publish --provider="Zenstruck\Foundry\FoundryServiceProvider"
This generates config/foundry.php.
First Fixture
Create a fixture for a model (e.g., User):
php artisan make:foundry User
This generates database/foundry/UserFixture.php with a default state.
Create and Persist
Use the create() method in tests or Tinker:
use Zenstruck\Foundry\ModelFactory;
$user = UserFixture::new()->create();
// or persist to DB:
$user = UserFixture::new()->createPersisted();
State-Based Fixtures Define reusable states in the fixture class:
class UserFixture extends ModelFactory
{
protected function definition(): array
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->email,
];
}
public function admin(): static
{
return $this->state([
'role' => 'admin',
'is_active' => true,
]);
}
}
Usage:
$admin = UserFixture::new()->admin()->createPersisted();
Relationships
Use has() or hasMany() to define relationships:
class PostFixture extends ModelFactory
{
protected function definition(): array
{
return [
'title' => $this->faker->sentence,
'user_id' => UserFixture::new()->create()->id,
];
}
public function withAuthor(): static
{
return $this->has('author', UserFixture::new()->admin());
}
}
Collections Create multiple records at once:
$users = UserFixture::new()->many(5)->create();
// or persisted:
$users = UserFixture::new()->many(3)->createPersisted();
Customization via Callbacks
Use afterCreating() for post-creation logic:
public function withProfile(): static
{
return $this->afterCreating(function (User $user) {
ProfileFixture::new()->create(['user_id' => $user->id]);
});
}
Laravel Testing
Replace create() with createPersisted() in tests to ensure DB interactions:
public function test_user_creation()
{
$user = UserFixture::new()->createPersisted();
$this->assertDatabaseHas('users', ['email' => $user->email]);
}
Seeding
Use fixtures in DatabaseSeeder for consistent test data:
public function run()
{
UserFixture::new()->many(10)->createPersisted();
PostFixture::new()->many(5)->createPersisted();
}
Dynamic Data Pass custom attributes to override defaults:
$user = UserFixture::new()->create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
Mocking External Services
Use afterCreating() to mock API calls or queue jobs:
$this->afterCreating(function (User $user) {
Mockery::mock('overload', App\Services\Analytics::class)
->shouldReceive('track')
->once()
->with($user->id);
});
State Overrides
States are not additive—each call to state() or *() (e.g., admin()) replaces previous states. Chain them carefully:
// Wrong: 'role' will be 'user' (overridden)
UserFixture::new()->admin()->user()->create();
// Correct: Explicitly merge states
UserFixture::new()->state(['role' => 'admin'])->user()->create();
Persisted vs. In-Memory
create() returns an in-memory model (not persisted to DB).createPersisted() triggers events (e.g., created) and validates.refresh() to reload a persisted model from DB:
$user = UserFixture::new()->createPersisted();
$user->refresh()->load('profile'); // Reload with relations
Faker Conflicts If using multiple Faker locales, ensure consistency:
// Set locale globally in config/foundry.php
'faker_locale' => 'en_US',
Or override per fixture:
public function __construct()
{
$this->faker = Faker::create('fr_FR');
}
Circular Relationships
Avoid infinite loops with recursive has() calls. Use afterCreating() for complex setups:
// Bad: Circular dependency
class CategoryFixture extends ModelFactory {
public function withParent(): static {
return $this->has('parent', CategoryFixture::new());
}
}
// Good: Break cycles with callbacks
$category = CategoryFixture::new()->createPersisted();
$parent = CategoryFixture::new()->createPersisted();
$category->parent()->associate($parent);
Inspect States Dump the final state before creation:
$fixture = UserFixture::new()->admin()->withProfile();
dd($fixture->getState());
Check Events Listen for fixture events in tests:
Foundry::on('creating', function ($fixture) {
dump($fixture->getState());
});
Clear Cached Factories If changes to fixtures aren’t reflected, clear the cache:
php artisan cache:clear
php artisan config:clear
Custom Factory Classes
Extend ModelFactory for shared logic:
class BaseUserFixture extends ModelFactory
{
protected function commonDefinition(): array
{
return ['is_active' => true];
}
protected function definition(): array
{
return array_merge($this->commonDefinition(), [
'name' => $this->faker->name,
]);
}
}
Dynamic Attributes
Use afterInstantiated() to compute attributes:
public function withHashedPassword(): static
{
return $this->afterInstantiated(function (User $user) {
$user->password = bcrypt('password');
});
}
Database-Specific Logic
Override persist() for custom DB logic (e.g., UUIDs):
protected function persist(Model $model): Model
{
$model->id = Str::uuid()->toString();
$model->saveOrFail();
return $model;
}
Testing Helpers Create a trait for reusable test setups:
trait CreatesUsers
{
protected function createTestUser(): User
{
return UserFixture::new()->createPersisted();
}
}
How can I help you explore Laravel packages today?