src/AppBundle/Session/LegacySessionHandler.php line 16

Open in your IDE?
  1. <?php
  2. namespace AppBundle\Session;
  3. use Doctrine\DBAL\Connection;
  4. use Doctrine\DBAL\Driver\DriverException;
  5. use Doctrine\DBAL\Platforms\SQLServer2008Platform;
  6. /**
  7.  * Copied from Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandler,
  8.  * but the existing sessions table we need to interface with for compatibility
  9.  * with uses different column names (and these properties are defined as private),
  10.  * the timestamp is a datetime field rather than integer, and the main data column
  11.  * is a blob (no encoding)
  12.  */
  13. class LegacySessionHandler implements \SessionHandlerInterface
  14. {
  15.     /**
  16.      * @var Connection
  17.      */
  18.     private $con;
  19.     /**
  20.      * @var string
  21.      */
  22.     private $table;
  23.     /**
  24.      * @var string Column for session id
  25.      */
  26.     private $idCol 'sessionId';
  27.     /**
  28.      * @var string Column for session data
  29.      */
  30.     private $dataCol 'contents';
  31.     /**
  32.      * @var string Column for timestamp
  33.      */
  34.     private $timeCol 'modify_date';
  35.     /**
  36.      * Constructor.
  37.      *
  38.      * @param Connection $con       A connection
  39.      * @param string     $tableName Table name
  40.      */
  41.     public function __construct(Connection $con$tableName 'sessions')
  42.     {
  43.         $this->con $con;
  44.         $this->table $tableName;
  45.     }
  46.     /**
  47.      * {@inheritdoc}
  48.      */
  49.     public function open($savePath$sessionName): bool
  50.     {
  51.         return true;
  52.     }
  53.     /**
  54.      * {@inheritdoc}
  55.      */
  56.     public function close(): bool
  57.     {
  58.         return true;
  59.     }
  60.     /**
  61.      * {@inheritdoc}
  62.      */
  63.     public function destroy($sessionId): bool
  64.     {
  65.         // delete the record associated with this id
  66.         $sql "DELETE FROM $this->table WHERE $this->idCol = :id";
  67.         try {
  68.             $stmt $this->con->prepare($sql);
  69.             $stmt->bindParam(':id'$sessionId\PDO::PARAM_STR);
  70.             $stmt->execute();
  71.         } catch (\Exception $e) {
  72.             throw new \RuntimeException(sprintf('Exception was thrown when trying to delete a session: %s'$e->getMessage()), 0$e);
  73.         }
  74.         return true;
  75.     }
  76.     /**
  77.      * {@inheritdoc}
  78.      */
  79.     public function gc($maxlifetime): bool
  80.     {
  81.         // delete the session records that have expired
  82.         $sql "DELETE FROM $this->table WHERE $this->timeCol < :time";
  83.         try {
  84.             $stmt $this->con->prepare($sql);
  85.             $stmt->bindValue(':time'date('Y-m-d H:i:s'time() - $maxlifetime), \PDO::PARAM_STR);
  86.             $stmt->execute();
  87.         } catch (\Exception $e) {
  88.             throw new \RuntimeException(sprintf('Exception was thrown when trying to delete expired sessions: %s'$e->getMessage()), 0$e);
  89.         }
  90.         return true;
  91.     }
  92.     /**
  93.      * {@inheritdoc}
  94.      */
  95.     public function read($sessionId): bool
  96.     {
  97.         $sql "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id";
  98.         try {
  99.             $stmt $this->con->prepare($sql);
  100.             $stmt->bindParam(':id'$sessionId\PDO::PARAM_STR);
  101.             $stmt->execute();
  102.             // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed
  103.             $sessionRows $stmt->fetchAll(\PDO::FETCH_NUM);
  104.             if ($sessionRows) {
  105.                 return $sessionRows[0][0];
  106.             }
  107.             return '';
  108.         } catch (\Exception $e) {
  109.             throw new \RuntimeException(sprintf('Exception was thrown when trying to read the session data: %s'$e->getMessage()), 0$e);
  110.         }
  111.     }
  112.     /**
  113.      * {@inheritdoc}
  114.      */
  115.     public function write($sessionId$data): bool
  116.     {
  117.         try {
  118.             // We use a single MERGE SQL query when supported by the database.
  119.             $mergeSql $this->getMergeSql();
  120.             if (null !== $mergeSql) {
  121.                 $mergeStmt $this->con->prepare($mergeSql);
  122.                 $mergeStmt->bindParam(':id'$sessionId\PDO::PARAM_STR);
  123.                 $mergeStmt->bindParam(':data'$data\PDO::PARAM_LOB);
  124.                 $mergeStmt->bindValue(':time'date('Y-m-d H:i:s'), \PDO::PARAM_STR);
  125.                 $mergeStmt->execute();
  126.                 return true;
  127.             }
  128.             $updateStmt $this->con->prepare(
  129.                 "UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id"
  130.             );
  131.             $updateStmt->bindParam(':id'$sessionId\PDO::PARAM_STR);
  132.             $updateStmt->bindParam(':data'$data\PDO::PARAM_LOB);
  133.             $updateStmt->bindValue(':time'date('Y-m-d H:i:s'), \PDO::PARAM_STR);
  134.             $updateStmt->execute();
  135.             // When MERGE is not supported, like in Postgres, we have to use this approach that can result in
  136.             // duplicate key errors when the same session is written simultaneously. We can just catch such an
  137.             // error and re-execute the update. This is similar to a serializable transaction with retry logic
  138.             // on serialization failures but without the overhead and without possible false positives due to
  139.             // longer gap locking.
  140.             if (!$updateStmt->rowCount()) {
  141.                 try {
  142.                     $insertStmt $this->con->prepare(
  143.                         "INSERT INTO $this->table ($this->idCol$this->dataCol$this->timeCol) VALUES (:id, :data, :time)"
  144.                     );
  145.                     $insertStmt->bindParam(':id'$sessionId\PDO::PARAM_STR);
  146.                     $insertStmt->bindParam(':data'$data\PDO::PARAM_LOB);
  147.                     $insertStmt->bindValue(':time'date('Y-m-d H:i:s'), \PDO::PARAM_STR);
  148.                     $insertStmt->execute();
  149.                 } catch (\Exception $e) {
  150.                     $driverException $e->getPrevious();
  151.                     // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
  152.                     // DriverException only available since DBAL 2.5
  153.                     if (
  154.                         ($driverException instanceof DriverException && === strpos($driverException->getSQLState(), '23')) ||
  155.                         ($driverException instanceof \PDOException && === strpos($driverException->getCode(), '23'))
  156.                     ) {
  157.                         $updateStmt->execute();
  158.                     } else {
  159.                         throw $e;
  160.                     }
  161.                 }
  162.             }
  163.         } catch (\Exception $e) {
  164.             throw new \RuntimeException(sprintf('Exception was thrown when trying to write the session data: %s'$e->getMessage()), 0$e);
  165.         }
  166.         return true;
  167.     }
  168.     /**
  169.      * Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database.
  170.      *
  171.      * @return string|null The SQL string or null when not supported
  172.      */
  173.     private function getMergeSql()
  174.     {
  175.         $platform $this->con->getDatabasePlatform()->getName();
  176.         switch ($platform) {
  177.             case 'mysql':
  178.                 return "INSERT INTO $this->table ($this->idCol$this->dataCol$this->timeCol) VALUES (:id, :data, :time) ".
  179.                     "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)";
  180.             case 'oracle':
  181.                 // DUAL is Oracle specific dummy table
  182.                 return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
  183.                     "WHEN NOT MATCHED THEN INSERT ($this->idCol$this->dataCol$this->timeCol) VALUES (:id, :data, :time) ".
  184.                     "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time";
  185.             case $this->con->getDatabasePlatform() instanceof SQLServer2008Platform:
  186.                 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
  187.                 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
  188.                 return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
  189.                     "WHEN NOT MATCHED THEN INSERT ($this->idCol$this->dataCol$this->timeCol) VALUES (:id, :data, :time) ".
  190.                     "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;";
  191.             case 'sqlite':
  192.                 return "INSERT OR REPLACE INTO $this->table ($this->idCol$this->dataCol$this->timeCol) VALUES (:id, :data, :time)";
  193.         }
  194.     }
  195. }