Description
Symfony version(s) affected
6.2.7
Description
the sulu project is using jackalope-doctrine-dbal through the phpcr-bundle. Sulu decorates the phpcr-bundle session service with its own service. since recently, the factory for the phpcr session accesses the doctrine connection. According to the stack trace we see, accessing the connection in turn triggers creating the session (and i did not fully understand why that does not lead to an endless loop).
the problem we observe now is that some parts of the system are using a different instance of the session than the rest, leading to problems.
i don't have the most simple reproducer. i would be happy to help fix the bug if somebody can point me to where i should put a functional test that recreates the problem in a phpunit test and then from there we can figure out how to fix it.
How to reproduce
i captured the stack trace in the problematic case:
ObjectManager.php:172, Jackalope\ObjectManager->__construct()
Factory.php:62, ReflectionClass->newInstanceArgs()
Factory.php:62, Jackalope\Factory->get()
Session.php:131, Jackalope\Session->__construct()
Factory.php:62, ReflectionClass->newInstanceArgs()
Factory.php:62, Jackalope\Factory->get()
Repository.php:155, Jackalope\Repository->login()
App_KernelDevDebugContainer.php:1430, ContainerTqpZCZm\App_KernelDevDebugContainer->getDoctrinePhpcr_SessionService()
App_KernelDevDebugContainer.php:5170, ContainerTqpZCZm\App_KernelDevDebugContainer->getSuluDocumentManager_NodeManagerService()
App_KernelDevDebugContainer.php:2505, ContainerTqpZCZm\App_KernelDevDebugContainer->getSuluDocumentManager_DocumentManagerService()
App_KernelDevDebugContainer.php:5498, ContainerTqpZCZm\App_KernelDevDebugContainer->getSuluSecurity_AccessControlManagerService()
getSuluSecurity_PermissionInheritanceSubscriberService.php:23, ContainerTqpZCZm\getSuluSecurity_PermissionInheritanceSubscriberService::do()
App_KernelDevDebugContainer.php:1118, ContainerTqpZCZm\App_KernelDevDebugContainer->load()
Container.php:381, Symfony\Component\DependencyInjection\Container->getService()
ServiceLocator.php:40, Symfony\Component\DependencyInjection\Argument\ServiceLocator->get()
ContainerAwareEventManager.php:191, Symfony\Bridge\Doctrine\ContainerAwareEventManager->initializeSubscribers()
ContainerAwareEventManager.php:105, Symfony\Bridge\Doctrine\ContainerAwareEventManager->hasListeners()
Connection.php:384, Doctrine\DBAL\Connection->connect()
Connection.php:446, Doctrine\DBAL\Connection->getDatabasePlatformVersion()
Connection.php:408, Doctrine\DBAL\Connection->detectDatabasePlatform()
Connection.php:316, Doctrine\DBAL\Connection->getDatabasePlatform()
RepositoryFactoryDoctrineDBAL.php:94, Jackalope\RepositoryFactoryDoctrineDBAL->getRepository()
App_KernelDevDebugContainer.php:1430, ContainerTqpZCZm\App_KernelDevDebugContainer->getDoctrinePhpcr_SessionService()
App_KernelDevDebugContainer.php:5170, ContainerTqpZCZm\App_KernelDevDebugContainer->getSuluDocumentManager_NodeManagerService()
App_KernelDevDebugContainer.php:2505, ContainerTqpZCZm\App_KernelDevDebugContainer->getSuluDocumentManager_DocumentManagerService()
App_KernelDevDebugContainer.php:1971, ContainerTqpZCZm\App_KernelDevDebugContainer->getSulu_Content_MapperService()
App_KernelDevDebugContainer.php:5741, ContainerTqpZCZm\App_KernelDevDebugContainer->getTwigService()
App_KernelDevDebugContainer.php:5312, ContainerTqpZCZm\App_KernelDevDebugContainer->getSuluForm_RequestListenerService()
App_KernelDevDebugContainer.php:1472, ContainerTqpZCZm\App_KernelDevDebugContainer->ContainerTqpZCZm\{closure:/home/david/tmp/reproducer-jackalope-error-1-9-0/var/cache/admin/dev/ContainerTqpZCZm/App_KernelDevDebugContainer.php:1471-1473}()
EventDispatcher.php:221, Symfony\Component\EventDispatcher\EventDispatcher->sortListeners()
EventDispatcher.php:70, Symfony\Component\EventDispatcher\EventDispatcher->getListeners()
TraceableEventDispatcher.php:257, Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->preProcess()
TraceableEventDispatcher.php:121, Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch()
HttpKernel.php:139, Symfony\Component\HttpKernel\HttpKernel->handleRaw()
HttpKernel.php:74, Symfony\Component\HttpKernel\HttpKernel->handle()
Kernel.php:184, Symfony\Component\HttpKernel\Kernel->handle()
index.php:68, {main}()
we have a reproducer application that you can hopefully use:
git clone git@github.com:alexander-schranz/reproducer-jackalope-error-1-9-0.git
cd reproducer-jackalope-error-1-9-0
composer install
docker compose up -d
bin/console sulu:build dev
symfony serve
go to https://localhost:8000/admin, login with admin/admin and try to create a new page in the example.com
site. there is a async call that fails because an expected item is not found (it was created in the other session).
Possible Solution
the sulu session is defined as decorating the phpcr session at https://github.com/sulu/sulu/blob/57bf8c4dcc59361033ef6d7acc5898b850f15b5c/src/Sulu/Bundle/DocumentManagerBundle/DependencyInjection/SuluDocumentManagerExtension.php#L160.
this is whats generated in the container:
/**
* Gets the public 'doctrine_phpcr.session' shared service.
*
* @return \Sulu\Bundle\DocumentManagerBundle\Session\Session
*/
protected function getDoctrinePhpcr_SessionService()
{
$a = ($this->services['doctrine.dbal.default_connection'] ?? $this->getDoctrine_Dbal_DefaultConnectionService());
if (isset($this->services['doctrine_phpcr.session'])) {
return $this->services['doctrine_phpcr.session'];
}
return $this->services['doctrine_phpcr.session'] = new \Sulu\Bundle\DocumentManagerBundle\Session\Session([($this->privates['doctrine_phpcr.jackalope.repository.factory.service.doctrinedbal'] ??= new \Jackalope\RepositoryFactoryDoctrineDBAL())->getRepository(['jackalope.doctrine_dbal_connection' => $a, 'jackalope.check_login_on_server' => false]), 'login'](new \PHPCR\SimpleCredentials('admin', 'admin'), $this->getEnv('PHPCR_WORKSPACE')));
}
the factory instance is shared, but getRepository
is called whenever the session does not yet exist.
if i hack the container to look as following, the error is gone:
/**
* Gets the public 'doctrine_phpcr.session' shared service.
*
* @return \Sulu\Bundle\DocumentManagerBundle\Session\Session
*/
protected function getDoctrinePhpcr_SessionService()
{
$a = ($this->services['doctrine.dbal.default_connection'] ?? $this->getDoctrine_Dbal_DefaultConnectionService());
if (isset($this->services['doctrine_phpcr.session'])) {
return $this->services['doctrine_phpcr.session'];
}
$b = [($this->privates['doctrine_phpcr.jackalope.repository.factory.service.doctrinedbal'] ??= new \Jackalope\RepositoryFactoryDoctrineDBAL())->getRepository(['jackalope.doctrine_dbal_connection' => $a, 'jackalope.check_login_on_server' => false]), 'login'](new \PHPCR\SimpleCredentials('admin', 'admin'), $this->getEnv('PHPCR_WORKSPACE'));
if (isset($this->services['doctrine_phpcr.session'])) {
return $this->services['doctrine_phpcr.session'];
}
return $this->services['doctrine_phpcr.session'] = new \Sulu\Bundle\DocumentManagerBundle\Session\Session($b);
}
to my understanding this means that $this->services['doctrine_phpcr.session']
is written twice, which seems incorrect to me.
the tricky bit is that my $b
should not be above the first check if we have a session service, as it would create overhead and potentially trigger new problems if we already have the session. i guess the idea with $a
is that if we already have the service, its super cheap as it must be in the container already.
Additional Context
As a workaround, we can make sulu define its decorating session service as lazy. When it is lazy, only one instance gets created as the access to doctrine connection that triggers the other instance only happens when the first instance is already registered in the container, so no extra instance is created.