4
votes

Currently I'm working on testing some services in Symfony2 and I'm trying to use Guzzle MockPlugin for controlling CURL responses. Symfony version 2.3.8 is used. I've got to an interesting behaviour and I'm not sure if this is a Symfony2 bug or not.

I have these services in services.yml:

lookup_service_client:
    class: FOO
    public: false
    factory_service: lookup_client_builder
    factory_method: build

lookup_repository_auth_type:
    class: AuthType
    arguments: ["@lookup_service_client"]

lookup_repository_cancel_reason:
    class: CancelReason
    arguments: ["@lookup_service_client"]

payment_service_client:
    class: FOO
    public: false
    factory_service: payment_client_builder
    factory_method: build

payment_repository:
    class: Payment
    arguments: ["@payment_service_client"]

The name of the classes are not important. You can see that both "lookup_service_client" and "lookup_service_client" are PRIVATE services.

I have a test class, which extends Symfony\Bundle\FrameworkBundle\Test\WebTestCase. In one test I need to do something like:

$lookup = $this->client->getContainer()->get('lookup_service_client');

$payment = $this->client->getContainer()->get('payment_service_client');

I expected that, setting those services as PRIVATE, will not let me retrieve the services from container in tests, but the actual result is:

$lookup = $this->client->getContainer()->get('lookup_service_client'); => returns the service instance

$payment = $this->client->getContainer()->get('payment_service_client'); => returns an exception saying: "You have requested a non-existent service"

The only difference between those tow service_client services is that "lookup_service_client" is injected in several other services, while "payment_service_client" is injected in only one other service.

So, the questions are:

  1. Why I can retrieve from container "lookup_service_client", since I've set it to private?

  2. Why I can retrieve "lookup_service_client", but cannot retrieve "payment_service_client" since the only difference is presented above?

  3. Is it a Symfony2 bug that I can access private service?

3

3 Answers

2
votes

There were some new changes regarding this in Symfony 4.1:

In Symfony 4.1, we did the same and now tests allow fetching private services by default.

In practice, tests based on WebTestCase and KernelTestCase now access to a special container via $client->getContainer() or the static::$container property that allows to fetch non-removed private services.

You can read more about it in the news post.

While this is not a bug, it is definitely counter intuitive. The manual specifically says:

Now that the service is private, you should not fetch the service directly from the container:

$container->get('foo');

This may or may not work, depending on how the container has optimized the service instanciation and, even in the cases where it works, is deprecated. Simply said: A service can be marked as private if you do not want to access it directly from your code.

Which is why the core team has decided to make this behavior more consistent and intuitive in Symfony 4:

Setting or unsetting a private service with the Container::set() method is deprecated in Symfony 3.2 and no longer supported in 4.0;

Checking the existence of a private service with the Container::has() will always return false in Symfony 4.0;

Requesting a private service with the Container::get() method is deprecated in Symfony 3.2 and no longer returns the service in 4.0.

2
votes

2018+ and Symfony 3.4/4.0+ solution

This approach with all its pros/cons is described in this post with code examples.


The best solution to access private services is to add a Compiler Pass that makes all services public for tests. That's it. How does it look in practice?

1. Update Kernel

 use Symfony\Component\HttpKernel\Kernel;
+use Symplify\PackageBuilder\DependencyInjection\CompilerPass\PublicForTestsCompilerPass;

 final class AppKernel extends Kernel
 {
     protected function build(ContainerBuilder $containerBuilder): void
     {
         $containerBuilder->addCompilerPass('...');
+        $containerBuilder->addCompilerPass(new PublicForTestsCompilerPass());
     }
 }

2. Require or create own Compiler Pass

Where PublicForTestsCompilerPass looks like:

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class PublicForTestsCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $containerBuilder): void
    {
        if (! $this->isPHPUnit()) {
            return;
        }

        foreach ($containerBuilder->getDefinitions() as $definition) {
            $definition->setPublic(true);
        }

        foreach ($containerBuilder->getAliases() as $definition) {
            $definition->setPublic(true);
        }
    }

    private function isPHPUnit(): bool
    {
        // defined by PHPUnit
        return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__');
    }
}

To use this class, just add the package by:

composer require symplify/package-builder

But of course, the better way is to use own class, that meets your needs (you might Behat for tests etc.).

Then all your tests will keep working as expected!

Let me know, how that works for you.

-1
votes

Check them in the container:

container:debug lookup_service_client

container:debug payment_service_client

in your example they both have class "FOO", maybe that's the case