database sessions issue

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

Post Reply
waynes888
Forum Newbie
Posts: 7
Joined: Mon Apr 05, 2010 7:44 pm

database sessions issue

Post by waynes888 »

I have an interesting issue which I am not sure if it is a bug or by design. I am using a database table to store sessions and I noticed that sessions are not always updated in the database. After some investigation I found some interesting observations where sessions were updated but the updates were not recorded in the database. There is too much code to paste here but I will try explain what is happening hoping that someone may be able to shed some light on the issue. Basically I am using a framework where the index file calls a bootstrapper that sets up the config & sessions, then a front controller that shows the page. In the index file, bootstrapper and front controller, session values that are set work correctly, that is they are stored in the session and database table. However this is where the fun begins. In the front controller I include the action pages and views of the modules that have been called. They are simply script pages that contain additional code for the page module. If I set session values in the included files, they are included in $_SESSION but not written to the database. Also if I use is_dir or file exists to check if a directory or file exists, it stops writing sessions to the database following the check and within the condition parenthesis, for example, in the following code "start, dir & end" should be recorded in session variables and recorded in the database table:

Code: Select all

$directory = 'c:\temp';

$_SESSION['registry']->start = 'start'; 

if (is_dir($directory)) 
{  
       // directory does exist
       $_SESSION['registry']->dir = $this->directory; 
}

$_SESSION['registry']->end = 'end'; 
In this case start, dir, end are recorded in $_SESSION, but only start & end are recorded in the database. also tried this way:

Code: Select all

