PHP/MongoDB SessionManager class problem

I asked this question in several places, and no one knew the answer. I shall ask it here, and maybe I’ll find someone to sort my issue out.

First, the system I sue is ubuntu, and I use localhost as a xampp based local server.

I am in a middle of reading a book called PHP and MongoDB development for Beginners. The problem i face is that I want to make database-based session management. The codes goes like:

  1. I had to create connection.php file:

<?php
	class DBConnection {
		const HOST = 'localhost';
		const PORT = 27017;
		const DBNAME = 'choqlet';
		private static $instance;
		public $connection;
		public $database;
		private function __construct() {
			$connectionString = sprintf('mongodb://%s:%d',
				DBConnection::HOST, DBConnection::PORT);
			try {
				$this->connection = new Mongo($connectionString);
				$this->database = $this->connection->
				selectDB(DBConnection::DBNAME);
			} catch (MongoConnectionException $e) {
				throw $e;
			}
		}
		static public function instantiate() {
			if(!isset(self::$instance)) {
					$class = __CLASS__;
					self::$instance = new $class;
				}
			return self::$instance;
			}
		public function getCollection($name) {
			return $this->database->selectCollection($name);
			}
		}
?>
  1. Then, the second step is creating the SessionManager class, that handles the session and instatiates it:

<?php
	require_once('connection.php');
	class SessionManager {
		const COLLECTION = 'sessions';
		const SESSION_TIMEOUT = 600;
		const SESSION_LIFESPAN = 3600;
		const SESSION_NAME = 'mongosessid';
		const SESSION_COOKIE_PATH = '/';
		const SESSION_COOKIE_DOMAIN = '';
		private $_mongo;
		private $_collection;
		private $_currentSession;
		public function __construct() {
			$this->_mongo = DBConnection::instantiate();
			$this->_collection = $this->_mongo->
				getCollection(SessionManager::COLLECTION);
				session_set_save_handler(
					array(&$this, 'open'),
					array(&$this, 'close'),
					array(&$this, 'read'),
					array(&$this, 'write'),
					array(&$this, 'destroy'),
					array(&$this, 'gc')
					);
				ini_set('session.gc_maxlifetime',
					SessionManager::SESSION_LIFESPAN);
				session_set_cookie_params(
					SessionManager::SESSION_LIFESPAN,
					SessionManager::SESSION_COOKIE_PATH,
					SessionManager::SESSION_COOKIE_DOMAIN
					);
				session_name(SessionManager::SESSION_NAME);
				session_cache_limiter('nocache');
				session_start();
				}
				public function open($path, $name) {
					return true;
				}
				public function close() {
					return true;
				}
				public function read($sessionId) {
					$query = array(
						'session_id' => $sessionId,
						'timeout_at' => array('$gte' => time()),
						'expired_at' => array('$gte' => time())
						);
				$result = $this->_collection->findOne($query);
				$this->_currentSession = $result;
				if(!isset($result['data'])) {
					return "";
				}
				return $result['data'];
				}
				public function write($sessionId, $data) {
					$expired_at = time() + self::SESSION_TIMEOUT;
					$new_obj = array(
					'data' => $data,
					'timedout_at' =>
						time() + self::SESSION_TIMEOUT,
					'expired_at' =>
						(empty($this->_currentSession)) ?
						time()+ SessionManager::SESSION_LIFESPAN
						: $this->_currentSession['expired_at']
				);
				$query = array('session_id' => $sessionId);
				$this->_collection->update(
					$query,
					array('$set' => $new_obj),
					array('upsert' => True)
				);
				return True;
			  }
			  public function destroy($sessionId)
			  {
				$this->_collection->remove(array('session_id' =>
					$sessionId));
				return True;
			  }
			  public function gc()
              {
				$query = array( 'expired_at' => array('$lt' => time()));
				$this->_collection->remove($query);
				return True;
			  }
			  public function __destruct() {
			  	session_write_close();
			  }
			}
			//initiate the session
			$session = new SessionManager();

?>
  1. Now, author advises to run two test for the session handling, the first page is:

