Fix Password Hasher Injection in Zenstruck Foundry Tests
Fix password hasher injection in Zenstruck Foundry tests: register UserFactory as a service and fetch it from the Symfony test container to enable hasher DI.
Why is PasswordHasherInterface not injected into my Zenstruck Foundry factory in PHPUnit tests, causing passwords to remain unhashed?
I’m trying to use Zenstruck Foundry factories as services in my PHPUnit tests. I enabled the Foundry PHPUnit extension in phpunit.xml:
<phpunit>
<extensions>
<bootstrap class="Zenstruck\\Foundry\\PHPUnit\\FoundryExtension"/>
</extensions>
</phpunit>
My UserFactory injects PasswordHasherInterface and hashes the password in afterInstantiate:
<?php
namespace App\DataFixtures\Factory;
use App\Entity\User;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/**
* @extends PersistentObjectFactory<User>
*/
final class UserFactory extends PersistentObjectFactory
{
private static ?User $admin = null;
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct(
private readonly ?PasswordHasherInterface $passwordHasher = null
) {
parent::__construct();
}
#[\Override]
public static function class(): string
{
return User::class;
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
#[\Override]
protected function defaults(): array|callable
{
return [
'email' => self::faker()->unique()->email(),
'username' => self::faker()->userName(),
'password' => self::faker()->password(),
'color' => ColorFactory::randomOrCreate(),
'roles' => [User::ROLE_USER],
];
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
#[\Override]
protected function initialize(): static
{
return $this
->afterInstantiate(function(User $user): void {
if (!$this->passwordHasher instanceof PasswordHasherInterface) {
return;
}
$user->setPassword($this->passwordHasher->hash($user->getPassword()));
})
;
}
}
In my test I use a DataProvider that yields either null or a UserFactory instance, and inside the test I call:
if ($userFactory) {
$user = $userFactory->create();
dump($user->getPassword()); // not hashed
dump(UserFactory::createOne()->getPassword()); // also not hashed
// ...
}
What am I missing? Specifically:
- How do I ensure Foundry creates factories as services in the test environment so constructor dependencies (PasswordHasherInterface) are injected?
- Are there additional configuration steps for phpunit.xml, FoundryBundle, or the Symfony test container to enable factories-as-services?
- Is there a recommended way to obtain the factory from the service container in tests (instead of using static factory methods) so services are injected?
Any guidance, minimal configuration examples, or debugging tips to make factories-as-services work in PHPUnit tests would be appreciated.
Static calls like UserFactory::createOne() in Zenstruck Foundry skip the Symfony container entirely, so your password hasher (PasswordHasherInterface) stays null and passwords remain unhashed. The fix? Register your factory as a service for the test environment, then grab it from the container in PHPUnit tests instead of using static helpers. This ensures proper dependency injection works seamlessly.
Contents
- Why Static Methods Bypass Dependency Injection
- Register Zenstruck Foundry Factories as Services
- Fetching Factories from the Test Container
- PHPUnit.xml and Bundle Setup
- Data Providers and Common Pitfalls
- Debugging Checklist
- Sources
- Conclusion
Why Static Methods Bypass Dependency Injection
Ever wonder why your meticulously injected password hasher vanishes in tests? Zenstruck Foundry’s static helpers—::createOne(), ::new(), ::find()—instantiate factories directly. No Symfony container involved.
That means constructor arguments like PasswordHasherInterface default to null (or whatever fallback you set). Your afterInstantiate hook checks if (!$this->passwordHasher instanceof PasswordHasherInterface), spots the null, and bails. Boom—raw passwords everywhere.
The official ZenstruckFoundryBundle docs spell it out: “Static calls create the factory without the Symfony container, so constructor dependencies are not resolved.” Same issue pops up in community threads. It’s not a bug. Just how standalone factories roll.
But here’s the good news. Switch to container-managed factories, and DI kicks in. Your hasher gets wired up automatically.
Register Zenstruck Foundry Factories as Services
To make Zenstruck Foundry factories as services work, tell Symfony to treat them like any other service—but only in tests. Drop this into config/packages/zenstruck_foundry.yaml:
zenstruck_foundry:
when@test:
services:
App\DataFixtures\Factory\UserFactory: ~
The when@test keeps it test-only, avoiding prod bloat. The tilde (~) auto-wires and auto-configures everything. If you prefer explicit control (say, for multiple factories), spell it out:
zenstruck_foundry:
when@test:
services:
App\DataFixtures\Factory\UserFactory:
public: true
autowire: true
autoconfigure: true
This mirrors advice from the bundle documentation. Factories now live in the container, ready for injection. No more null hashers.
Quick test: Run bin/phpunit --testdox or bin/console debug:container UserFactory --env=test. See it listed? You’re golden.
Fetching Factories from the Test Container
Ditch the static calls. In your PHPUnit tests (extending KernelTestCase or using the Foundry extension), pull the factory like this:
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use App\DataFixtures\Factory\UserFactory;
class UserTest extends KernelTestCase
{
public function testSomething(): void
{
self::bootKernel();
/** @var UserFactory $factory */
$factory = self::getContainer()->get(UserFactory::class);
$user = $factory->create([
'password' => 'plaintext123'
]);
// Now hashed!
$this->assertNotEquals('plaintext123', $user->getPassword());
}
}
See? Container-fetched factory means password hasher injection works. The docs recommend this exact pattern. Static UserFactory::createOne()? Still broken. Container version? Hashed passwords every time.
Pro tip: Cache it if you’re creating tons of users. Symfony containers are cheap to query, but why not $userFactory ??= self::getContainer()->get(UserFactory::class);?
PHPUnit.xml and Bundle Setup
Your phpunit.xml extension looks solid—that boots Foundry’s test helpers. But double-check these:
- KernelTestCase or Traits: Use
Zenstruck\Foundry\Test\Factoriestrait or extendKernelTestCasefor container access. - Bundles Enabled: In
config/bundles.php, ensureZenstruck\Foundry\Bundle\ZenstruckFoundryBundle::class => ['test' => true]is active for test env. - Test Env Config:
ZENSTRUCK_FOUNDRY_DEFAULT_PERSISTENCE=doctrine/ormif needed, but usually auto-detected.
From GitHub issues, missing test-env bundle registration empties the factory manager. Run bin/console debug:container --env=test | grep Foundry to verify.
No extra phpunit.xml tweaks beyond your extension. It handles proxying and cleanup.
Data Providers and Common Pitfalls
Data providers trip folks up. Your yield setup passes a raw new UserFactory() or static instance—neither hits the container.
Fix: Resolve inside the test method:
/**
* @dataProvider userProvider
*/
public function testWithFactory(?UserFactory $userFactory): void
{
self::bootKernel();
$containerFactory = self::getContainer()->get(UserFactory::class);
if ($userFactory) {
// Use passed one if non-null, but prefer container for DI
$user = $containerFactory->create();
} else {
$user = $containerFactory->create();
}
dump($user->getPassword()); // Hashed!
}
Or refactor the provider to always return container instances (boot kernel there? Messy). Better: Make the provider yield data arrays, create inside the test.
Pitfalls? PHP 8.1+ proxies can confuse static calls. Unit tests sans kernel need nullable constructors (your code already does). Always bootKernel() before container grabs.
Debugging Checklist
Stuck? Run this:
bin/console debug:container UserFactory --env=test→ Factory registered?- Dump
$this->passwordHasherinafterInstantiate→ Null or service? bin/phpunit --verbose→ Check for FoundryExtension boot.grep -r "UserFactory" config/→ Services.yaml overrides?- Test static vs. container:
UserFactory::createOne()(fails) vs. container (wins).
If factories vanish, peek this StackOverflow thread. Nine times out of ten, it’s missing when@test services.
Sources
- ZenstruckFoundryBundle Documentation
- Stack Overflow: User ZenstruckFoundryBundle Factories with PHPUnit
- GitHub Issue: Using Foundry in Doctrine Fixtures with Service Injection
Conclusion
Container-managed Zenstruck Foundry factories solve password hasher injection woes in PHPUnit tests—register with when@test, fetch via self::getContainer()->get(), done. Skip statics, embrace services. Your tests run faster, passwords stay secure, and debugging headaches fade. Tweak for data providers, verify with debug:container, and you’re shipping reliable code.