Skip to content

[HttpFoundation][FrameworkBundle] allow configuring the session handler with a DSN #34177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ public function load(array $configs, ContainerBuilder $container)
}
}

// 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.');
Expand Down Expand Up @@ -326,7 +329,6 @@ public function load(array $configs, ContainerBuilder $container)
$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);
Expand Down Expand Up @@ -925,7 +927,18 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c
$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']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
</argument>
</service>

<service id="session.abstract_handler" class="Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler">
<factory class="Symfony\Component\HttpFoundation\Session\Storage\Handler\SessionHandlerFactory" method="createHandler" />
<argument />
</service>

<service id="session_listener" class="Symfony\Component\HttpKernel\EventListener\SessionListener">
<tag name="kernel.event_subscriber" />
<argument type="service_locator">
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <p@tchwork.com>
*/
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));
}
}
80 changes: 48 additions & 32 deletions src/Symfony/Component/Lock/Store/StoreFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,59 +26,74 @@
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));
}
}