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 + +