Verzeichnisstruktur phpBB-3.2.0


Veröffentlicht
06.01.2017

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

PdoSessionHandler.php

Zuletzt modifiziert: 09.10.2024, 12:58 - Dateigröße: 29.20 KiB


 001  <?php
 002   
 003  /*
 004   * This file is part of the Symfony package.
 005   *
 006   * (c) Fabien Potencier <fabien@symfony.com>
 007   *
 008   * For the full copyright and license information, please view the LICENSE
 009   * file that was distributed with this source code.
 010   */
 011   
 012  namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
 013   
 014  /**
 015   * Session handler using a PDO connection to read and write data.
 016   *
 017   * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements
 018   * different locking strategies to handle concurrent access to the same session.
 019   * Locking is necessary to prevent loss of data due to race conditions and to keep
 020   * the session data consistent between read() and write(). With locking, requests
 021   * for the same session will wait until the other one finished writing. For this
 022   * reason it's best practice to close a session as early as possible to improve
 023   * concurrency. PHPs internal files session handler also implements locking.
 024   *
 025   * Attention: Since SQLite does not support row level locks but locks the whole database,
 026   * it means only one session can be accessed at a time. Even different sessions would wait
 027   * for another to finish. So saving session in SQLite should only be considered for
 028   * development or prototypes.
 029   *
 030   * Session data is a binary string that can contain non-printable characters like the null byte.
 031   * For this reason it must be saved in a binary column in the database like BLOB in MySQL.
 032   * Saving it in a character column could corrupt the data. You can use createTable()
 033   * to initialize a correctly defined table.
 034   *
 035   * @see http://php.net/sessionhandlerinterface
 036   *
 037   * @author Fabien Potencier <fabien@symfony.com>
 038   * @author Michael Williams <michael.williams@funsational.com>
 039   * @author Tobias Schultze <http://tobion.de>
 040   */
 041  class PdoSessionHandler implements \SessionHandlerInterface
 042  {
 043      /**
 044       * No locking is done. This means sessions are prone to loss of data due to
 045       * race conditions of concurrent requests to the same session. The last session
 046       * write will win in this case. It might be useful when you implement your own
 047       * logic to deal with this like an optimistic approach.
 048       */
 049      const LOCK_NONE = 0;
 050   
 051      /**
 052       * Creates an application-level lock on a session. The disadvantage is that the
 053       * lock is not enforced by the database and thus other, unaware parts of the
 054       * application could still concurrently modify the session. The advantage is it
 055       * does not require a transaction.
 056       * This mode is not available for SQLite and not yet implemented for oci and sqlsrv.
 057       */
 058      const LOCK_ADVISORY = 1;
 059   
 060      /**
 061       * Issues a real row lock. Since it uses a transaction between opening and
 062       * closing a session, you have to be careful when you use same database connection
 063       * that you also use for your application logic. This mode is the default because
 064       * it's the only reliable solution across DBMSs.
 065       */
 066      const LOCK_TRANSACTIONAL = 2;
 067   
 068      /**
 069       * @var \PDO|null PDO instance or null when not connected yet
 070       */
 071      private $pdo;
 072   
 073      /**
 074       * @var string|null|false DSN string or null for session.save_path or false when lazy connection disabled
 075       */
 076      private $dsn = false;
 077   
 078      /**
 079       * @var string Database driver
 080       */
 081      private $driver;
 082   
 083      /**
 084       * @var string Table name
 085       */
 086      private $table = 'sessions';
 087   
 088      /**
 089       * @var string Column for session id
 090       */
 091      private $idCol = 'sess_id';
 092   
 093      /**
 094       * @var string Column for session data
 095       */
 096      private $dataCol = 'sess_data';
 097   
 098      /**
 099       * @var string Column for lifetime
 100       */
 101      private $lifetimeCol = 'sess_lifetime';
 102   
 103      /**
 104       * @var string Column for timestamp
 105       */
 106      private $timeCol = 'sess_time';
 107   
 108      /**
 109       * @var string Username when lazy-connect
 110       */
 111      private $username = '';
 112   
 113      /**
 114       * @var string Password when lazy-connect
 115       */
 116      private $password = '';
 117   
 118      /**
 119       * @var array Connection options when lazy-connect
 120       */
 121      private $connectionOptions = array();
 122   
 123      /**
 124       * @var int The strategy for locking, see constants
 125       */
 126      private $lockMode = self::LOCK_TRANSACTIONAL;
 127   
 128      /**
 129       * It's an array to support multiple reads before closing which is manual, non-standard usage.
 130       *
 131       * @var \PDOStatement[] An array of statements to release advisory locks
 132       */
 133      private $unlockStatements = array();
 134   
 135      /**
 136       * @var bool True when the current session exists but expired according to session.gc_maxlifetime
 137       */
 138      private $sessionExpired = false;
 139   
 140      /**
 141       * @var bool Whether a transaction is active
 142       */
 143      private $inTransaction = false;
 144   
 145      /**
 146       * @var bool Whether gc() has been called
 147       */
 148      private $gcCalled = false;
 149   
 150      /**
 151       * Constructor.
 152       *
 153       * You can either pass an existing database connection as PDO instance or
 154       * pass a DSN string that will be used to lazy-connect to the database
 155       * when the session is actually used. Furthermore it's possible to pass null
 156       * which will then use the session.save_path ini setting as PDO DSN parameter.
 157       *
 158       * List of available options:
 159       *  * db_table: The name of the table [default: sessions]
 160       *  * db_id_col: The column where to store the session id [default: sess_id]
 161       *  * db_data_col: The column where to store the session data [default: sess_data]
 162       *  * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime]
 163       *  * db_time_col: The column where to store the timestamp [default: sess_time]
 164       *  * db_username: The username when lazy-connect [default: '']
 165       *  * db_password: The password when lazy-connect [default: '']
 166       *  * db_connection_options: An array of driver-specific connection options [default: array()]
 167       *  * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
 168       *
 169       * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
 170       * @param array            $options  An associative array of options
 171       *
 172       * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
 173       */
 174      public function __construct($pdoOrDsn = null, array $options = array())
 175      {
 176          if ($pdoOrDsn instanceof \PDO) {
 177              if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
 178                  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__));
 179              }
 180   
 181              $this->pdo = $pdoOrDsn;
 182              $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
 183          } else {
 184              $this->dsn = $pdoOrDsn;
 185          }
 186   
 187          $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
 188          $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
 189          $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
 190          $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol;
 191          $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol;
 192          $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username;
 193          $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
 194          $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
 195          $this->lockMode = isset($options['lock_mode']) ? $options['lock_mode'] : $this->lockMode;
 196      }
 197   
 198      /**
 199       * Creates the table to store sessions which can be called once for setup.
 200       *
 201       * Session ID is saved in a column of maximum length 128 because that is enough even
 202       * for a 512 bit configured session.hash_function like Whirlpool. Session data is
 203       * saved in a BLOB. One could also use a shorter inlined varbinary column
 204       * if one was sure the data fits into it.
 205       *
 206       * @throws \PDOException    When the table already exists
 207       * @throws \DomainException When an unsupported PDO driver is used
 208       */
 209      public function createTable()
 210      {
 211          // connect if we are not yet
 212          $this->getConnection();
 213   
 214          switch ($this->driver) {
 215              case 'mysql':
 216                  // We use varbinary for the ID column because it prevents unwanted conversions:
 217                  // - character set conversions between server and client
 218                  // - trailing space removal
 219                  // - case-insensitivity
 220                  // - language processing like é == e
 221                  $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";
 222                  break;
 223              case 'sqlite':
 224                  $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)";
 225                  break;
 226              case 'pgsql':
 227                  $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)";
 228                  break;
 229              case 'oci':
 230                  $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)";
 231                  break;
 232              case 'sqlsrv':
 233                  $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)";
 234                  break;
 235              default:
 236                  throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver));
 237          }
 238   
 239          try {
 240              $this->pdo->exec($sql);
 241          } catch (\PDOException $e) {
 242              $this->rollback();
 243   
 244              throw $e;
 245          }
 246      }
 247   
 248      /**
 249       * Returns true when the current session exists but expired according to session.gc_maxlifetime.
 250       *
 251       * Can be used to distinguish between a new session and one that expired due to inactivity.
 252       *
 253       * @return bool Whether current session expired
 254       */
 255      public function isSessionExpired()
 256      {
 257          return $this->sessionExpired;
 258      }
 259   
 260      /**
 261       * {@inheritdoc}
 262       */
 263      public function open($savePath, $sessionName)
 264      {
 265          if (null === $this->pdo) {
 266              $this->connect($this->dsn ?: $savePath);
 267          }
 268   
 269          return true;
 270      }
 271   
 272      /**
 273       * {@inheritdoc}
 274       */
 275      public function read($sessionId)
 276      {
 277          try {
 278              return $this->doRead($sessionId);
 279          } catch (\PDOException $e) {
 280              $this->rollback();
 281   
 282              throw $e;
 283          }
 284      }
 285   
 286      /**
 287       * {@inheritdoc}
 288       */
 289      public function gc($maxlifetime)
 290      {
 291          // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process.
 292          // This way, pruning expired sessions does not block them from being started while the current session is used.
 293          $this->gcCalled = true;
 294   
 295          return true;
 296      }
 297   
 298      /**
 299       * {@inheritdoc}
 300       */
 301      public function destroy($sessionId)
 302      {
 303          // delete the record associated with this id
 304          $sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
 305   
 306          try {
 307              $stmt = $this->pdo->prepare($sql);
 308              $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
 309              $stmt->execute();
 310          } catch (\PDOException $e) {
 311              $this->rollback();
 312   
 313              throw $e;
 314          }
 315   
 316          return true;
 317      }
 318   
 319      /**
 320       * {@inheritdoc}
 321       */
 322      public function write($sessionId, $data)
 323      {
 324          $maxlifetime = (int) ini_get('session.gc_maxlifetime');
 325   
 326          try {
 327              // We use a single MERGE SQL query when supported by the database.
 328              $mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime);
 329              if (null !== $mergeStmt) {
 330                  $mergeStmt->execute();
 331   
 332                  return true;
 333              }
 334   
 335              $updateStmt = $this->pdo->prepare(
 336                  "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
 337              );
 338              $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
 339              $updateStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
 340              $updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
 341              $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
 342              $updateStmt->execute();
 343   
 344              // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in
 345              // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior).
 346              // We can just catch such an error and re-execute the update. This is similar to a serializable
 347              // transaction with retry logic on serialization failures but without the overhead and without possible
 348              // false positives due to longer gap locking.
 349              if (!$updateStmt->rowCount()) {
 350                  try {
 351                      $insertStmt = $this->pdo->prepare(
 352                          "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
 353                      );
 354                      $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
 355                      $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
 356                      $insertStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
 357                      $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
 358                      $insertStmt->execute();
 359                  } catch (\PDOException $e) {
 360                      // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
 361                      if (0 === strpos($e->getCode(), '23')) {
 362                          $updateStmt->execute();
 363                      } else {
 364                          throw $e;
 365                      }
 366                  }
 367              }
 368          } catch (\PDOException $e) {
 369              $this->rollback();
 370   
 371              throw $e;
 372          }
 373   
 374          return true;
 375      }
 376   
 377      /**
 378       * {@inheritdoc}
 379       */
 380      public function close()
 381      {
 382          $this->commit();
 383   
 384          while ($unlockStmt = array_shift($this->unlockStatements)) {
 385              $unlockStmt->execute();
 386          }
 387   
 388          if ($this->gcCalled) {
 389              $this->gcCalled = false;
 390   
 391              // delete the session records that have expired
 392              $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
 393   
 394              $stmt = $this->pdo->prepare($sql);
 395              $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
 396              $stmt->execute();
 397          }
 398   
 399          if (false !== $this->dsn) {
 400              $this->pdo = null; // only close lazy-connection
 401          }
 402   
 403          return true;
 404      }
 405   
 406      /**
 407       * Lazy-connects to the database.
 408       *
 409       * @param string $dsn DSN string
 410       */
 411      private function connect($dsn)
 412      {
 413          $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions);
 414          $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
 415          $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
 416      }
 417   
 418      /**
 419       * Helper method to begin a transaction.
 420       *
 421       * Since SQLite does not support row level locks, we have to acquire a reserved lock
 422       * on the database immediately. Because of https://bugs.php.net/42766 we have to create
 423       * such a transaction manually which also means we cannot use PDO::commit or
 424       * PDO::rollback or PDO::inTransaction for SQLite.
 425       *
 426       * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions
 427       * due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
 428       * So we change it to READ COMMITTED.
 429       */
 430      private function beginTransaction()
 431      {
 432          if (!$this->inTransaction) {
 433              if ('sqlite' === $this->driver) {
 434                  $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION');
 435              } else {
 436                  if ('mysql' === $this->driver) {
 437                      $this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
 438                  }
 439                  $this->pdo->beginTransaction();
 440              }
 441              $this->inTransaction = true;
 442          }
 443      }
 444   
 445      /**
 446       * Helper method to commit a transaction.
 447       */
 448      private function commit()
 449      {
 450          if ($this->inTransaction) {
 451              try {
 452                  // commit read-write transaction which also releases the lock
 453                  if ('sqlite' === $this->driver) {
 454                      $this->pdo->exec('COMMIT');
 455                  } else {
 456                      $this->pdo->commit();
 457                  }
 458                  $this->inTransaction = false;
 459              } catch (\PDOException $e) {
 460                  $this->rollback();
 461   
 462                  throw $e;
 463              }
 464          }
 465      }
 466   
 467      /**
 468       * Helper method to rollback a transaction.
 469       */
 470      private function rollback()
 471      {
 472          // We only need to rollback if we are in a transaction. Otherwise the resulting
 473          // error would hide the real problem why rollback was called. We might not be
 474          // in a transaction when not using the transactional locking behavior or when
 475          // two callbacks (e.g. destroy and write) are invoked that both fail.
 476          if ($this->inTransaction) {
 477              if ('sqlite' === $this->driver) {
 478                  $this->pdo->exec('ROLLBACK');
 479              } else {
 480                  $this->pdo->rollBack();
 481              }
 482              $this->inTransaction = false;
 483          }
 484      }
 485   
 486      /**
 487       * Reads the session data in respect to the different locking strategies.
 488       *
 489       * We need to make sure we do not return session data that is already considered garbage according
 490       * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
 491       *
 492       * @param string $sessionId Session ID
 493       *
 494       * @return string The session data
 495       */
 496      private function doRead($sessionId)
 497      {
 498          $this->sessionExpired = false;
 499   
 500          if (self::LOCK_ADVISORY === $this->lockMode) {
 501              $this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
 502          }
 503   
 504          $selectSql = $this->getSelectSql();
 505          $selectStmt = $this->pdo->prepare($selectSql);
 506          $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
 507   
 508          do {
 509              $selectStmt->execute();
 510              $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM);
 511   
 512              if ($sessionRows) {
 513                  if ($sessionRows[0][1] + $sessionRows[0][2] < time()) {
 514                      $this->sessionExpired = true;
 515   
 516                      return '';
 517                  }
 518   
 519                  return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
 520              }
 521   
 522              if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
 523                  // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
 524                  // until other connections to the session are committed.
 525                  try {
 526                      $insertStmt = $this->pdo->prepare(
 527                          "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
 528                      );
 529                      $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
 530                      $insertStmt->bindValue(':data', '', \PDO::PARAM_LOB);
 531                      $insertStmt->bindValue(':lifetime', 0, \PDO::PARAM_INT);
 532                      $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
 533                      $insertStmt->execute();
 534                  } catch (\PDOException $e) {
 535                      // Catch duplicate key error because other connection created the session already.
 536                      // It would only not be the case when the other connection destroyed the session.
 537                      if (0 === strpos($e->getCode(), '23')) {
 538                          // Retrieve finished session data written by concurrent connection by restarting the loop.
 539                          // We have to start a new transaction as a failed query will mark the current transaction as
 540                          // aborted in PostgreSQL and disallow further queries within it.
 541                          $this->rollback();
 542                          $this->beginTransaction();
 543                          continue;
 544                      }
 545   
 546                      throw $e;
 547                  }
 548              }
 549   
 550              return '';
 551          } while (true);
 552      }
 553   
 554      /**
 555       * Executes an application-level lock on the database.
 556       *
 557       * @param string $sessionId Session ID
 558       *
 559       * @return \PDOStatement The statement that needs to be executed later to release the lock
 560       *
 561       * @throws \DomainException When an unsupported PDO driver is used
 562       *
 563       * @todo implement missing advisory locks
 564       *       - for oci using DBMS_LOCK.REQUEST
 565       *       - for sqlsrv using sp_getapplock with LockOwner = Session
 566       */
 567      private function doAdvisoryLock($sessionId)
 568      {
 569          switch ($this->driver) {
 570              case 'mysql':
 571                  // should we handle the return value? 0 on timeout, null on error
 572                  // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
 573                  $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)');
 574                  $stmt->bindValue(':key', $sessionId, \PDO::PARAM_STR);
 575                  $stmt->execute();
 576   
 577                  $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)');
 578                  $releaseStmt->bindValue(':key', $sessionId, \PDO::PARAM_STR);
 579   
 580                  return $releaseStmt;
 581              case 'pgsql':
 582                  // Obtaining an exclusive session level advisory lock requires an integer key.
 583                  // So we convert the HEX representation of the session id to an integer.
 584                  // Since integers are signed, we have to skip one hex char to fit in the range.
 585                  if (4 === PHP_INT_SIZE) {
 586                      $sessionInt1 = hexdec(substr($sessionId, 0, 7));
 587                      $sessionInt2 = hexdec(substr($sessionId, 7, 7));
 588   
 589                      $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)');
 590                      $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT);
 591                      $stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT);
 592                      $stmt->execute();
 593   
 594                      $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)');
 595                      $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT);
 596                      $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT);
 597                  } else {
 598                      $sessionBigInt = hexdec(substr($sessionId, 0, 15));
 599   
 600                      $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)');
 601                      $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT);
 602                      $stmt->execute();
 603   
 604                      $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)');
 605                      $releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT);
 606                  }
 607   
 608                  return $releaseStmt;
 609              case 'sqlite':
 610                  throw new \DomainException('SQLite does not support advisory locks.');
 611              default:
 612                  throw new \DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver));
 613          }
 614      }
 615   
 616      /**
 617       * Return a locking or nonlocking SQL query to read session information.
 618       *
 619       * @return string The SQL string
 620       *
 621       * @throws \DomainException When an unsupported PDO driver is used
 622       */
 623      private function getSelectSql()
 624      {
 625          if (self::LOCK_TRANSACTIONAL === $this->lockMode) {
 626              $this->beginTransaction();
 627   
 628              switch ($this->driver) {
 629                  case 'mysql':
 630                  case 'oci':
 631                  case 'pgsql':
 632                      return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
 633                  case 'sqlsrv':
 634                      return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id";
 635                  case 'sqlite':
 636                      // we already locked when starting transaction
 637                      break;
 638                  default:
 639                      throw new \DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver));
 640              }
 641          }
 642   
 643          return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id";
 644      }
 645   
 646      /**
 647       * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data.
 648       *
 649       * @param string $sessionId   Session ID
 650       * @param string $data        Encoded session data
 651       * @param int    $maxlifetime session.gc_maxlifetime
 652       *
 653       * @return \PDOStatement|null The merge statement or null when not supported
 654       */
 655      private function getMergeStatement($sessionId, $data, $maxlifetime)
 656      {
 657          $mergeSql = null;
 658          switch (true) {
 659              case 'mysql' === $this->driver:
 660                  $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
 661                      "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
 662                  break;
 663              case 'oci' === $this->driver:
 664                  // DUAL is Oracle specific dummy table
 665                  $mergeSql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
 666                      "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
 667                      "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
 668                  break;
 669              case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
 670                  // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
 671                  // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
 672                  $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
 673                      "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
 674                      "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
 675                  break;
 676              case 'sqlite' === $this->driver:
 677                  $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
 678                  break;
 679              case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='):
 680                  $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
 681                      "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
 682                  break;
 683          }
 684   
 685          if (null !== $mergeSql) {
 686              $mergeStmt = $this->pdo->prepare($mergeSql);
 687   
 688              if ('sqlsrv' === $this->driver || 'oci' === $this->driver) {
 689                  $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
 690                  $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
 691                  $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
 692                  $mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT);
 693                  $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
 694                  $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
 695                  $mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT);
 696                  $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
 697              } else {
 698                  $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
 699                  $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
 700                  $mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
 701                  $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
 702              }
 703   
 704              return $mergeStmt;
 705          }
 706      }
 707   
 708      /**
 709       * Return a PDO instance.
 710       *
 711       * @return \PDO
 712       */
 713      protected function getConnection()
 714      {
 715          if (null === $this->pdo) {
 716              $this->connect($this->dsn ?: ini_get('session.save_path'));
 717          }
 718   
 719          return $this->pdo;
 720      }
 721  }
 722