Skip to content

Commit

Permalink
feature #34177 [HttpFoundation][FrameworkBundle] allow configuring th…
Browse files Browse the repository at this point in the history
…e session handler with a DSN (nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

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

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

```yaml
framework:
    session:
        handler_id: 'redis://localhost'
        handler_id: '%env(REDIS_URL)%'
        handler_id: '%env(DATABASE_URL)%'
        handler_id: 'file://%kernel.project_dir%/var/sessions'
```

etc.

the database connection is not shared with the ORM (don't mess with transactions.)
redis/memcached connections are shared between cache and session.
(as a reminder, cache and ORM share the db connection: we're ok with trashing the cache on a rollback)

Lock-related changes are a follow up of #34043.
(fabbot failure is false positive)

Commits
-------

de9c61f [HttpFoundation][FrameworkBundle] allow configuring the session handler with a DSN
  • Loading branch information
fabpot committed Oct 30, 2019
2 parents 9e7ab8c + de9c61f commit 6bb7751
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 35 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
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
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
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
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
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
@@ -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
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));
}
}

0 comments on commit 6bb7751

Please sign in to comment.