[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 class PdoSessionHandler implements \SessionHandlerInterface
{ {
/** /**
* @var \PDO PDO instance * @var \PDO|null PDO instance or null when not connected yet
*/ */
private $pdo; 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 * @var string Database driver
*/ */
@ -66,6 +71,21 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/ */
private $timeCol; 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 * @var bool Whether a transaction is active
*/ */
@ -79,37 +99,54 @@ class PdoSessionHandler implements \SessionHandlerInterface
/** /**
* Constructor. * 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: * List of available options:
* * db_table: The name of the table [default: sessions] * * db_table: The name of the table [default: sessions]
* * db_id_col: The column where to store the session id [default: sess_id] * * 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_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_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 \PDO|string|null $pdoOrDns A \PDO instance or DNS string or null
* @param array $options An associative array of DB options * @param array $options An associative array of DB options
* *
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION * @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)) { if ($pdoOrDns instanceof \PDO) {
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 (\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( $options = array_replace(array(
'db_table' => 'sessions', 'db_table' => 'sessions',
'db_id_col' => 'sess_id', 'db_id_col' => 'sess_id',
'db_data_col' => 'sess_data', 'db_data_col' => 'sess_data',
'db_time_col' => 'sess_time', 'db_time_col' => 'sess_time',
'db_username' => '',
'db_password' => '',
'db_connection_options' => array()
), $options); ), $options);
$this->table = $options['db_table']; $this->table = $options['db_table'];
$this->idCol = $options['db_id_col']; $this->idCol = $options['db_id_col'];
$this->dataCol = $options['db_data_col']; $this->dataCol = $options['db_data_col'];
$this->timeCol = $options['db_time_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) public function open($savePath, $sessionName)
{ {
$this->gcCalled = false; $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; return true;
} }
@ -270,6 +312,10 @@ class PdoSessionHandler implements \SessionHandlerInterface
$stmt->execute(); $stmt->execute();
} }
if (false !== $this->dns) {
$this->pdo = null;
}
return true; return true;
} }

View File

@ -25,7 +25,7 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$this->pdo = new \PDO('sqlite::memory:'); $this->pdo = new \PDO('sqlite::memory:');
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $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); $this->pdo->exec($sql);
} }
@ -51,17 +51,74 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$storage->close(); $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() public function testReadWriteRead()
{ {
$storage = new PdoSessionHandler($this->pdo); $storage = new PdoSessionHandler($this->pdo);
$storage->open('', 'sid'); $storage->open('', 'sid');
$this->assertSame('', $storage->read('id'), 'New session returns empty string data'); $data = $storage->read('id');
$storage->write('id', 'data'); $storage->write('id', 'data');
$storage->close(); $storage->close();
$this->assertSame('', $data, 'New session returns empty string data');
$storage->open('', 'sid'); $storage->open('', 'sid');
$this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly'); $data = $storage->read('id');
$storage->close(); $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->close();
$storage->open('', 'sid'); $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(); $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()); $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$storage->open('', 'sid'); $storage->open('', 'sid');
$this->assertSame('', $storage->read('id'), 'Destroyed session returns empty string'); $data = $storage->read('id');
$storage->close(); $storage->close();
$this->assertSame('', $data, 'Destroyed session returns empty string');
} }
public function testSessionGC() public function testSessionGC()
@ -125,12 +184,14 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$storage->open('', 'sid'); $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->gc(0);
$storage->close(); $storage->close();
$this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
ini_set('session.gc_maxlifetime', $previousLifeTime); 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() public function testGetConnection()