Reputation: 361
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
Reputation: 2517
All the answers to this question seem to overlook the complete history of replacing services in Symfony:
$service->set()
, even for test purposes.$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:
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
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
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
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
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
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