How to access a private service in Symfony 5.3 phpunit tests?
Asked Answered
A

4

5

I want to define a functional testcase for my phpunit-tests for my Symfony 5.3 application which requires the private service security.password_hasher from the container.

I get the following execption

  1. App\Tests\Functional\SiteResourceTest::testCreateSite Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The security.password_hasher service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.

I followed the instructions from the documentation about retrieving services in the test

What am i doing wrong? How can i fix this?

class CustomApiTestCase extends ApiTestCase
{
    protected UserPasswordHasher $passwordHasher;

    protected function setUp(): void
    {
        // (1) boot the Symfony kernel
        self::bootKernel();

        // (2) use static::getContainer() to access the service container
        $container = static::getContainer();

        // (3) run some service & test the result
        $this->passwordHasher = $container->get('security.password_hasher');
    }

    protected function createUser(
        string $email,
        string $password,
    ): User {
        $user = new User();
        $user->setEmail($email);

        $encoded = $this->passwordHasher->hash($password);
        $user->setPassword($encoded);

        $em = self::getContainer()->get('doctrine')->getManager();
        $em->persist($user);
        $em->flush();

        return $user;
    }


    protected function createUserAndLogIn(Client $client, string $email, string $password): User
    {
        $user = $this->createUser($email, $password);
        $this->logIn($client, $email, $password);

        return $user;
    }



    protected function logIn(Client $client, string $email, string $password)
    {
        $client->request('POST', '/login', [
            'headers' => ['Content-Type' => 'application/json'],
            'json' => [
                'email' => $email,
                'password' => $password
            ],
        ]);
        $this->assertResponseStatusCodeSame(204);
    }
}
Amary answered 20/8, 2021 at 16:2 Comment(1)
From the link you posted: If you need to test private services that have been removed (those who are not used by any other services), you need to declare those private services as public in the config/services_test.yaml file.Monteria
F
4

I had to rewrite the complete definition of the service to prevent the following error during cache compilation:

The definition for "security.user_password_hasher" has no class. If you intend to inject this service dynamically at runtime, please mark it as synthetic=true. If this is an abstract definition solely used by child definitions, please add abstract=true, otherwise specify a class to get rid of this error.

services.yaml

security.user_password_hasher:
    class: Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher
    public: true
    arguments:
      [ '@security.password_hasher_factory' ]
French answered 18/10, 2021 at 10:0 Comment(0)
A
2

I solved it by making the service explicitly public in the services_test.yaml:

services:
    Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher:
        public: true

And then retrieving the service by its classname

$this->passwordHasher = $container->get(UserPasswordHasher::class);
Amary answered 20/8, 2021 at 16:19 Comment(1)
Just be aware that by changing the service id you are actually going to have two instances of the hasher floating around. Probably won't matter in this case but I think it might be best to use snake case here. And a bit off-topic perhaps but it's a bit unusual to access the password hasher directly. Usually you would access the UserPasswordHasher which in turn accesses the password hasher based on the type of user.Monteria
G
1

For Symfony 6

If you extend from Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

You can get the container and get the UserPasswordHasherInterface like this:

$paswordHasher = static::getContainer()->get(UserPasswordHasherInterface::class);

Also, you can get the entity manager or repositories:

$entityManager = static::getContainer()->get('doctrine')->getManager();

$repository = static::getContainer()->get('doctrine')->getManager()->getRepository(Foo::class);
Glasswork answered 10/10, 2023 at 18:10 Comment(0)
C
0

A test kernel may also do the following:

class PublicService implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getDefinitions() as $id => $definition) {
            if (stripos($id, 'whatEverIWant') === 0) {
                $definition->setPublic(true);
            }
        }
        foreach ($container->getAliases() as $id => $definition) {
            if (stripos($id, 'whatEverIWant') === 0) {
                $definition->setPublic(true);
            }
        }
    }
}

class AppKernel extends BaseKernel
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new PublicService(), PassConfig::TYPE_OPTIMIZE);
        parent::build($container);
    }
}
Cubature answered 7/4, 2022 at 1:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.