Skip to content

Commit

Permalink
[HttpFoundation] implement lazy connect for pdo session handler
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobion committed Sep 29, 2014
1 parent 7dad54c commit 251238d
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 20 deletions.
Expand Up @@ -37,10 +37,15 @@
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
*/
Expand All @@ -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
*/
Expand All @@ -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 = $pdo;
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
$this->pdo = $pdoOrDns;
} else {
$this->dns = $pdoOrDns;
}

$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'];
}

/**
Expand All @@ -118,6 +155,11 @@ public function __construct(\PDO $pdo, array $options = array())
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;
}
Expand Down Expand Up @@ -270,6 +312,10 @@ public function close()
$stmt->execute();
}

if (false !== $this->dns) {
$this->pdo = null;
}

return true;
}

Expand Down
Expand Up @@ -25,7 +25,7 @@ protected function setUp()

$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);
}

Expand All @@ -51,17 +51,74 @@ public function testInexistentTable()
$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');
}

/**
Expand All @@ -77,8 +134,9 @@ public function testWriteDifferentSessionIdThanRead()
$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');
}

/**
Expand Down Expand Up @@ -109,8 +167,9 @@ public function testSessionDestroy()
$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()
Expand All @@ -125,12 +184,14 @@ public function testSessionGC()
$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()
Expand Down

0 comments on commit 251238d

Please sign in to comment.