Web

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.

1 answer 1 view

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:

xml
<phpunit>
 <extensions>
 <bootstrap class="Zenstruck\\Foundry\\PHPUnit\\FoundryExtension"/>
 </extensions>
</phpunit>

My UserFactory injects PasswordHasherInterface and hashes the password in afterInstantiate:

php
<?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:

php
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

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:

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:

yaml
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:

php
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:

  1. KernelTestCase or Traits: Use Zenstruck\Foundry\Test\Factories trait or extend KernelTestCase for container access.
  2. Bundles Enabled: In config/bundles.php, ensure Zenstruck\Foundry\Bundle\ZenstruckFoundryBundle::class => ['test' => true] is active for test env.
  3. Test Env Config: ZENSTRUCK_FOUNDRY_DEFAULT_PERSISTENCE=doctrine/orm if 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:

php
/**
 * @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->passwordHasher in afterInstantiate → 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

  1. ZenstruckFoundryBundle Documentation
  2. Stack Overflow: User ZenstruckFoundryBundle Factories with PHPUnit
  3. 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.

Authors
Verified by moderation
Moderation
Fix Password Hasher Injection in Zenstruck Foundry Tests