2015-08-18 00:00:26 +00:00
< ? 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 ;
/**
* Session handler using a PDO connection to read and write data .
*
* It works with MySQL , PostgreSQL , Oracle , SQL Server and SQLite and implements
* different locking strategies to handle concurrent access to the same session .
* Locking is necessary to prevent loss of data due to race conditions and to keep
* the session data consistent between read () and write () . With locking , requests
* for the same session will wait until the other one finished writing . For this
* reason it ' s best practice to close a session as early as possible to improve
* concurrency . PHPs internal files session handler also implements locking .
*
* Attention : Since SQLite does not support row level locks but locks the whole database ,
* it means only one session can be accessed at a time . Even different sessions would wait
* for another to finish . So saving session in SQLite should only be considered for
* development or prototypes .
*
* Session data is a binary string that can contain non - printable characters like the null byte .
* For this reason it must be saved in a binary column in the database like BLOB in MySQL .
* Saving it in a character column could corrupt the data . You can use createTable ()
* to initialize a correctly defined table .
*
* @ see http :// php . net / sessionhandlerinterface
*
* @ author Fabien Potencier < fabien @ symfony . com >
* @ author Michael Williams < michael . williams @ funsational . com >
* @ author Tobias Schultze < http :// tobion . de >
*/
class PdoSessionHandler implements \SessionHandlerInterface
{
/**
* No locking is done . This means sessions are prone to loss of data due to
* race conditions of concurrent requests to the same session . The last session
* write will win in this case . It might be useful when you implement your own
* logic to deal with this like an optimistic approach .
*/
const LOCK_NONE = 0 ;
/**
* Creates an application - level lock on a session . The disadvantage is that the
* lock is not enforced by the database and thus other , unaware parts of the
* application could still concurrently modify the session . The advantage is it
* does not require a transaction .
* This mode is not available for SQLite and not yet implemented for oci and sqlsrv .
*/
const LOCK_ADVISORY = 1 ;
/**
* Issues a real row lock . Since it uses a transaction between opening and
* closing a session , you have to be careful when you use same database connection
* that you also use for your application logic . This mode is the default because
* it ' s the only reliable solution across DBMSs .
*/
const LOCK_TRANSACTIONAL = 2 ;
/**
* @ var \PDO | null PDO instance or null when not connected yet
*/
private $pdo ;
/**
* @ var string | null | false DSN string or null for session . save_path or false when lazy connection disabled
*/
private $dsn = false ;
/**
* @ var string Database driver
*/
private $driver ;
/**
* @ var string Table name
*/
private $table = 'sessions' ;
/**
* @ var string Column for session id
*/
private $idCol = 'sess_id' ;
/**
* @ var string Column for session data
*/
private $dataCol = 'sess_data' ;
/**
* @ var string Column for lifetime
*/
private $lifetimeCol = 'sess_lifetime' ;
/**
* @ var string Column for timestamp
*/
private $timeCol = 'sess_time' ;
/**
* @ 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 int The strategy for locking , see constants
*/
private $lockMode = self :: LOCK_TRANSACTIONAL ;
/**
2015-08-27 19:03:05 +00:00
* It ' s an array to support multiple reads before closing which is manual , non - standard usage .
2015-08-18 00:00:26 +00:00
*
* @ var \PDOStatement [] An array of statements to release advisory locks
*/
private $unlockStatements = array ();
/**
* @ var bool True when the current session exists but expired according to session . gc_maxlifetime
*/
private $sessionExpired = false ;
/**
* @ var bool Whether a transaction is active
*/
private $inTransaction = false ;
/**
* @ var bool Whether gc () has been called
*/
private $gcCalled = false ;
/**
* Constructor .
*
* You can either pass an existing database connection as PDO instance or
* pass a DSN 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 DSN 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_lifetime_col : The column where to store the lifetime [ default : sess_lifetime ]
* * 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 ()]
* * lock_mode : The strategy for locking , see constants [ default : LOCK_TRANSACTIONAL ]
*
* @ param \PDO | string | null $pdoOrDsn A \PDO instance or DSN string or null
* @ param array $options An associative array of options
*
* @ throws \InvalidArgumentException When PDO error mode is not PDO :: ERRMODE_EXCEPTION
*/
public function __construct ( $pdoOrDsn = null , array $options = array ())
{
if ( $pdoOrDsn instanceof \PDO ) {
if ( \PDO :: ERRMODE_EXCEPTION !== $pdoOrDsn -> 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 = $pdoOrDsn ;
$this -> driver = $this -> pdo -> getAttribute ( \PDO :: ATTR_DRIVER_NAME );
} else {
$this -> dsn = $pdoOrDsn ;
}
$this -> table = isset ( $options [ 'db_table' ]) ? $options [ 'db_table' ] : $this -> table ;
$this -> idCol = isset ( $options [ 'db_id_col' ]) ? $options [ 'db_id_col' ] : $this -> idCol ;
$this -> dataCol = isset ( $options [ 'db_data_col' ]) ? $options [ 'db_data_col' ] : $this -> dataCol ;
$this -> lifetimeCol = isset ( $options [ 'db_lifetime_col' ]) ? $options [ 'db_lifetime_col' ] : $this -> lifetimeCol ;
$this -> timeCol = isset ( $options [ 'db_time_col' ]) ? $options [ 'db_time_col' ] : $this -> timeCol ;
$this -> username = isset ( $options [ 'db_username' ]) ? $options [ 'db_username' ] : $this -> username ;
$this -> password = isset ( $options [ 'db_password' ]) ? $options [ 'db_password' ] : $this -> password ;
$this -> connectionOptions = isset ( $options [ 'db_connection_options' ]) ? $options [ 'db_connection_options' ] : $this -> connectionOptions ;
$this -> lockMode = isset ( $options [ 'lock_mode' ]) ? $options [ 'lock_mode' ] : $this -> lockMode ;
}
/**
* Creates the table to store sessions which can be called once for setup .
*
* Session ID is saved in a column of maximum length 128 because that is enough even
* for a 512 bit configured session . hash_function like Whirlpool . Session data is
* saved in a BLOB . One could also use a shorter inlined varbinary column
* if one was sure the data fits into it .
*
* @ throws \PDOException When the table already exists
* @ throws \DomainException When an unsupported PDO driver is used
*/
public function createTable ()
{
// connect if we are not yet
$this -> getConnection ();
switch ( $this -> driver ) {
case 'mysql' :
// We use varbinary for the ID column because it prevents unwanted conversions:
// - character set conversions between server and client
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
$sql = " CREATE TABLE $this->table ( $this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB " ;
break ;
case 'sqlite' :
$sql = " CREATE TABLE $this->table ( $this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL) " ;
break ;
case 'pgsql' :
$sql = " CREATE TABLE $this->table ( $this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL) " ;
break ;
case 'oci' :
$sql = " CREATE TABLE $this->table ( $this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL) " ;
break ;
case 'sqlsrv' :
$sql = " CREATE TABLE $this->table ( $this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL) " ;
break ;
default :
throw new \DomainException ( sprintf ( 'Creating the session table is currently not implemented for PDO driver "%s".' , $this -> driver ));
}
try {
$this -> pdo -> exec ( $sql );
} catch ( \PDOException $e ) {
$this -> rollback ();
throw $e ;
}
}
/**
* Returns true when the current session exists but expired according to session . gc_maxlifetime .
*
* Can be used to distinguish between a new session and one that expired due to inactivity .
*
* @ return bool Whether current session expired
*/
public function isSessionExpired ()
{
return $this -> sessionExpired ;
}
/**
* { @ inheritdoc }
*/
public function open ( $savePath , $sessionName )
{
if ( null === $this -> pdo ) {
$this -> connect ( $this -> dsn ? : $savePath );
}
return true ;
}
/**
* { @ inheritdoc }
*/
public function read ( $sessionId )
{
try {
return $this -> doRead ( $sessionId );
} catch ( \PDOException $e ) {
$this -> rollback ();
throw $e ;
}
}
/**
* { @ inheritdoc }
*/
public function gc ( $maxlifetime )
{
// We delay gc() to close() so that it is executed outside the transactional and blocking read-write process.
// This way, pruning expired sessions does not block them from being started while the current session is used.
$this -> gcCalled = true ;
return true ;
}
/**
* { @ inheritdoc }
*/
public function destroy ( $sessionId )
{
// delete the record associated with this id
$sql = " DELETE FROM $this->table WHERE $this->idCol = :id " ;
try {
$stmt = $this -> pdo -> prepare ( $sql );
$stmt -> bindParam ( ':id' , $sessionId , \PDO :: PARAM_STR );
$stmt -> execute ();
} catch ( \PDOException $e ) {
$this -> rollback ();
throw $e ;
}
return true ;
}
/**
* { @ inheritdoc }
*/
public function write ( $sessionId , $data )
{
$maxlifetime = ( int ) ini_get ( 'session.gc_maxlifetime' );
try {
// We use a single MERGE SQL query when supported by the database.
$mergeSql = $this -> getMergeSql ();
if ( null !== $mergeSql ) {
$mergeStmt = $this -> pdo -> prepare ( $mergeSql );
$mergeStmt -> bindParam ( ':id' , $sessionId , \PDO :: PARAM_STR );
$mergeStmt -> bindParam ( ':data' , $data , \PDO :: PARAM_LOB );
$mergeStmt -> bindParam ( ':lifetime' , $maxlifetime , \PDO :: PARAM_INT );
$mergeStmt -> bindValue ( ':time' , time (), \PDO :: PARAM_INT );
$mergeStmt -> execute ();
return true ;
}
$updateStmt = $this -> pdo -> prepare (
" UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id "
);
$updateStmt -> bindParam ( ':id' , $sessionId , \PDO :: PARAM_STR );
$updateStmt -> bindParam ( ':data' , $data , \PDO :: PARAM_LOB );
$updateStmt -> bindParam ( ':lifetime' , $maxlifetime , \PDO :: PARAM_INT );
$updateStmt -> bindValue ( ':time' , time (), \PDO :: PARAM_INT );
$updateStmt -> execute ();
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
// duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior).
// We can just catch such an error and re-execute the update. This is similar to a serializable
// transaction with retry logic on serialization failures but without the overhead and without possible
// false positives due to longer gap locking.
if ( ! $updateStmt -> rowCount ()) {
try {
$insertStmt = $this -> pdo -> prepare (
" INSERT INTO $this->table ( $this->idCol , $this->dataCol , $this->lifetimeCol , $this->timeCol ) VALUES (:id, :data, :lifetime, :time) "
);
$insertStmt -> bindParam ( ':id' , $sessionId , \PDO :: PARAM_STR );
$insertStmt -> bindParam ( ':data' , $data , \PDO :: PARAM_LOB );
$insertStmt -> bindParam ( ':lifetime' , $maxlifetime , \PDO :: PARAM_INT );
$insertStmt -> bindValue ( ':time' , time (), \PDO :: PARAM_INT );
$insertStmt -> execute ();
} catch ( \PDOException $e ) {
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
if ( 0 === strpos ( $e -> getCode (), '23' )) {
$updateStmt -> execute ();
} else {
throw $e ;
}
}
}
} catch ( \PDOException $e ) {
$this -> rollback ();
throw $e ;
}
return true ;
}
/**
* { @ inheritdoc }
*/
public function close ()
{
$this -> commit ();
while ( $unlockStmt = array_shift ( $this -> unlockStatements )) {
$unlockStmt -> execute ();
}
if ( $this -> gcCalled ) {
$this -> gcCalled = false ;
// delete the session records that have expired
$sql = " DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time " ;
$stmt = $this -> pdo -> prepare ( $sql );
$stmt -> bindValue ( ':time' , time (), \PDO :: PARAM_INT );
$stmt -> execute ();
}
if ( false !== $this -> dsn ) {
$this -> pdo = null ; // only close lazy-connection
}
return true ;
}
/**
* Lazy - connects to the database .
*
* @ param string $dsn DSN string
*/
private function connect ( $dsn )
{
$this -> pdo = new \PDO ( $dsn , $this -> username , $this -> password , $this -> connectionOptions );
$this -> pdo -> setAttribute ( \PDO :: ATTR_ERRMODE , \PDO :: ERRMODE_EXCEPTION );
$this -> driver = $this -> pdo -> getAttribute ( \PDO :: ATTR_DRIVER_NAME );
}
/**
* Helper method to begin a transaction .
*
* Since SQLite does not support row level locks , we have to acquire a reserved lock
* on the database immediately . Because of https :// bugs . php . net / 42766 we have to create
* such a transaction manually which also means we cannot use PDO :: commit or
* PDO :: rollback or PDO :: inTransaction for SQLite .
*
* Also MySQLs default isolation , REPEATABLE READ , causes deadlock for different sessions
* due to http :// www . mysqlperformanceblog . com / 2013 / 12 / 12 / one - more - innodb - gap - lock - to - avoid / .
* So we change it to READ COMMITTED .
*/
private function beginTransaction ()
{
if ( ! $this -> inTransaction ) {
if ( 'sqlite' === $this -> driver ) {
$this -> pdo -> exec ( 'BEGIN IMMEDIATE TRANSACTION' );
} else {
if ( 'mysql' === $this -> driver ) {
$this -> pdo -> exec ( 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED' );
}
$this -> pdo -> beginTransaction ();
}
$this -> inTransaction = true ;
}
}
/**
* Helper method to commit a transaction .
*/
private function commit ()
{
if ( $this -> inTransaction ) {
try {
// commit read-write transaction which also releases the lock
if ( 'sqlite' === $this -> driver ) {
$this -> pdo -> exec ( 'COMMIT' );
} else {
$this -> pdo -> commit ();
}
$this -> inTransaction = false ;
} catch ( \PDOException $e ) {
$this -> rollback ();
throw $e ;
}
}
}
/**
* Helper method to rollback a transaction .
*/
private function rollback ()
{
// We only need to rollback if we are in a transaction. Otherwise the resulting
// error would hide the real problem why rollback was called. We might not be
// in a transaction when not using the transactional locking behavior or when
// two callbacks (e.g. destroy and write) are invoked that both fail.
if ( $this -> inTransaction ) {
if ( 'sqlite' === $this -> driver ) {
$this -> pdo -> exec ( 'ROLLBACK' );
} else {
2015-08-27 19:03:05 +00:00
$this -> pdo -> rollBack ();
2015-08-18 00:00:26 +00:00
}
$this -> inTransaction = false ;
}
}
/**
* Reads the session data in respect to the different locking strategies .
*
* We need to make sure we do not return session data that is already considered garbage according
* to the session . gc_maxlifetime setting because gc () is called after read () and only sometimes .
*
* @ param string $sessionId Session ID
*
* @ return string The session data
*/
private function doRead ( $sessionId )
{
$this -> sessionExpired = false ;
if ( self :: LOCK_ADVISORY === $this -> lockMode ) {
$this -> unlockStatements [] = $this -> doAdvisoryLock ( $sessionId );
}
$selectSql = $this -> getSelectSql ();
$selectStmt = $this -> pdo -> prepare ( $selectSql );
$selectStmt -> bindParam ( ':id' , $sessionId , \PDO :: PARAM_STR );
$selectStmt -> execute ();
$sessionRows = $selectStmt -> fetchAll ( \PDO :: FETCH_NUM );
if ( $sessionRows ) {
if ( $sessionRows [ 0 ][ 1 ] + $sessionRows [ 0 ][ 2 ] < time ()) {
$this -> sessionExpired = true ;
return '' ;
}
return is_resource ( $sessionRows [ 0 ][ 0 ]) ? stream_get_contents ( $sessionRows [ 0 ][ 0 ]) : $sessionRows [ 0 ][ 0 ];
}
if ( self :: LOCK_TRANSACTIONAL === $this -> lockMode && 'sqlite' !== $this -> driver ) {
// Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
// until other connections to the session are committed.
try {
$insertStmt = $this -> pdo -> prepare (
" INSERT INTO $this->table ( $this->idCol , $this->dataCol , $this->lifetimeCol , $this->timeCol ) VALUES (:id, :data, :lifetime, :time) "
);
$insertStmt -> bindParam ( ':id' , $sessionId , \PDO :: PARAM_STR );
$insertStmt -> bindValue ( ':data' , '' , \PDO :: PARAM_LOB );
$insertStmt -> bindValue ( ':lifetime' , 0 , \PDO :: PARAM_INT );
$insertStmt -> bindValue ( ':time' , time (), \PDO :: PARAM_INT );
$insertStmt -> execute ();
} catch ( \PDOException $e ) {
// Catch duplicate key error because other connection created the session already.
// It would only not be the case when the other connection destroyed the session.
if ( 0 === strpos ( $e -> getCode (), '23' )) {
// Retrieve finished session data written by concurrent connection. SELECT
// FOR UPDATE is necessary to avoid deadlock of connection that starts reading
// before we write (transform intention to real lock).
$selectStmt -> execute ();
$sessionRows = $selectStmt -> fetchAll ( \PDO :: FETCH_NUM );
if ( $sessionRows ) {
return is_resource ( $sessionRows [ 0 ][ 0 ]) ? stream_get_contents ( $sessionRows [ 0 ][ 0 ]) : $sessionRows [ 0 ][ 0 ];
}
return '' ;
}
throw $e ;
}
}
return '' ;
}
/**
* Executes an application - level lock on the database .
*
* @ param string $sessionId Session ID
*
* @ return \PDOStatement The statement that needs to be executed later to release the lock
*
* @ throws \DomainException When an unsupported PDO driver is used
*
* @ todo implement missing advisory locks
* - for oci using DBMS_LOCK . REQUEST
* - for sqlsrv using sp_getapplock with LockOwner = Session
*/
private function doAdvisoryLock ( $sessionId )
{
switch ( $this -> driver ) {
case 'mysql' :
// should we handle the return value? 0 on timeout, null on error
// we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
$stmt = $this -> pdo -> prepare ( 'SELECT GET_LOCK(:key, 50)' );
$stmt -> bindValue ( ':key' , $sessionId , \PDO :: PARAM_STR );
$stmt -> execute ();
$releaseStmt = $this -> pdo -> prepare ( 'DO RELEASE_LOCK(:key)' );
$releaseStmt -> bindValue ( ':key' , $sessionId , \PDO :: PARAM_STR );
return $releaseStmt ;
case 'pgsql' :
// Obtaining an exclusive session level advisory lock requires an integer key.
// So we convert the HEX representation of the session id to an integer.
// Since integers are signed, we have to skip one hex char to fit in the range.
if ( 4 === PHP_INT_SIZE ) {
$sessionInt1 = hexdec ( substr ( $sessionId , 0 , 7 ));
$sessionInt2 = hexdec ( substr ( $sessionId , 7 , 7 ));
$stmt = $this -> pdo -> prepare ( 'SELECT pg_advisory_lock(:key1, :key2)' );
$stmt -> bindValue ( ':key1' , $sessionInt1 , \PDO :: PARAM_INT );
$stmt -> bindValue ( ':key2' , $sessionInt2 , \PDO :: PARAM_INT );
$stmt -> execute ();
$releaseStmt = $this -> pdo -> prepare ( 'SELECT pg_advisory_unlock(:key1, :key2)' );
$releaseStmt -> bindValue ( ':key1' , $sessionInt1 , \PDO :: PARAM_INT );
$releaseStmt -> bindValue ( ':key2' , $sessionInt2 , \PDO :: PARAM_INT );
} else {
$sessionBigInt = hexdec ( substr ( $sessionId , 0 , 15 ));
$stmt = $this -> pdo -> prepare ( 'SELECT pg_advisory_lock(:key)' );
$stmt -> bindValue ( ':key' , $sessionBigInt , \PDO :: PARAM_INT );
$stmt -> execute ();
$releaseStmt = $this -> pdo -> prepare ( 'SELECT pg_advisory_unlock(:key)' );
$releaseStmt -> bindValue ( ':key' , $sessionBigInt , \PDO :: PARAM_INT );
}
return $releaseStmt ;
case 'sqlite' :
throw new \DomainException ( 'SQLite does not support advisory locks.' );
default :
throw new \DomainException ( sprintf ( 'Advisory locks are currently not implemented for PDO driver "%s".' , $this -> driver ));
}
}
/**
* Return a locking or nonlocking SQL query to read session information .
*
* @ return string The SQL string
*
* @ throws \DomainException When an unsupported PDO driver is used
*/
private function getSelectSql ()
{
if ( self :: LOCK_TRANSACTIONAL === $this -> lockMode ) {
$this -> beginTransaction ();
switch ( $this -> driver ) {
case 'mysql' :
case 'oci' :
case 'pgsql' :
return " SELECT $this->dataCol , $this->lifetimeCol , $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE " ;
case 'sqlsrv' :
return " SELECT $this->dataCol , $this->lifetimeCol , $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id " ;
case 'sqlite' :
// we already locked when starting transaction
break ;
default :
throw new \DomainException ( sprintf ( 'Transactional locks are currently not implemented for PDO driver "%s".' , $this -> driver ));
}
}
return " SELECT $this->dataCol , $this->lifetimeCol , $this->timeCol FROM $this->table WHERE $this->idCol = :id " ;
}
/**
* Returns a merge / upsert ( i . e . insert or update ) SQL query when supported by the database for writing session data .
*
* @ return string | null The SQL string or null when not supported
*/
private function getMergeSql ()
{
switch ( $this -> driver ) {
case 'mysql' :
return " INSERT INTO $this->table ( $this->idCol , $this->dataCol , $this->lifetimeCol , $this->timeCol ) VALUES (:id, :data, :lifetime, :time) " .
" ON DUPLICATE KEY UPDATE $this->dataCol = VALUES( $this->dataCol ), $this->lifetimeCol = VALUES( $this->lifetimeCol ), $this->timeCol = VALUES( $this->timeCol ) " ;
case 'oci' :
// DUAL is Oracle specific dummy table
return " MERGE INTO $this->table USING DUAL ON ( $this->idCol = :id) " .
" WHEN NOT MATCHED THEN INSERT ( $this->idCol , $this->dataCol , $this->lifetimeCol , $this->timeCol ) VALUES (:id, :data, :lifetime, :time) " .
" WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time " ;
case 'sqlsrv' === $this -> driver && version_compare ( $this -> pdo -> getAttribute ( \PDO :: ATTR_SERVER_VERSION ), '10' , '>=' ) :
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
return " MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ( $this->idCol = :id) " .
" WHEN NOT MATCHED THEN INSERT ( $this->idCol , $this->dataCol , $this->lifetimeCol , $this->timeCol ) VALUES (:id, :data, :lifetime, :time) " .
" WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time; " ;
case 'sqlite' :
return " INSERT OR REPLACE INTO $this->table ( $this->idCol , $this->dataCol , $this->lifetimeCol , $this->timeCol ) VALUES (:id, :data, :lifetime, :time) " ;
}
}
/**
2015-08-27 19:03:05 +00:00
* Return a PDO instance .
2015-08-18 00:00:26 +00:00
*
* @ return \PDO
*/
protected function getConnection ()
{
if ( null === $this -> pdo ) {
$this -> connect ( $this -> dsn ? : ini_get ( 'session.save_path' ));
}
return $this -> pdo ;
}
}