diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 3035c25664b2..594b5fe37de1 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -75,13 +75,14 @@
Cache\IntegrationTests
Symfony\Bridge\Doctrine\Middleware\Debug
- Symfony\Component\Cache
- Symfony\Component\Cache\Tests\Fixtures
- Symfony\Component\Cache\Tests\Traits
- Symfony\Component\Cache\Traits
- Symfony\Component\Console
- Symfony\Component\HttpFoundation
- Symfony\Component\Uid
+ Symfony\Bridge\Doctrine\Middleware\IdleConnection
+ Symfony\Component\Cache
+ Symfony\Component\Cache\Tests\Fixtures
+ Symfony\Component\Cache\Tests\Traits
+ Symfony\Component\Cache\Traits
+ Symfony\Component\Console
+ Symfony\Component\HttpFoundation
+ Symfony\Component\Uid
diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md
index fee531ebfd1e..4f5f58547731 100644
--- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md
+++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
* Deprecate the `DoctrineExtractor::getTypes()` method, use `DoctrineExtractor::getType()` instead
* Allow `EntityValueResolver` to return a list of entities
+ * Add support for auto-closing idle connections
7.0
---
diff --git a/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php
new file mode 100644
index 000000000000..566002cf8487
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection;
+
+use Doctrine\DBAL\Driver as DriverInterface;
+use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
+use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
+
+final class Driver extends AbstractDriverMiddleware
+{
+ public function __construct(
+ DriverInterface $driver,
+ private \ArrayObject $connectionExpiries,
+ private readonly int $ttl,
+ private readonly string $connectionName,
+ ) {
+ parent::__construct($driver);
+ }
+
+ public function connect(array $params): ConnectionInterface
+ {
+ $timestamp = time();
+ $connection = parent::connect($params);
+ $this->connectionExpiries[$this->connectionName] = $timestamp + $this->ttl;
+
+ return $connection;
+ }
+}
diff --git a/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php
new file mode 100644
index 000000000000..eb27a3ee4b0b
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php
@@ -0,0 +1,55 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+final class Listener implements EventSubscriberInterface
+{
+ /**
+ * @param \ArrayObject $connectionExpiries
+ */
+ public function __construct(
+ private readonly \ArrayObject $connectionExpiries,
+ private ContainerInterface $container,
+ ) {
+ }
+
+ public function onKernelRequest(RequestEvent $event): void
+ {
+ $timestamp = time();
+
+ foreach ($this->connectionExpiries as $name => $expiry) {
+ if ($timestamp >= $expiry) {
+ // unset before so that we won't retry in case of any failure
+ $this->connectionExpiries->offsetUnset($name);
+
+ try {
+ $connection = $this->container->get("doctrine.dbal.{$name}_connection");
+ $connection->close();
+ } catch (\Exception) {
+ // ignore exceptions to remain fail-safe
+ }
+ }
+ }
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ KernelEvents::REQUEST => 'onKernelRequest',
+ ];
+ }
+}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/DriverTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/DriverTest.php
new file mode 100644
index 000000000000..010e1879a8ab
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/DriverTest.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\Doctrine\Tests\Middleware\IdleConnection;
+
+use Doctrine\DBAL\Driver as DriverInterface;
+use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
+use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver;
+
+class DriverTest extends TestCase
+{
+ /**
+ * @group time-sensitive
+ */
+ public function testConnect()
+ {
+ $driverMock = $this->createMock(DriverInterface::class);
+ $connectionMock = $this->createMock(ConnectionInterface::class);
+
+ $driverMock->expects($this->once())
+ ->method('connect')
+ ->willReturn($connectionMock);
+
+ $connectionExpiries = new \ArrayObject();
+
+ $driver = new Driver($driverMock, $connectionExpiries, 60, 'default');
+ $connection = $driver->connect([]);
+
+ $this->assertSame($connectionMock, $connection);
+ $this->assertArrayHasKey('default', $connectionExpiries);
+ $this->assertSame(time() + 60, $connectionExpiries['default']);
+ }
+}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/ListenerTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/ListenerTest.php
new file mode 100644
index 000000000000..099ab4877713
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/ListenerTest.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Middleware\IdleConnection;
+
+use Doctrine\DBAL\Connection as ConnectionInterface;
+use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+
+class ListenerTest extends TestCase
+{
+ public function testOnKernelRequest()
+ {
+ $containerMock = $this->createMock(ContainerInterface::class);
+ $connectionExpiries = new \ArrayObject(['connectionone' => time() - 30, 'connectiontwo' => time() + 40]);
+
+ $connectionOneMock = $this->getMockBuilder(ConnectionInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $containerMock->expects($this->exactly(1))
+ ->method('get')
+ ->with('doctrine.dbal.connectionone_connection')
+ ->willReturn($connectionOneMock);
+
+ $listener = new Listener($connectionExpiries, $containerMock);
+
+ $listener->onKernelRequest($this->createMock(RequestEvent::class));
+
+ $this->assertArrayNotHasKey('connectionone', (array) $connectionExpiries);
+ $this->assertArrayHasKey('connectiontwo', (array) $connectionExpiries);
+ }
+}
diff --git a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist
index f99086654eca..0328649ec4f2 100644
--- a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist
+++ b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist
@@ -33,7 +33,12 @@
- Symfony\Bridge\Doctrine\Middleware\Debug
+
+
+ Symfony\Bridge\Doctrine\Middleware\Debug
+ Symfony\Bridge\Doctrine\Middleware\Debug
+
+