Skip to content

Commit 27e36c1

Browse files
committed
[DoctrineBridge] doctrine connection listener for long running runtime
1 parent 014bfac commit 27e36c1

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Listener;
13+
14+
use Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\Exception as DBALException;
16+
use Doctrine\ORM\EntityManagerInterface;
17+
use Doctrine\Persistence\ManagerRegistry;
18+
use ProxyManager\Proxy\LazyLoadingInterface;
19+
use Symfony\Bridge\Doctrine\Event\ForceKernelRebootEvent;
20+
use Symfony\Component\DependencyInjection\ContainerInterface;
21+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
23+
use Symfony\Component\HttpKernel\Event\RequestEvent;
24+
use Symfony\Component\HttpKernel\KernelEvents;
25+
use Symfony\Component\VarExporter\LazyObjectInterface;
26+
27+
/**
28+
* Based on https://github.com/Baldinof/roadrunner-bundle/blob/3.x/src/Integration/Doctrine/DoctrineORMMiddleware.php.
29+
*/
30+
class DoctrineConnectionSubscriber implements EventSubscriberInterface
31+
{
32+
public function __construct(private ManagerRegistry $managerRegistry, private ContainerInterface $container, private EventDispatcherInterface $eventDispatcher)
33+
{
34+
}
35+
36+
public function onKernelRequest(RequestEvent $event)
37+
{
38+
$connectionServices = $this->managerRegistry->getConnectionNames();
39+
40+
foreach ($connectionServices as $connectionServiceName) {
41+
if (!$this->container->initialized($connectionServiceName)) {
42+
continue;
43+
}
44+
45+
$connection = $this->container->get($connectionServiceName);
46+
47+
if (!$connection instanceof Connection) {
48+
throw new \RuntimeException(sprintf('The value "%s" is not an instance of "%s".', $connection ? $connection::class : 'null', Connection::class));
49+
}
50+
51+
if ($connection->isConnected() && !$this->ping($connection)) {
52+
$connection->close();
53+
}
54+
55+
$managerNames = $this->managerRegistry->getManagerNames();
56+
57+
foreach ($managerNames as $managerName) {
58+
if (!$this->container->initialized($managerName)) {
59+
continue;
60+
}
61+
62+
$manager = $this->container->get($managerName);
63+
64+
if (!$manager instanceof EntityManagerInterface) {
65+
throw new \RuntimeException(sprintf('The value "%s" is not an instance of "%s".', $manager ? $manager::class : 'null', EntityManagerInterface::class));
66+
}
67+
68+
if ($manager instanceof LazyLoadingInterface || $manager instanceof LazyObjectInterface) {
69+
continue; // Doctrine bundle will handle manager reset on next request
70+
}
71+
72+
if (!$manager->isOpen()) {
73+
$this->eventDispatcher->dispatch(new ForceKernelRebootEvent(
74+
sprintf('Entity manager "%s" is closed and the package `symfony/proxy-manager-bridge` is not installed so kernel reset will not re-open it', $managerName)
75+
));
76+
77+
return;
78+
}
79+
}
80+
}
81+
}
82+
83+
private function ping(Connection $con): bool
84+
{
85+
try {
86+
$con->executeQuery($con->getDatabasePlatform()->getDummySelectSQL());
87+
88+
return true;
89+
} catch (DBALException $e) {
90+
return false;
91+
}
92+
}
93+
94+
public static function getSubscribedEvents(): array
95+
{
96+
return [
97+
KernelEvents::REQUEST => 'onKernelRequest',
98+
];
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Middleware;
13+
14+
use Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\Driver;
16+
use Doctrine\DBAL\Driver\Connection as DriverConnection;
17+
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
18+
use Doctrine\DBAL\Exception as DBALException;
19+
use Doctrine\ORM\EntityManagerInterface;
20+
use Doctrine\Persistence\ManagerRegistry;
21+
use Symfony\Component\DependencyInjection\ContainerInterface;
22+
23+
class DoctrineConnectionDriver extends AbstractDriverMiddleware
24+
{
25+
public function __construct(private Driver $driver, private readonly ManagerRegistry $managerRegistry, private readonly ContainerInterface $container)
26+
{
27+
parent::__construct($driver);
28+
}
29+
30+
public function connect(array $params): DriverConnection
31+
{
32+
$connectionServices = $this->managerRegistry->getConnectionNames();
33+
34+
foreach ($connectionServices as $connectionServiceName) {
35+
if (!$this->container->initialized($connectionServiceName)) {
36+
continue;
37+
}
38+
39+
$connection = $this->container->get($connectionServiceName);
40+
41+
if (!$connection instanceof Connection) {
42+
continue;
43+
}
44+
45+
if ($connection->isConnected()) {
46+
$this->pingConnection($connection);
47+
}
48+
49+
$managerNames = $this->managerRegistry->getManagerNames();
50+
51+
foreach ($managerNames as $managerName) {
52+
if (!$this->container->initialized($managerName)) {
53+
continue;
54+
}
55+
56+
$manager = $this->container->get($managerName);
57+
58+
if (!$manager instanceof EntityManagerInterface) {
59+
continue;
60+
}
61+
62+
if (!$manager->isOpen()) {
63+
$this->managerRegistry->resetManager($managerName);
64+
}
65+
}
66+
}
67+
68+
return parent::connect($params);
69+
}
70+
71+
private function pingConnection(Connection $connection): void
72+
{
73+
try {
74+
$this->executeDummySql($connection);
75+
} catch (DBALException $e) {
76+
$connection->close();
77+
// Attempt to reestablish the lazy connection by sending another query.
78+
$this->executeDummySql($connection);
79+
}
80+
}
81+
82+
/**
83+
* @throws DBALException
84+
*/
85+
private function executeDummySql(Connection $connection): void
86+
{
87+
$connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL());
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Middleware;
13+
14+
use Doctrine\DBAL\Driver;
15+
use Doctrine\DBAL\Driver\Middleware;
16+
use Doctrine\Persistence\ManagerRegistry;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
18+
19+
/**
20+
* Based on https://github.com/Baldinof/roadrunner-bundle/blob/3.x/src/Integration/Doctrine/DoctrineORMMiddleware.php.
21+
*/
22+
class DoctrineConnectionMiddleware implements Middleware
23+
{
24+
public function __construct(private ManagerRegistry $managerRegistry, private ContainerInterface $container)
25+
{
26+
}
27+
28+
public function wrap(Driver $driver): Driver
29+
{
30+
return new DoctrineConnectionDriver($driver, $this->managerRegistry, $this->container);
31+
}
32+
}

0 commit comments

Comments
 (0)