Last active
December 15, 2015 00:15
-
-
Save yohgaki/a7b130bc93b2f9467ccc to your computer and use it in GitHub Desktop.
[PHP] PostgreSQL user defined session save handler example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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