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