[HttpFoundation] implement lazy connect for pdo session handler

This commit is contained in:
Tobias Schultze 2014-05-18 03:41:33 +02:00
parent 7dad54ca08
commit 251238d9a6
2 changed files with 127 additions and 20 deletions

View File

@ -37,10 +37,15 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
class PdoSessionHandler implements \SessionHandlerInterface
{
/**
* @var \PDO PDO instance
* @var \PDO|null PDO instance or null when not connected yet
*/
private $pdo;
/**
* @var string|null|false DNS string or null for session.save_path or false when lazy connection disabled
*/
private $dns = false;
/**
* @var string Database driver
*/
@ -66,6 +71,21 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/
private $timeCol;
/**
* @var string Username when lazy-connect
*/
private $username;
/**
* @var string Password when lazy-connect
*/
private $password;
/**
* @var array Connection options when lazy-connect
*/
private $connectionOptions = array();
/**
* @var bool Whether a transaction is active
*/
@ -79,37 +99,54 @@ class PdoSessionHandler implements \SessionHandlerInterface
/**
* Constructor.
*
* You can either pass an existing database connection as PDO instance or
* pass a DNS string that will be used to lazy-connect to the database
* when the session is actually used. Furthermore it's possible to pass null
* which will then use the session.save_path ini setting as PDO DNS parameter.
*
* List of available options:
* * db_table: The name of the table [default: sessions]
* * db_id_col: The column where to store the session id [default: sess_id]
* * db_data_col: The column where to store the session data [default: sess_data]
* * db_time_col: The column where to store the timestamp [default: sess_time]
* * db_username: The username when lazy-connect [default: '']
* * db_password: The password when lazy-connect [default: '']
* * db_connection_options: An array of driver-specific connection options [default: array()]
*
* @param \PDO $pdo A \PDO instance
* @param array $options An associative array of DB options
* @param \PDO|string|null $pdoOrDns A \PDO instance or DNS string or null
* @param array $options An associative array of DB options
*
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
*/
public function __construct(\PDO $pdo, array $options = array())
public function __construct($pdoOrDns, array $options = array())
{
if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) {
throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__));
if ($pdoOrDns instanceof \PDO) {
if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDns->getAttribute(\PDO::ATTR_ERRMODE)) {
throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__));
}
$this->pdo = $pdoOrDns;
} else {
$this->dns = $pdoOrDns;
}
$this->pdo = $pdo;
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
$options = array_replace(array(
'db_table' => 'sessions',
'db_id_col' => 'sess_id',
'db_data_col' => 'sess_data',
'db_time_col' => 'sess_time',
'db_table' => 'sessions',
'db_id_col' => 'sess_id',
'db_data_col' => 'sess_data',
'db_time_col' => 'sess_time',
'db_username' => '',
'db_password' => '',
'db_connection_options' => array()
), $options);
$this->table = $options['db_table'];
$this->idCol = $options['db_id_col'];
$this->dataCol = $options['db_data_col'];
$this->timeCol = $options['db_time_col'];
$this->username = $options['db_username'];
$this->password = $options['db_password'];
$this->connectionOptions = $options['db_connection_options'];
}
/**
@ -118,6 +155,11 @@ class PdoSessionHandler implements \SessionHandlerInterface
public function open($savePath, $sessionName)
{
$this->gcCalled = false;
if (null === $this->pdo) {
$this->pdo = new \PDO($this->dns ?: $savePath, $this->username, $this->password, $this->connectionOptions);
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
return true;
}
@ -270,6 +312,10 @@ class PdoSessionHandler implements \SessionHandlerInterface
$stmt->execute();
}
if (false !== $this->dns) {
$this->pdo = null;
}
return true;
}

View File

@ -25,7 +25,7 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$this->pdo = new \PDO('sqlite::memory:');
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)';
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_time INTEGER)';
$this->pdo->exec($sql);
}
@ -51,17 +51,74 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$storage->close();
}
public function testWithLazyDnsConnection()
{
$dbFile = tempnam(sys_get_temp_dir(), 'sf2_sqlite_sessions');
if (file_exists($dbFile)) {
@unlink($dbFile);
}
$pdo = new \PDO('sqlite:' . $dbFile);
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data BLOB, sess_time INTEGER)';
$pdo->exec($sql);
$pdo = null;
$storage = new PdoSessionHandler('sqlite:' . $dbFile);
$storage->open('', 'sid');
$data = $storage->read('id');
$storage->write('id', 'data');
$storage->close();
$this->assertSame('', $data, 'New session returns empty string data');
$storage->open('', 'sid');
$data = $storage->read('id');
$storage->close();
$this->assertSame('data', $data, 'Written value can be read back correctly');
@unlink($dbFile);
}
public function testWithLazySavePathConnection()
{
$dbFile = tempnam(sys_get_temp_dir(), 'sf2_sqlite_sessions');
if (file_exists($dbFile)) {
@unlink($dbFile);
}
$pdo = new \PDO('sqlite:' . $dbFile);
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data BLOB, sess_time INTEGER)';
$pdo->exec($sql);
$pdo = null;
// Open is called with what ini_set('session.save_path', 'sqlite:' . $dbFile) would mean
$storage = new PdoSessionHandler(null);
$storage->open('sqlite:' . $dbFile, 'sid');
$data = $storage->read('id');
$storage->write('id', 'data');
$storage->close();
$this->assertSame('', $data, 'New session returns empty string data');
$storage->open('sqlite:' . $dbFile, 'sid');
$data = $storage->read('id');
$storage->close();
$this->assertSame('data', $data, 'Written value can be read back correctly');
@unlink($dbFile);
}
public function testReadWriteRead()
{
$storage = new PdoSessionHandler($this->pdo);
$storage->open('', 'sid');
$this->assertSame('', $storage->read('id'), 'New session returns empty string data');
$data = $storage->read('id');
$storage->write('id', 'data');
$storage->close();
$this->assertSame('', $data, 'New session returns empty string data');
$storage->open('', 'sid');
$this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly');
$data = $storage->read('id');
$storage->close();
$this->assertSame('data', $data, 'Written value can be read back correctly');
}
/**
@ -77,8 +134,9 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$storage->close();
$storage->open('', 'sid');
$this->assertSame('data_of_new_session_id', $storage->read('new_id'), 'Data of regenerated session id is available');
$data = $storage->read('new_id');
$storage->close();
$this->assertSame('data_of_new_session_id', $data, 'Data of regenerated session id is available');
}
/**
@ -109,8 +167,9 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$storage->open('', 'sid');
$this->assertSame('', $storage->read('id'), 'Destroyed session returns empty string');
$data = $storage->read('id');
$storage->close();
$this->assertSame('', $data, 'Destroyed session returns empty string');
}
public function testSessionGC()
@ -125,12 +184,14 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$storage->open('', 'sid');
$this->assertSame('', $storage->read('id'), 'Session already considered garbage, so not returning data even if it is not pruned yet');
$data = $storage->read('id');
$storage->gc(0);
$storage->close();
$this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
ini_set('session.gc_maxlifetime', $previousLifeTime);
$this->assertSame('', $data, 'Session already considered garbage, so not returning data even if it is not pruned yet');
$this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
}
public function testGetConnection()