diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 11c3d000b2..97e8433867 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -18,6 +18,7 @@ CHANGELOG * Added sort option for `translation:update` command. * [BC Break] The `framework.messenger.routing.senders` config key is not deep merged anymore. * Added `secrets:*` commands and `%env(secret:...)%` processor to deal with secrets seamlessly. + * Made `framework.session.handler_id` accept a DSN 4.3.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 375852b4a8..aebe9517e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -240,6 +240,9 @@ class FrameworkExtension extends Extension } } + // register cache before session so both can share the connection services + $this->registerCacheConfiguration($config['cache'], $container); + if ($this->isConfigEnabled($container, $config['session'])) { if (!\extension_loaded('session')) { throw new LogicException('Session support cannot be enabled as the session extension is not installed. See https://php.net/session.installation for instructions.'); @@ -326,7 +329,6 @@ class FrameworkExtension extends Extension $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']); $this->registerProfilerConfiguration($config['profiler'], $container, $loader); - $this->registerCacheConfiguration($config['cache'], $container); $this->registerWorkflowConfiguration($config['workflows'], $container, $loader); $this->registerDebugConfiguration($config['php_errors'], $container, $loader); $this->registerRouterConfiguration($config['router'], $container, $loader); @@ -925,7 +927,18 @@ class FrameworkExtension extends Extension $container->getDefinition('session.storage.native')->replaceArgument(1, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); } else { - $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); + $container->resolveEnvPlaceholders($config['handler_id'], null, $usedEnvs); + + if ($usedEnvs || preg_match('#^[a-z]++://#', $config['handler_id'])) { + $id = '.cache_connection.'.ContainerBuilder::hash($config['handler_id']); + + $container->getDefinition('session.abstract_handler') + ->replaceArgument(0, $container->hasDefinition($id) ? new Reference($id) : $config['handler_id']); + + $container->setAlias('session.handler', 'session.abstract_handler')->setPrivate(true); + } else { + $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); + } } $container->setParameter('session.save_path', $config['save_path']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml index 020e29e721..5aae3b4d9f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml @@ -56,6 +56,11 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 55f7706427..ebba20c08f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -22,7 +22,7 @@ "symfony/config": "^4.3.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/error-renderer": "^4.4|^5.0", - "symfony/http-foundation": "^4.3|^5.0", + "symfony/http-foundation": "^4.4|^5.0", "symfony/http-kernel": "^4.4", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "^3.4|^4.0|^5.0", diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 577f27e6c6..10eaa0ac5a 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database to speed up garbage collection of expired sessions. + * added `SessionHandlerFactory` to create session handlers with a DSN 4.3.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php new file mode 100644 index 0000000000..41dba3a2b3 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Doctrine\DBAL\DriverManager; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; + +/** + * @author Nicolas Grekas + */ +class SessionHandlerFactory +{ + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN + */ + public static function createHandler($connection): AbstractSessionHandler + { + if (!\is_string($connection) && !\is_object($connection)) { + throw new \TypeError(sprintf('Argument 1 passed to %s() must be a string or a connection object, %s given.', __METHOD__, \gettype($connection))); + } + + switch (true) { + case $connection instanceof \Redis: + case $connection instanceof \RedisArray: + case $connection instanceof \RedisCluster: + case $connection instanceof \Predis\ClientInterface: + case $connection instanceof RedisProxy: + case $connection instanceof RedisClusterProxy: + return new RedisSessionHandler($connection); + + case $connection instanceof \Memcached: + return new MemcachedSessionHandler($connection); + + case $connection instanceof \PDO: + return new PdoSessionHandler($connection); + + case !\is_string($connection): + throw new \InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection))); + case 0 === strpos($connection, 'file://'): + return new StrictSessionHandler(new NativeFileSessionHandler(substr($connection, 7))); + + case 0 === strpos($connection, 'redis://'): + case 0 === strpos($connection, 'rediss://'): + case 0 === strpos($connection, 'memcached://'): + if (!class_exists(AbstractAdapter::class)) { + throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $this->dsn)); + } + $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); + + return 0 === strpos($connection, 'memcached://') ? new MemcachedSessionHandler($connection) : new RedisSessionHandler($connection); + + case 0 === strpos($connection, 'pdo_oci://'): + if (!class_exists(DriverManager::class)) { + throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); + } + $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection(); + // no break; + + case 0 === strpos($connection, 'mssql://'): + case 0 === strpos($connection, 'mysql://'): + case 0 === strpos($connection, 'mysql2://'): + case 0 === strpos($connection, 'pgsql://'): + case 0 === strpos($connection, 'postgres://'): + case 0 === strpos($connection, 'postgresql://'): + case 0 === strpos($connection, 'sqlsrv://'): + case 0 === strpos($connection, 'sqlite://'): + case 0 === strpos($connection, 'sqlite3://'): + return new PdoSessionHandler($connection); + } + + throw new \InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection)); + } +} diff --git a/src/Symfony/Component/Lock/Store/StoreFactory.php b/src/Symfony/Component/Lock/Store/StoreFactory.php index 255a72a736..8fea94551c 100644 --- a/src/Symfony/Component/Lock/Store/StoreFactory.php +++ b/src/Symfony/Component/Lock/Store/StoreFactory.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Lock\Store; +use Doctrine\DBAL\Connection; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Traits\RedisClusterProxy; use Symfony\Component\Cache\Traits\RedisProxy; @@ -25,59 +26,74 @@ use Symfony\Component\Lock\PersistingStoreInterface; class StoreFactory { /** - * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Memcached|\Zookeeper|string $connection Connection or DSN or Store short name + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|Connection|\Zookeeper|string $connection Connection or DSN or Store short name * * @return PersistingStoreInterface */ public static function createStore($connection) { - if ( - $connection instanceof \Redis || - $connection instanceof \RedisArray || - $connection instanceof \RedisCluster || - $connection instanceof \Predis\ClientInterface || - $connection instanceof RedisProxy || - $connection instanceof RedisClusterProxy - ) { - return new RedisStore($connection); - } - if ($connection instanceof \Memcached) { - return new MemcachedStore($connection); - } - if ($connection instanceof \Zookeeper) { - return new ZookeeperStore($connection); - } - if (!\is_string($connection)) { - throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection))); + if (!\is_string($connection) && !\is_object($connection)) { + throw new \TypeError(sprintf('Argument 1 passed to %s() must be a string or a connection object, %s given.', __METHOD__, \gettype($connection))); } switch (true) { + case $connection instanceof \Redis: + case $connection instanceof \RedisArray: + case $connection instanceof \RedisCluster: + case $connection instanceof \Predis\ClientInterface: + case $connection instanceof RedisProxy: + case $connection instanceof RedisClusterProxy: + return new RedisStore($connection); + + case $connection instanceof \Memcached: + return new MemcachedStore($connection); + + case $connection instanceof \PDO: + case $connection instanceof Connection: + return new PdoStore($connection); + + case $connection instanceof \Zookeeper: + return new ZookeeperStore($connection); + + case !\is_string($connection): + throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection))); case 'flock' === $connection: return new FlockStore(); + case 0 === strpos($connection, 'flock://'): return new FlockStore(substr($connection, 8)); + case 'semaphore' === $connection: return new SemaphoreStore(); - case 0 === strpos($connection, 'redis://') && class_exists(AbstractAdapter::class): - case 0 === strpos($connection, 'rediss://') && class_exists(AbstractAdapter::class): - return new RedisStore(AbstractAdapter::createConnection($connection, ['lazy' => true])); - case 0 === strpos($connection, 'memcached://') && class_exists(AbstractAdapter::class): - return new MemcachedStore(AbstractAdapter::createConnection($connection, ['lazy' => true])); - case 0 === strpos($connection, 'sqlite:'): + + case 0 === strpos($connection, 'redis://'): + case 0 === strpos($connection, 'rediss://'): + case 0 === strpos($connection, 'memcached://'): + if (!class_exists(AbstractAdapter::class)) { + throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $this->dsn)); + } + $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); + + return 0 === strpos($connection, 'memcached://') ? new MemcachedStore($connection) : new RedisStore($connection); + + case 0 === strpos($connection, 'mssql://'): case 0 === strpos($connection, 'mysql:'): - case 0 === strpos($connection, 'pgsql:'): - case 0 === strpos($connection, 'oci:'): - case 0 === strpos($connection, 'sqlsrv:'): - case 0 === strpos($connection, 'sqlite3://'): case 0 === strpos($connection, 'mysql2://'): + case 0 === strpos($connection, 'oci:'): + case 0 === strpos($connection, 'oci8://'): + case 0 === strpos($connection, 'pdo_oci://'): + case 0 === strpos($connection, 'pgsql:'): case 0 === strpos($connection, 'postgres://'): case 0 === strpos($connection, 'postgresql://'): - case 0 === strpos($connection, 'mssql://'): + case 0 === strpos($connection, 'sqlsrv:'): + case 0 === strpos($connection, 'sqlite:'): + case 0 === strpos($connection, 'sqlite3://'): return new PdoStore($connection); + case 0 === strpos($connection, 'zookeeper://'): return new ZookeeperStore(ZookeeperStore::createConnection($connection)); - default: - throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection)); } + + throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection)); } }