<?php
//Session started by requiring the script
require('session.php');
//Generate a random number
$random_number = rand();
//Put the number into session
$_SESSION['random_number'] = $random_number;
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=utf-8"/>
<link rel="stylesheet" href="style.css" />
<title>Using the SessionManager...Page 1</title>
</head>


<body>
	<div id="contentarea">
		<div id="innercontentarea">
			<h2>Using the SessionManager...Page 1</h2>
				<p>Random number generated
					<span style="font-weight:bold;">
					<?php echo $_SESSION['random_number']; ?>
					</span>
				</p>
				<p>PHP session id
					<span style="text-decoration:underline;">
					<?php echo session_id(); ?>
					</span>
				</p>
				<a href="mongo_session2.php">Go to next page</a>
		</div>
	</div>
</body>
</html>

And then it anchors to the second page, on which a random number generated, should be taken from previously created session, and put in the same place:


<?php
//Session started by requiring the script
require('session.php');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=utf-8"/>
<link rel="stylesheet" href="style.css" />
<title>Using the SessionManager...Page 1</title>
</head>

<body>
	<div id="contentarea">
		<div id="innercontentarea">
			<h2>Using the SessionManager...Page 2</h2>
				<p>The random number generated in previous page is still
					<span style="font-weight:bold;">
					<?php echo $_SESSION['random_number']; ?>
					</span>
				</p>
				<p>PHP session id
					<span style="text-decoration:underline;">
					<?php echo session_id(); ?>
					</span>
				</p>
		</div>
	</div>
</body>
</html>

But instead of presenting the same number as the first page, it gives me an error:

Notice: Undefined index: random_number in /opt/lampp/htdocs/choqlet/mongo_session2.php on line 22

Like the session was created, but the random_number did not save. Means that I have got some internal problem of storing temp files, or it is a mistake in the book?

Hello, maciejsitko. I looked into your problem and have found out why things aren’t working the way they should be.

The TLDR; problem and fix is that you copied the code wrong in the read method in the SessionManager class. Your method was:


public function read($sessionId)
{
	$query = array(
		'session_id' => $sessionId,
		'timeout_at' => array('$gte' => time()), // notice this line
		'expired_at' => array('$gte' => time()) // and this line
		);
	$result = $this->_collection->findOne($query);
	$this->_currentSession = $result;
	if(!isset($result['data'])) {
		return "";
	}
	return $result['data'];
}

When it should have been the following:


public function read($sessionId) {
	$query = array(
		'session_id' => $sessionId,
		'timedout_at' => array('$gte' => time()), // notice this line
		'expired_at' => array('$gte' => time() - SessionManager::SESSION_LIFESPAN) // and this line
		);
	$result = $this->_collection->findOne($query);
	$this->_currentSession = $result;
	if(!isset($result['data'])) {
		return "";
	}
	return $result['data'];
}

So you spelt the timedout_at attribute name wrong, and you forgot to take away the session’s lifespan from the current time.

Here’s how I debugged your script (so that you can attempt to debug your own scripts in future - a handy thing to know! ;)).
I found the error by comparing the data in the sessions collection (through mongo CLI) from after loading the mongo_session1.php page:


{
	"_id" : ObjectId("53f8fb6f7567b40dd58b7551"),
	"session_id" : "atn7mgjd37l0bqbseodmrdesa1",
	"data" : "random_number|i:78738229;",
	"timedout_at" : NumberLong(1408827454),
	"expired_at" : NumberLong(1408830454)
}

and then after loading the mongo_session2.php page:


{
	"_id" : ObjectId("53f8fb6f7567b40dd58b7551"),
	"session_id" : "atn7mgjd37l0bqbseodmrdesa1",
	"data" : "",
	"timedout_at" : NumberLong(1408827496),
	"expired_at" : NumberLong(1408830496)
}

The data attribute was missing its value after the mongo_session2.php attempted to read it (narrowing it down the read method of SessionManager). And so I added a var_dump($result);die; line just before the return statement in the read method, and the result was NULL - which meant that you weren’t finding any records in your query (this sort of basic validation is something you’ll want to implement). This lead me to the search query $query array, which was where your error was!

Awesome! Thank you for the tips, I know about var_dump, but just didn’t take it under consideration, thought that it would maybe more like session handler error, so it used destroy way before write function. But as it seems, it wasn’t the case!