$dir = is_dir($directory);
if ($dir) { ......
However, if I don't use a variable for the directory, it works fine writing start, dir and end to the database, eg:

Code: Select all

if (is_dir('c:\temp')) {.....
And this also works:

Code: Select all

$dir = true; 
if (!(is_dir($directory))) $dir = false; 
if ($dir) { ......
All 3 sessions are written to the database in the above example.

This testing on a windows development PC using xampp 1.7.2 so not sure if that has anything to do with it. Xampp is using php 5.3.0 and MySql 5.

Has anyone come across this issue before? Is there some sort of depth limit for database sessions? What gets me is the sessions are recorded correctly in $_SESSIONs but just not getting updated in the database in the above conditions. I also tried to recreate the sessions at the end of the script by reading the session into an array, then rewriting back out to the session, for example:

$registry_items = $_SESSION['registry'];
unset($_SESSION['registry']);

$_SESSION['registry'] = new Registry(); //Local class with basic session helper eg set, get etc

foreach ($registry_items as $key => $value)
{
Registry::set($key, $value); // same as $_SESSION['registry']->$key = $value;
}

Interestingly enough, this will write all the sessions except those set in the included file even though they are unset and rewritten. I am pulling my hair out over this......

Happy to attach the session files if anyone can help.
User avatar
flying_circus
Forum Regular
Posts: 732
Joined: Wed Mar 05, 2008 10:23 pm
Location: Sunriver, OR

Re: database sessions issue

Post by flying_circus »

I am confused on what you are trying to accomplish. Why are you checking directories for database driven sessions? If your session handler is set up correctly, you dont need to do anything special as far as setting or destroying session variables. Post your session handler code.
waynes888
Forum Newbie
Posts: 7
Joined: Mon Apr 05, 2010 7:44 pm

Re: database sessions issue

Post by waynes888 »

Sorry if that was unclear. The actual code in the examples in the first post is in my front_controller class and is checking directories and files to include in the output of the page. They have nothing to do with the sessions. I am setting the sessions in the example at those points to figure where they stop being added to the database which happened to be at the point when I did a directory check or a file check or included a page file in my controller class. I have attached my code below for further clarification (this is still in developement but here goes anyway):

The index file:

Code: Select all

   include('../application/bootstrapper.php');
   $front = new Front_Controller($__ENV->config['application']['layout']);
   $front->showPage();
Relevant code from bootstrapper:

Code: Select all

ini_set("session.gc_maxlifetime", 3600);
ini_set("session.gc_divisor", 50);

	$sessionService = new SessionManager();	

	session_start();
	if(empty($_SESSION['registry']))
	{
		$_SESSION['registry'] = new Registry();
	}

Registry:

Code: Select all

class Registry {

		public static function set($reference, $value) {
			$_SESSION["registry"]->$reference = $value;
		}

		public static function un_set($reference) {
			unset($_SESSION["registry"]->$reference);
		}

		public static function get($reference) {
			return isset($_SESSION["registry"]->$reference) ? $_SESSION["registry"]->$reference : null;
		}
		
		public static function setMessage($messageType, $message) {
			self::set('messageType', $messageType);
			self::set('message', $message);
		}
	}
Session handler:

Code: Select all

class SessionManager {

		private $sessionModel;

		public $maxLifetime = 3600; // 1 hour

		public function __construct()
		{
			$this->sessionModel = new Session_Model();
			session_set_save_handler(
						 array(&$this, "open"),
						 array(&$this, "close"),
						 array(&$this, "read"),
						 array(&$this, "write"),
						 array(&$this, "destroy"),
						 array(&$this, "gc")
						);
		}

		/**
		 * Destructor.
		 */
		public function __destruct()
		{
			session_write_close();
		}

		/**
		 * Assigns a reference to the connection publicly.
		 */
		public function open()
		{
			return true;
		}

		/**
		 * Simply needs to return true.
		 */
		public function close()
		{
			return true;
		}

		/**
		 * return session data only if it hasn't expired.
		 */
		public function read($sessionID)
		{
			$sessionData = $this->sessionModel->getSessionByID($sessionID, $this->maxLifetime);
			return ($sessionData === false) ? "" : $sessionData;

		}

		/**
		 * Creates/updates session data.
		 */
		public function write($sessionID, $sessionData)
		{
			$session = $this->sessionModel->getSessionByID($sessionID);

			return (!$session)
				? $this->sessionModel->createSessionByID($sessionID, $sessionData)
				: $this->sessionModel->updateSessionByID($sessionID, $sessionData);
		}

		/**
		 * Destroys the session.
		 */
		public function destroy($sessionID)
		{
			return $this->sessionModel->deleteSessionByID($sessionID);
		}

		/**
		 * Garbage clean up.
		 */
		public function gc($maxLifetime)
		{
			$this->sessionModel->deleteExpiredSessions($this->maxLifetime);
			return true;
		}
Session model:

Code: Select all

class Session_Model extends Base_Model {
		private $dbTable = 'core_sessions';
		public function createSessionByID($sessionID, $sessionData) {
			// Bind
			$bindValues_array = array("s_id" => $sessionID,  "s_data" => $sessionData  );

			return $this->insert($this->dbTable, $bindValues_array);

		}

		public function getSessionByID($sessionID, $maxAgeInSeconds=null) {
			// Bind
			$bindValues_array = array("s_id" => $sessionID);

			// Build and execute statement
			$sql = "SELECT
						s_data
					FROM
						{$this->dbTable}
					WHERE
						s_id = :s_id";

			if ($maxAgeInSeconds)
			{
				$bindValues_array = array_merge($bindValues_array, array("max_age"	=> $maxAgeInSeconds));

				$sql .= " AND
						   TIMESTAMPADD(SECOND, :max_age, s_timestamp) > now()";
			}
			return current($this->select($sql, $bindValues_array));
		}

		public function deleteSessionByID($sessionID) {
			// Bind
			$bindValues_array = array("s_id" => $sessionID);
			return $this->delete($this->dbTable, $bindValues_array);
		}

		/**
		 * Deletes all expired sessions.
		 */
		public function deleteExpiredSessions($maxAge) {

			// Bind
			$bindValues_array = array("max_age" => $maxAge);

			// Build and execute statement
			$sql = "DELETE FROM
						{$this->dbTable}
					WHERE
						TIMESTAMPADD(SECOND, :max_age, s_timestamp) < now()";

			if(!$query = $this->prepQuery($sql, $bindValues_array))
			{
				throw new Exception('Cannot execute Delete Sessions: '. $bindValues_array);
					return false;
			}
		}

		/**
		 * Updates session data by ID.
		 */
		public function updateSessionByID($sessionID, $sessionData) {

			$arrFieldValues = array("s_data" => $sessionData);
			$arrConditions	= array("s_id"	 => $sessionID);
			$this->update($this->dbTable, $arrFieldValues, $arrConditions);
		}
	}
Front controller (note: some code removed and in this sample all the values get written to the DB. If I change the line the checks the directory to "return (is_dir($this->moduleDirectory))", the session value "dir" is not written to the database but start and end are) :

Code: Select all

class Front_Controller extends Base_Controller
{
	public function __construct($layout = 'default')
	{
		parent::__construct();
		$this->getPageRequest(); 			 // Sets module, page/view and actions
		$this->layout = $layout;
		$this->setSiteLayout($this->layout);  // Sets layout page to use
	}

	public function showPage()
	{
		registry::set('start', time());  // only set for testing

		// If the directory exists, set template, include the index file and actions file
		if ($this->moduleExists())
		{
			registry::set('dir', $this->moduleDirectory);  // only set for testing 

                       // Set the current template - this can be changed by the include pages
			$pageView = (empty($this->page)) ?  $this->defaultView : $this->page;

			// Include the index page for module if it exists
			if ($this->pageExists($this->moduleDirectory . "/" . $this->defaultPage . ".php"))
			{
				include($this->moduleDirectory . "/" . $this->defaultPage . ".php");
			}

			$currentPage  = $this->moduleDirectory . "/" . $this->page . ".php";

			// include current page template if it exists
			if (!($this->page == $this->defaultPage) && $this->pageExists($currentPage))
			{
			    include $currentPage;
			}
			$templatePage = $this->setPageView($pageView);
		} else {
			//show the error page
			$templatePage = $this->setErrorPage();
		}

		ob_start();
		include $templatePage;
		$content = ob_get_contents();
		ob_end_clean();

		include $this->siteLayout;

		registry::set('end', time()); // testing 
	}

	private function moduleExists()
	{
		// return boolen using this method because of bug - is_dir stops database sessions
		$isDir = true;
		if (!is_dir($this->moduleDirectory)) $isDir = false;
		return $isDir;
	}

	private function pageExists($page)
	{
		// return boolen using this method because of bug - file_exists stops database sessions
		$isFile = true;
		if (!file_exists($page)) $isFile = false;
		return $isFile;
	}

	/**
	* Sets the layout used for the site, in not found will use default
	*
	*/
	public function setSiteLayout($layout)
	{
		 $this->siteLayout = _APP_DIR . '/layout/'  . $layout . '/index.tpl';
		 if (!file_exists($this->siteLayout))
		 {
			  $this->siteLayout = _APP_DIR . '/layout/default/index.tpl';
		 }
	}

	/**
	* Gets the page request and splits into module, page, actions
	*/
	private function getPageRequest()
	{

		$request = (!empty($_GET['req'])) ? explode('/', $_GET['req']) : array();

		..... 

		$this->module  = array_shift($request);
		$this->page     = array_shift($request);

		$this->actions = (empty($request[0])) ? '' : $request;  // Remaining actions

		$this->page = (empty($this->page)) ? $this->defaultPage : $this->page;

		$this->moduleDirectory = _PUBLIC_DIR . '/pages/' . $this->normalizeFilename($this->module);
	}

	/**
	* Sets the currect page view if found, else the error page
	*/
	private function setPageView($pageView)
	{
		$template = $this->moduleDirectory . '/views/' . $pageView . '.tpl';
		if (!file_exists($template))
		{
			$template = $this->setErrorPage();
		}
		return $template;
	}

	/**
	* Sets the error page template and content type
	*/
	private function setErrorPage()
	{
		$this->headerContentType= "HTTP/1.0 404 Not Found";
		return _COMMON_VIEWS . 'error404.tpl';
	}
}
cpetercarter
Forum Contributor
Posts: 474
Joined: Sat Jul 25, 2009 2:00 am

Re: database sessions issue

Post by cpetercarter »

This may not be a sessions problem at all, but a bug in the way that "is_dir" works in Windows. There are user contributions in the manual (http://php.net/manual/en/function.is-dir.php) which suggest that others have found that is_dir can be erratic, particularly when testing for the 'temp' directory. You seem to have found something which works (ie test for is_dir('c:\temp') rather than is_dir($dir)), so why not just use it and put this buggy behaviour into the box marked Life's Great Mysteries!
User avatar
flying_circus
Forum Regular
Posts: 732
Joined: Wed Mar 05, 2008 10:23 pm
Location: Sunriver, OR

Re: database sessions issue

Post by flying_circus »

I haven't gone through all your code but what stood out immediately is that session_write_close() is in your SessionManager destructor. Session_write_close() must be called before the database object is destroyed and I don't see where you are creating/destroying a database connection. The best place (debateable) to put session_write_close() is within your database class destructor. Now you can be certain that you close the session (commit session to the database) before closing the database connection. If there are any errors in your script and the database class is instantiated prior to the SessionManager, it will subsequently be terminated prior to the SessionManager, hence a loss of session data.
waynes888
Forum Newbie
Posts: 7
Joined: Mon Apr 05, 2010 7:44 pm

Re: database sessions issue

Post by waynes888 »

a bug in the way that "is_dir" works in Windows
Thanks cpetercarter, I discorvered that quite by mistake when I was check where the sessions stopped writing to the database and got around the is_dir & file_exists issues with the 2 functions moduleExists and pageExists but still have an issue with pages included in the front controller. So if the page exists, I include it. The included page may set session variables (usually messages) and it is these that are not written to the database even they do appear in $_SESSIONS. I may change the page to a class and see if that makes a difference but I was trying to avoid making each page a class.
Session_write_close() must be called before the database object is destroyed and I don't see where you are creating/destroying a database connection.
The database connector is created before the sessions in the config and the placement of the session_write_close doesn't seem to make a difference. I am not entirely sure how php destroys classes but I would assume it would be in the reverse order, eg, first sessions, then the database connector. I did read somewhere that the session handler can close the database prematurely which is why someone recommended placing the session_write_close in the session destructor. I set a session variable as the very last line of code and it still wrote to the database so that rules out any issues there. The database connector is a singleton class so the connector is never refreshed or changed at any point. The only thing I can think of is it may be a buffer issue. Using firebug and firePHP I noticed that if I used fb a few times sessions set after fb would stop writing to the database. It was totally random though so I can't be too sure about that. I am going to move the site to a unix box today to rule out any windows issue.
waynes888
Forum Newbie
Posts: 7
Joined: Mon Apr 05, 2010 7:44 pm

Re: database sessions issue

Post by waynes888 »

Just confirmed that this happens in the unix environment as well...will have to dig deeper....
waynes888
Forum Newbie
Posts: 7
Joined: Mon Apr 05, 2010 7:44 pm

Re: database sessions issue

Post by waynes888 »

I found the cause but no closer to solution. Seems the serialized data is being truncated by PDO. I traced the serialised data all the way through to when PDO actually executed the SQL statement. Up until this point all the data exists in the session variable. Once the data is written to the database, some variables mysteriously disappear. I even tried to change it so I first set variables to the registry object and then at the end of processing I read the registry into the session variable. The below is a trace through the process. I am really at a loss to explain it, I have tried encoding the session data, converting the variables to strings, bypassing the session handler and creating my own table to store the registry settings and always I get the same result. The only thing I can come up with is that it is an addressing issue. In order to save the data PDO is trying to reference an address that is no longer valid, that may have destructed after the page was included or something like that. The only reason I suspect that is because take a look at the variable "before". This is actually a random number generated by rand(100, 1000) and stored in a variable called $randomNumber. You will notice that in the registry settings this is 814, but it is saved as 970. If I refresh all the numbers change but the pattern remains the same which leads me to think the PDO is actually referencing an address location and not the string data that is passed. In this case the address location must containing the function "rand(100, 1000)" so it's recalculating the random number. Tried this with time() and got the same result. Therefore I think it trying to reference the address of the variables that were set in the include file, ie, the missing ones, and finding the reference no longer valid. It's a long shot but maybe, just maybe.....Not sure what to do now.......Could this be a PHP bug??

Sessions Before
NULL
Registry Settings
a:8:{s:6:"before";i:814;s:5:"start";s:7:"started";s:13:"Default Page ";s:5:"index";s:3:"one";s:1:"1";s:3:"two";i:2;s:5:"three";i:3;s:3:"end";s:5:"ended";s:5:"after";s:5:"after";}
Saving to Sessions
a:8:{s:6:"before";i:814;s:5:"start";s:7:"started";s:13:"Default Page ";s:5:"index";s:3:"one";s:1:"1";s:3:"two";i:2;s:5:"three";i:3;s:3:"end";s:5:"ended";s:5:"after";s:5:"after";}
Sessions After
a:8:{s:6:"before";i:814;s:5:"start";s:7:"started";s:13:"Default Page ";s:5:"index";s:3:"one";s:1:"1";s:3:"two";i:2;s:5:"three";i:3;s:3:"end";s:5:"ended";s:5:"after";s:5:"after";}
Database Sessions After
registry|s:97:"a:4:{s:6:"before";i:970;s:5:"start";s:7:"started";s:3:"end";s:5:"ended";s:5:"after";s:5:"after";}";
waynes888
Forum Newbie
Posts: 7
Joined: Mon Apr 05, 2010 7:44 pm

[SOLVED] Re: database sessions issue

Post by waynes888 »

After spending many hours trying to resolve this issue I finally stumbled on the solution and I thought I would just add it here so others can benefit.

I suspected that it was an addressing issue so I changed registry class to a singleton class so it is only ever instantiated once and then it worked! Session data was now getting written to the database correctly when set in include files. I suspect the issue was that the registry was getting instantiated again in the include file by Registry::set(variable) and once the include file was loaded that registry was effectively destroyed even though the variables appeared to be in the registry and sesssion, the actual address space was no longer valid and PDO obviously was trying to access the address space. My theory anyway, I am no expert but it sounds logical to me.

However, is the issue with is is_dir & file_exists still remains. Any direct logical evaluation using those kills session writing to the database (see previous examples). Again I suspect that it's an addressing issue but my workarounds work fine so I will leave it at that.

Hope this may help others, it highlights the need & importance of using singleton classes.
Post Reply