Skip to content

Instantly share code, notes, and snippets.

@xocasdashdash
Last active August 8, 2022 02:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save xocasdashdash/48c3871aee9e898d4fb4 to your computer and use it in GitHub Desktop.
Save xocasdashdash/48c3871aee9e898d4fb4 to your computer and use it in GitHub Desktop.
Storing symfony sessions using doctrine. Based on the PDOSessionHandler that you can find in Symfony\Component\HttpFoundation\Session\Storage\Handler
framework:
session:
handler_id: session.handler.db_session
parameters:
oci8.db_options:
db_table: PHPSessionTable
db_id_col: session_id
db_data_col: session_value
db_time_col: session_time
session.handler.db_session:
class: AcmeDemoBundle\DoctrineSessionHandler
arguments: ["@doctrine.dbal.default_connection", "%oci8.db_options%"]
class DoctrineSessionHandler implements \SessionHandlerInterface {
/**
*
* @var array Parametros de conexión de Doctrine a la base de datos
*/
private $dbalConnection;
/**
*
* @var Entity Entidad que uso para guardar los datos en la BD
*/
private $table;
/**
* @var string Column for session id
*/
private $idCol;
/**
* @var string Column for session data
*/
private $dataCol;
/**
* @var string Column for timestamp
*/
private $timeCol;
/**
* Constructor.
*
* List of available options:
* * db_table: The name of the table [required]
* * db_id_col: The column where to store the session id [default: sess_id]
* * db_data_col: The column where to store the session data [default: sess_data]
* * db_time_col: The column where to store the timestamp [default: sess_time]
*
* @param array Doctrine connection parameters to the database
* @param array $dbOptions An associative array of DB options
*
* @throws \InvalidArgumentException When "doctrine_entity" option is not provided
*/
public function __construct(Connection $dbalConnection, array $dbOptions = array()) {
if (!array_key_exists('db_table', $dbOptions)) {
throw new \InvalidArgumentException('You must provide the "db_table" option for a DoctrineSessionStorage.');
}
$this->dbalConnection = $dbalConnection;//->getParams();
$dbOptions = array_merge(array(
'db_id_col' => 'sess_id',
'db_data_col' => 'sess_data',
'db_time_col' => 'sess_time',
), $dbOptions);
$this->table = $dbOptions['db_table'];
$this->idCol = $dbOptions['db_id_col'];
$this->dataCol = $dbOptions['db_data_col'];
$this->timeCol = $dbOptions['db_time_col'];
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName) {
return true;
}
/**
* {@inheritdoc}
*/
public function close() {
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->getConnection()->prepare($sql);
$stmt->bindValue(':id', $sessionId, "string");
$stmt->execute();
} catch (Exception $e) {
throw new \RuntimeException(sprintf('Exception was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e);
}
return true;
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime) {
// delete the session records that have expired
$sql = "DELETE FROM $this->table WHERE $this->timeCol < :time";
try {
$stmt = $this->getConnection()->prepare($sql);
$stmt->bindValue(':time', time() - $maxlifetime, "integer");
$stmt->execute();
} catch (Exception $e) {
throw new \RuntimeException(sprintf('Exception was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e);
}
return true;
}
/**
* {@inheritdoc}
*/
public function read($sessionId) {
$sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id";
try {
$stmt = $this->getConnection()->prepare($sql);
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$stmt->execute();
$sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM);
if ($sessionRows) {
return base64_decode($sessionRows[0][0]);
}
return '';
} catch (Exception $e) {
throw new \RuntimeException(sprintf('Exception was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $data) {
// Session data can contain non binary safe characters so we need to encode it.
$encoded = base64_encode($data);
// We use a MERGE SQL query when supported by the database.
// Otherwise we have to use a transactional DELETE followed by INSERT to prevent duplicate entries under high concurrency.
try {
$mergeSql = $this->getMergeSql();
if (null !== $mergeSql) {
$mergeStmt = $this->getConnection()->prepare($mergeSql);
$mergeStmt->bindParam(':id', $sessionId, "string");
$mergeStmt->bindParam(':data', $encoded, "string");
$mergeStmt->bindValue(':time', time(), "integer");
$mergeStmt->execute();
return true;
}
$this->getConnection()->beginTransaction();
try {
$deleteStmt = $this->getConnection()->prepare(
"DELETE FROM $this->table WHERE $this->idCol = :id"
);
$deleteStmt->bindParam(':id', $sessionId, "string");
$deleteStmt->execute();
$insertStmt = $this->getConnection()->prepare(
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"
);
$insertStmt->bindParam(':id', $sessionId, "string");
$insertStmt->bindParam(':data', $encoded, "string");
$insertStmt->bindValue(':time', time(), "integer");
$insertStmt->execute();
$this->getConnection()->commit();
} catch (Exception $e) {
$this->getConnection()->rollback();
throw $e;
}
} catch (Exception $e) {
throw new \RuntimeException(sprintf('Exception was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
}
return true;
}
/**
* Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database.
*
* @return string|null The SQL string or null when not supported
*/
private function getMergeSql() {
$driver = $this->getConnection()->getDriver()->getName();
switch ($driver) {
case 'pdo_mysql':
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " .
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)";
case 'oci8':
// 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->timeCol) VALUES (:id, :data, :time) " .
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data";
case 'sqlsrv':
// MS SQL Server requires MERGE be terminated by semicolon
return "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " .
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " .
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data;";
case 'pdo_sqlite':
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
}
}
/**
* Return a DBAL\Connection instance
*
* @return \DBAL\Connection
*/
protected function getConnection() {
return $this->dbalConnection;
}
}
<?php
namespace AcmeDemoBundle\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Entity;
/**
* User
*
* @Table(name="PHPSessionTable")
* @Entity
*/
class Session {
/**
* @Column(name="session_id", type="string", length=255)
* @Id
*/
private $session_id;
/**
* @Column(name="session_value", type="string", length=4000)
*
*/
private $session_value;
/**
* @Column(name="session_time", type="integer")
*
*/
private $session_time;
//Insert public getters and setters
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment