Description
Symfony version(s) affected: 4.3.5
Description
@nicolas-grekas Following up on our discussion at the meetup, I've researched the segmentation fault / memory limit error.
The bug comes from the way the method getDoctrine_Dbal_DefaultConnectionService()
in the dumped container instantiates $this->services['doctrine.dbal.default_connection']
for Doctrine\DBAL\Connection
The method first instantiate all dependencies as expected:
protected function getDoctrine_Dbal_DefaultConnectionService()
{
$a = ($this->services['validator'] ?? $this->getValidatorService());
if (isset($this->services['doctrine.dbal.default_connection'])) {
return $this->services['doctrine.dbal.default_connection'];
}
$b = ($this->services['search.search_indexer_subscriber'] ?? $this->getSearch_SearchIndexerSubscriberService());
if (isset($this->services['doctrine.dbal.default_connection'])) {
return $this->services['doctrine.dbal.default_connection'];
}
$c = new \Doctrine\DBAL\Configuration();
$d = new \Doctrine\DBAL\Logging\LoggerChain();
$e = new \Symfony\Bridge\Monolog\Logger('doctrine');
$e->pushHandler(($this->privates['monolog.handler.main'] ?? $this->getMonolog_Handler_MainService()));
$e->pushHandler(($this->privates['monolog.handler.syslog_handler'] ?? $this->getMonolog_Handler_SyslogHandlerService()));
$d->addLogger(new \Symfony\Bridge\Doctrine\Logger\DbalLogger($e, ($this->privates['debug.stopwatch'] ?? ($this->privates['debug.stopwatch'] = new \Symfony\Component\Stopwatch\Stopwatch(true)))));
$d->addLogger(new \Doctrine\DBAL\Logging\DebugStack());
$c->setSQLLogger($d);
$c->setSchemaAssetsFilter(new \Doctrine\Bundle\DoctrineBundle\Dbal\SchemaAssetsFilterManager([0 => new \Doctrine\Bundle\DoctrineBundle\Dbal\RegexSchemaAssetFilter('~^(?!(BuyPacker_|SMoney_|Spark_|buypacker_|smoney_|spark))~')]));
$f = new \Symfony\Bridge\Doctrine\ContainerAwareEventManager(new \Symfony\Component\DependencyInjection\Argument\ServiceLocator($this->getService, [
'doctrine.orm.default_listeners.attach_entity_listeners' => ['privates', 'doctrine.orm.default_listeners.attach_entity_listeners', 'getDoctrine_Orm_DefaultListeners_AttachEntityListenersService.php', true],
], [
'doctrine.orm.default_listeners.attach_entity_listeners' => '?',
]));
$g = new \App\Doctrine\ORM\Subscriber\LoggerSubscriber();
$g->setDebugHelper(($this->services['legacy.helpers.debug'] ?? ($this->services['legacy.helpers.debug'] = new \App\Helper\DebugHelper($this->targetDirs[3]))));
$g->setFormatter(new \App\Formatter\DoctrineLogFormatter());
$h = new \App\Doctrine\ORM\Subscriber\ValidatorSubscriber();
$h->setValidator($a);
$f->addEventSubscriber($g);
$f->addEventSubscriber($h);
$f->addEventSubscriber($b);
$f->addEventListener([0 => 'loadClassMetadata'], 'doctrine.orm.default_listeners.attach_entity_listeners');
return $this->services['doctrine.dbal.default_connection'] = (new \Doctrine\Bundle\DoctrineBundle\ConnectionFactory($this->parameters['doctrine.dbal.connection_factory.types']))->createConnection(['driver' => 'pdo_mysql', 'charset' => 'utf8mb4', 'url' => 'mysql://root:root@127.0.0.1:3306/assoconnect', 'host' => 'localhost', 'port' => NULL, 'user' => 'root', 'password' => NULL, 'driverOptions' => [], 'serverVersion' => '5.7', 'defaultTableOptions' => []], $c, $f, ['bic' => 'string', 'country' => 'string', 'currency' => 'string', 'datetimetz' => 'datetime', 'datetimeutc' => 'datetime', 'email' => 'string', 'iban' => 'string', 'ip' => 'string', 'latitude' => 'decimal', 'locale' => 'string', 'longitude' => 'decimal', 'money' => 'decimal', 'percent' => 'decimal', 'phone' => 'string', 'phonelandline' => 'string', 'phonemobile' => 'string', 'timezone' => 'string', 'uuid_binary_ordered_time' => 'binary']);
}
and ends by defining $this->services['doctrine.dbal.default_connection']
as expected.
BUT in our case there is one dependency $b = ($this->services['search.search_indexer_subscriber'] ?? $this->getSearch_SearchIndexerSubscriberService());
to define Algolia\SearchBundle\EventListener\SearchIndexerSubscriber
.
This dependency is used as an event subscriber:
$f->addEventSubscriber($b);
The dependency chain is:
Algolia\SearchBundle\EventListener\SearchIndexerSubscriber
Algolia\SearchBundle\IndexManager
Symfony\Component\Serializer\Serializer
The Serializer lists all the Normalizers for its first argument:
\Symfony\Component\Serializer\Serializer::__construct(array $normalizers = [], array $encoders = []))
One of our normalizers depends on a service depending on EntityManagerInterface
which requires the actual service Doctrine\ORM\EntityManager
which depends on Doctrine\DBAL\Connection
.
So in the end, $this->services['doctrine.dbal.default_connection']
is never defined and we have an infine loop.
Bottom line, I think there are different bugs here:
- the circular-dependency checker seems to only look at constructors' argument type hints
- the documentation should state to "never inject
EntityManagerInterface
but rely onDoctrine\Common\Persistence\ManagerRegistry
" as you were saying.
For the 2nd one, https://symfony.com/doc/current/doctrine.html states:
you can add an argument to the action: createProduct(EntityManagerInterface $entityManager)
This works because Controllers usually are not used as dependencies.
But it is misleading for people writing services: a best practice should be given somewhere.
How to reproduce
I haven't written yet a reproducer but I'm not sure it's useful here.
Possible Solution
I can submit a doc PR if you want.
And if updating the circular-dependency checker is too difficult, an alternative may be to create a state checker in the dumper container for each method:
protected function getService_XXX()
{
if (in_array('some.service', $this->instantiating)) {
// dump some useful stack for the developer
die(1);
} else {
$this->instantiating []= 'some.service';
}
// Current code
return $this->services['some.service'] = new SomeService(...);
}