13

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?

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Mohammed Mehira
  • 343
  • 1
  • 3
  • 9

6 Answers6

7

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

kallosz
  • 521
  • 3
  • 10
  • `$this->container->getDefinition('user.user_service')->setSynthetic(true);` I cannot use this in my tests suites. The compiler is already compiled. – Mohammed Mehira Jul 27 '18 at 08:27
  • if you want to replace service in your test code it means that you have something wrong design in your app. – kallosz Jul 27 '18 at 14:41
  • 9
    it's a functional test, it's a normal behaviour. – Mohammed Mehira Jul 28 '18 at 15:14
  • 20
    @kallosz A service used by the app to send mails must not send mails during tests. It must be replaced by a mock. So must any service performing network communication, online payment or producing heavy load such as generating a pdf. Finally, a service must also be replaced by a mock to return controlled error codes to check the app is handling errors correctly. This is normal behavior during tests. – Frédéric Marchal Nov 28 '18 at 09:40
  • doesn't work in 4.4 – max4ever Aug 12 '22 at 15:12
5

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.

Mohammed Mehira
  • 343
  • 1
  • 3
  • 9
1

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.

Alister Bulman
  • 34,482
  • 9
  • 71
  • 110
1

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).

max4ever
  • 11,909
  • 13
  • 77
  • 115
1

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.
GuyPaddock
  • 2,233
  • 2
  • 23
  • 27
-1

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:

Vitas Brazas
  • 21
  • 1
  • 2