Mohammed Mehira
Mohammed Mehira

Reputation: 361

Symfony 4 : Override public services in container

I am migrating our project to Symfony 4. In my test suites, we used PHPUnit for functional tests (I mean, we call endpoints and we check result). Often, we mock services to check different steps.

Since I migrated to Symfony 4, I am facing this issue: Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "my.service" service is already initialized, you cannot replace it. when we redefine it like this : static::$container->set("my.service", $mock);

Only for tests, how can I fix this issue?

Upvotes: 15

Views: 17999

Answers (6)

GuyPaddock
GuyPaddock

Reputation: 2517

All the answers to this question seem to overlook the complete history of replacing services in Symfony:

  1. Prior to Symfony 3.2, it was possible to replace services even after they were initialized.
  2. Symfony 3.2 deprecated the ability to replace services with $service->set(), even for test purposes.
  3. After some community feedback about DX in tests with the deprecation, the ability to replace services with $service->set() was restored in 3.3.10, but with the constraint that the service being replaced must not have been "initialized". In other words, the service must not have already been injected into other services (since it's hard for the container to know how to reinitialize the entire graph of all services if such a service is being replaced).

So, if you are getting the The "my.service" service is already initialized, you cannot replace it. message, that means one of two things:

  1. The service you are attempting to replace was already used to initialize another service. You may need to replace the service earlier in your test or rearrange the order in which you are initializing services in your test.
  2. (Not documented anywhere) You are trying to replace a lazy service. Lazy services get a lazy-loading stub injected by the container immediately upon definition, and this causes the container to consider them initialized. As a workaround, define the lazy service as not lazy in your services_test.yaml file. I found this out by stepping through the code in a project that has a lazy service, and this is the workaround I had to use.

Upvotes: 6

max4ever
max4ever

Reputation: 12142

As I understood it, it means that class X was already injected(because of some other dependency) somewhere before your code tries to overwrite it with self::$container->set(X:class, $someMock).

Upvotes: 2

Vitautas Brazas
Vitautas Brazas

Reputation: 51

If you on Symfony 3.4 and below you can ovverride services in container regardless it privite or public. Only deprication notice will be emmited, with content similar to error message from question.

On Symfony 4.0 error from the question was thown.

But on Symfony 4.1 and above you can lean on special "test" container. To learn how to use it consider follow next links:

Upvotes: -1

Mohammed Mehira
Mohammed Mehira

Reputation: 361

Finally, I found a solution. Maybe not the best, but, it's working:

I created another test container class and I override the services property using Reflection:

<?php

namespace My\Bundle\Test;

use Symfony\Bundle\FrameworkBundle\Test\TestContainer as BaseTestContainer;

class TestContainer extends BaseTestContainer
{
    private $publicContainer;

    public function set($id, $service)
    {
        $r = new \ReflectionObject($this->publicContainer);
        $p = $r->getProperty('services');
        $p->setAccessible(true);

        $services = $p->getValue($this->publicContainer);

        $services[$id] = $service;

        $p->setValue($this->publicContainer, $services);
    }

    public function setPublicContainer($container)
    {
        $this->publicContainer = $container;
    }

Kernel.php :

<?php

namespace App;

use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    public function getOriginalContainer()
    {
        if(!$this->container) {
            parent::boot();
        }

        /** @var Container $container */
        return $this->container;
    }

    public function getContainer()
    {
        if ($this->environment == 'prod') {
            return parent::getContainer();
        }

        /** @var Container $container */
        $container = $this->getOriginalContainer();

        $testContainer = $container->get('my.test.service_container');

        $testContainer->setPublicContainer($container);

        return $testContainer;
    }

It's really ugly, but it's working.

Upvotes: 4

kallosz
kallosz

Reputation: 531

Replacing is deprecated since Symfony 3.3. Instead of replacing service you should try using aliases. http://symfony.com/doc/current/service_container/alias_private.html

Also, you can try this approach:

$this->container->getDefinition('user.user_service')->setSynthetic(true); before doing $container->set()

Replace Symfony service in tests for php 7.2

Upvotes: 7

Alister Bulman
Alister Bulman

Reputation: 35149

I've got a couple of tests like this (the real code performs some actions and returns a result, the test-version just returns false for every answer).

If you create and use a custom config for each environment (eg: a services_test.yaml, or in Symfony4 probably tests/services.yaml), and first have it include dev/services.yaml, but then override the service you want, the last definition will be used.

app/config/services_test.yml:

imports:
    - { resource: services.yml }

App\BotDetector\BotDetectable: '@App\BotDetector\BotDetectorNeverBot'

# in the top-level 'live/prod' config this would be 
# App\BotDetector\BotDetectable: '@App\BotDetector\BotDetector'

Here, I'm using an Interface as a service-name, but it will do the same with '@service.name' style as well.

Upvotes: 1

Related Questions