You want to store session data in a database instead of in files. If multiple web servers all have access to the same database, the session data is then mirrored across all the web servers.
Set session.save_handler to user in php.ini and use the pc_DB_Session class shown in Example 8-1. For example:
$s = new pc_DB_Session('mysql://user:password@localhost/db');
ini_get('session.auto_start') or session_start();
One of the most powerful aspects of the session module is its abstraction of how sessions get saved. The session_set_save_handler( ) function tells PHP to use different functions for the various session operations such as saving a session and reading session data. The pc_DB_Session class stores the session data in a database. If this database is shared between multiple web servers, users' session information is portable across all those web servers. So, if you have a bunch of web servers behind a load balancer, you don't need any fancy tricks to ensure that a user's session data is accurate no matter which web server they get sent to.
To use pc_DB_Session, pass a data source name (DSN) to the class when you instantiate it. The session data is stored in a table called php_session whose structure is:
CREATE TABLE php_session ( id CHAR(32) NOT NULL, data MEDIUMBLOB, last_access INT UNSIGNED NOT NULL, PRIMARY KEY(id) )
If you want the table name to be different than php_session, set session.save_path in php.ini to your new table name. Example 8-1 shows the pc_DB_Session class.
require 'PEAR.php';
require 'DB.php';
class pc_DB_Session extends PEAR {
var $_dbh;
var $_table;
var $_connected = false;
var $_gc_maxlifetime;
var $_prh_read;
var $error = null;
/**
* Constructor
*/
function pc_DB_Session($dsn = null) {
if (is_null($dsn)) {
$this->error = PEAR::raiseError('No DSN specified');
return;
}
$this->_gc_maxlifetime = ini_get('session.gc_maxlifetime');
// Sessions last for a day unless otherwise specified.
if (! $this->_gc_maxlifetime) {
$this->_gc_maxlifetime = 86400;
}
$this->_table = ini_get('session.save_path');
if ((! $this->_table) || ('/tmp' == $this->_table)) {
$this->_table = 'php_session';
}
$this->_dbh = DB::connect($dsn);
if (DB::isError($this->_dbh)) {
$this->error = $this->_dbh;
return;
}
$this->_prh_read = $this->_dbh->prepare(
"SELECT data FROM $this->_table WHERE id LIKE ? AND last_access >= ?");
if (DB::isError($this->_prh_read)) {
$this->error = $this->_prh_read;
return;
}
if (! session_set_save_handler(array(&$this,'_open'),
array(&$this,'_close'),
array(&$this,'_read'),
array(&$this,'_write'),
array(&$this,'_destroy'),
array(&$this,'_gc'))) {
$this->error = PEAR::raiseError('session_set_save_handler() failed');
return;
}
return $this->_connected = true;
}
function _open() {
return $this->_connected;
}
function _close() {
return $this->_connected;
}
function _read($id) {
if (! $this->_connected) { return false; }
$sth =
$this->_dbh->execute($this->_prh_read,
array($id,time() - $this->_gc_maxlifetime));
if (DB::isError($sth)) {
$this->error = $sth;
return '';
} else {
if (($sth->numRows() == 1) &&
($ar = $sth->fetchRow(DB_FETCHMODE_ORDERED))) {
return $ar[0];
} else {
return '';
}
}
}
function _write($id,$data) {
$sth = $this->_dbh->query(
"REPLACE INTO $this->_table (id,data,last_access) VALUES (?,?,?)",
array($id,$data,time()));
if (DB::isError($sth)) {
$this->error = $sth;
return false;
} else {
return true;
}
}
function _destroy($id) {
$sth = $this->_dbh->query("DELETE FROM $this->_table WHERE id LIKE ?",
array($id));
if (DB::isError($sth)) {
$this->error = $sth;
return false;
} else {
return true;
}
}
function _gc($maxlifetime) {
$sth = $this->_dbh->query("DELETE FROM $this->_table WHERE last_access < ?",
array(time() - $maxlifetime));
if (DB::isError($sth)) {
$this->error = $sth;
return false;
} else {
return true;
}
}
}
The pc_DB_Session::_write( ) method uses a MySQL-specific SQL command, REPLACE INTO, which updates an existing record or inserts a new one, depending on whether there is already a record in the database with the given id field. If you use a different database, modify the _write( ) function to accomplish the same task. For instance, delete the existing row (if any), and insert a new one, all inside a transaction:
function _write($id,$data) {
$sth = $this->_dbh->query('BEGIN WORK');
if (DB::isError($sth)) {
$this->error = $sth;
return false;
}
$sth = $this->_dbh->query("DELETE FROM $this->_table WHERE id LIKE ?",
array($id));
if (DB::isError($sth)) {
$this->error = $sth;
$this->_dbh->query('ROLLBACK');
return false;
}
$sth = $this->_dbh->query(
"INSERT INTO $this->_table (id,data,last_access) VALUES (?,?,?)",
array($id,$data,time()));
if (DB::isError($sth)) {
$this->error = $sth;
$this->_dbh->query('ROLLBACK');
return false;
}
$sth = $this->_dbh->query('COMMIT');
if (DB::isError($sth)) {
$this->error = $sth;
$this->_dbh->query('ROLLBACK');
return false;
}
return true;
}
Documentation on session_set_save_handler( ) at http://www.php.net/session-set-save-handler; a handler using PostgreSQL is available at http://www.zend.com/codex.php?id=456&single=1; the format for data source names is discussed in Recipe 10.4.
Copyright © 2003 O'Reilly & Associates. All rights reserved.