Skip to content

Instantly share code, notes, and snippets.

@yohgaki
Last active December 15, 2015 00:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yohgaki/a7b130bc93b2f9467ccc to your computer and use it in GitHub Desktop.
Save yohgaki/a7b130bc93b2f9467ccc to your computer and use it in GitHub Desktop.
[PHP] PostgreSQL user defined session save handler example
<?php
/**
Use following table
CREATE TABLE php_session (
id text UNIQUE NOT NULL,
data bytea NOT NULL,
updated int8 NOT NULL
);
Note:
This save handler handles concurrency error by ignoring failed transactions
and try again upto pgsql_session_save_handler::$max_transaction_errors.
Concurrency errors may happen when browser sent concurrent requests to
web server. e.g. Browsers may send multiple requests for faster page loading,
from multiple frames, from multiple tabs, etc.
Unlike file system based session data storage, database system may not show
newly created/deleted rows in transaction. Database system maximizes concurrency
by hiding transaction details, but this may result in concurrency errors. In
order to achive concurrency and consistency, save handler must retry transaction
when error happenned.
*/
class pgsql_session_save_handler implements SessionHandlerInterface {
protected $db;
protected $max_transaction_errors = 10;
public function open($savePath, $sessionName) {
$this->db = pg_pconnect("host=localhost port=5432 dbname=yohgaki user=yohgaki");
//$this->gc(0); // This is for debugging purpose only
if ($this->db === FALSE) {
return FALSE;
}
return TRUE;
}
public function close() {
// Uses persistent connection. Return TRUE always.
return TRUE;
}
public function destroy($sid) {
$result = pg_query_params($this->db, 'DELETE FROM "php_session" WHERE id = $1;',[$sid]);
if ($result === FALSE) {
return FALSE;
}
// Do not care if it is really deleted or not.
return TRUE;
}
/* Following private functions ignore error intentionally, so that
transaction errors will be suppressed */
private function sel_session($sid) {
// "FOR UPDATE" is needed for proper session data locking.
return @pg_query_params($this->db, 'SELECT data, updated FROM php_session WHERE id = $1 FOR UPDATE;', [$sid]);
}
private function ins_session($sid) {
return @pg_query_params($this->db, 'INSERT INTO php_session (id, data, updated) VALUES ($1, $2, $3)', [$sid, '', time()]);
}
private function del_session($sid) {
return @pg_query_params($this->db, 'DELETE FROM php_session WHERE id = $1', [$sid]);
}
private function upd_session($sid, $data) {
$data = pg_escape_bytea($this->db, $data);
return @pg_query_params($this->db, 'UPDATE php_session SET data = $2, updated = $3 WHERE id = $1', [$sid, $data, time()]);
}
public function read($sid) {
$maxlifetime = (int)ini_get('session.gc_maxlifetime');
if ($maxlifetime <= 0) {
trigger_error('Max session life time in seconds should be positive numbers.');
return FALSE;
}
while ($this->max_transaction_errors--) {
// Clean up old transaction.
pg_query($this->db, 'COMMIT;');
// Transaction isolation level must be serializable for data consistency.
if (!pg_query($this->db, 'BEGIN;') || !pg_query($this->db, 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;')) {
return FALSE;
}
// If you would not like to select expired data, select the row and delete it if it is needed.
$result = $this->sel_session($sid);
if (!$result) {
continue;
}
$row = pg_fetch_assoc($result);
/*
Note: Transaction for concurrent new session ID may fail because
browser uses multiple concurrent connection to web server.
*/
if (!$row) {
// New session
if (!$this->ins_session($sid)) {
continue;
}
return '';
}
else if ($row['updated'] < time() - $maxlifetime) {
// Expired. Delete and create
if (!$this->del_session($sid)) {
continue;
}
if (!$this->ins_session($sid)) {
continue;
}
return '';
}
return pg_unescape_bytea($row['data']);
}
// Under normal circumstances, it's transaction error. Gave up.
// write() will be called anyway. Let write() call COMMIT.
return FALSE;
}
public function write($sid, $data) {
/*
Note:
upd_session() may result in transaction error and
COMMIT may result in ROLLBACK due to concurrency error.
However, COMMIT failure does not raise PHP error.
*/
$this->upd_session($sid, $data);
pg_query($this->db, 'COMMIT;');
return TRUE;
}
public function gc($maxlifetime) {
return pg_affected_rows(pg_query_params($this->db, 'DELETE FROM php_session WHERE updated < $1', [time() - $maxlifetime]));
}
}
//////////// test //////////////////
ini_set('session.save_handler', 'user');
ini_set('session.gc_maxlifetime', 120);
$pgsql_handler = new pgsql_session_save_handler;
session_set_save_handler($pgsql_handler, TRUE);
ob_start();
session_start();
var_dump($_SESSION['cnt']++);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment