The internet is by default a stateless, anonymous platform. A website cannot track which users are present or logged in without some help. In general, sessions are used to introduce a way to monitor an application’s state between requests. It is not often that developers need to look closely at how PHP sessions are managed by their server, as PHP provides reasonably good session handling straight out of the box. As we are working on a scalable application, that may run a single user’s requests on multiple servers, or multiple instances of our application, lets explore how sessions are stored and used.

Default PHP Session Handling

By default, PHP sessions are written to text files on the server where the requests for sessions are made. In general, this works quite well. A user visits a website, which stores a session on its server to track the user, and on each request to that website, the user data is made available by the server. What if we now introduce multiple servers? Let’s imagine we have a web app that runs across 2 servers. A user signs in to our web application, and all requests are handled by Server A. On the next page load, the user is moved to Server B, as it has less traffic at the time. The User’s browser now send in it’s session ID, but no session is found on Server B, because it was originally stored on Server A. Clearly, this is going to cause some problems! How can we share this session information between instances of our application on different servers?

Google App Engine Session Handling

Google App Engine, which we will be using to create our scalable application stores sessions a little differently, to solve the issue we just discovered! Rather than storing sessions in text files on the server, Google App Engine stores session information in its shared memchache service. This allows sessions to be shared between instances, which sounds good in theory. In reality however, memcache stores data in memory, and can be flushed or emptied at any time to recover from errors, or to free memory for other purposes. By default, App Engine Apps have access to a ‘non-dedicated’ memcache, in which resources, while secure between apps, are shared between apps. If another app suddenly places a huge demand on the memcache, our sessions may be lost. This would mean every user that was logged in, would be instantly logged out. Apps can have a dedicated memcache, however this comes at extra cost, and our application would not currently benefit from a dedicated memcache service. So what can we do?

Custom Session Handling

We can develop our own way of storing our sessions! Luckily for us, PHP allows you to implement custom session handling functions. Google App Engine’s shared memcache is very fast and efficient, however as we just found out, may not be as reliable as we would like at times. With this in mind, we need a backup plan! What do we do if we look for a session in memcache, but it is not available. We can use a combination of memcache, and database storage. Database storage will be slower, however we can look for our session data in memcache, and if it is not found, we can then look for it in our database. This way we benefit from the speed of memcache, and will fall back to the reliability of a database when needed. In our application we will be using Google App Engine’s Datastore, which is a very efficient NoSQL style datastore. We will look further into Datastore later, so don’t worry if you don’t follow exactly what is happening with datastore here. We are just focussing on the session handling aspects in this article.

PHP session_set_save_handler()

We will be using PHP’s session_set_save_handler() function to set our custom session handling functions. Let’s first have a look at the functions we need to provide to PHP in order to manage our sessions. You can find more information at http://php.net/manual/en/function.session-set-save-handler.php

  • open – open is called when a session is opened or started. This is called when session_start() is used to open a session.
  • close – close is called when a session is closed.
  • read – read is called to lookup the session data after a session is opened
  • write – write is used to store session data
  • destroy – destroy is used to delete or destroy the session and its data
  • gc – is used to collect garbage, or clean up our session store when expired data is present

With this information, we can begin to design and build our custom session handler. First, we need to create our CustomSessionHandler class, and then we need to write functions to store and retrieve our session information from our Datastore and Memcache.

<?PHP // core/sessionhandler.class.php

	class CustomSessionHandler{
	
		private $Store;
		private $Cache;

		private $ID;
		private $Life;
	
		public function __construct($Store, $Cache){
			$this->Store = $Store;
			$this->Cache = $Cache;
		}
	
		public function readCache($ID){
			$Session = $this->Cache->get($ID);
			return $Session;
		}

		public function writeCache($ID, $Data){
			$this->Cache->set($ID, $Data, 0, $this->Life);
		}

		public function deleteCache($ID){
			$this->Cache->delete($ID);
		}
	
		
	}
	
?>

First we created our CustomSessionHandler class. Next, we defined a few properties to help us manage our sessions. These were $Store, where we will keep a connection to our Datastore, $Cache, where we will keep a connection to our Memcache, $ID, where we can keep our session ID, and Life, where we can store our session life span in seconds.

Next, we added functions to handle reading, writing and deleting from memcache. Reading from memcache is quite easy, you simply call get($ID) on the memcache instance, and the session data for that ID will be returned. If there is nor result found, false will be returned.

Writing to the cache is also quite simple, we just call set() and provide the session ID, Data, 0 (To specify no compression), and our expire time, or lifetime which we have stored in $this->Life. To Delete from our cache, we simply call delete() and provide the session ID.

To work with Datastore, we need a class named Session, and we will need a way to access the datastore, and for this we will use a class named SessionStore. Both of these classes extend classes from a library by Tom Walder, which is a Google Datastore library for PHP. We will talk more about datastore later in this series, however I have included the source of the Session and SessionStore classes below.

<?PHP // core/sessionstore.class.php
	class SessionStore extends GDSStore{
		public function buildSchema(){
			$Schema = new GDSSchema('Session');
			$Schema->addString('Data', FALSE);
			$Schema->addDateTime('LastRequest', FALSE);
			$Schema->addDatetime('ExpireTime', TRUE);
			return $Schema;
		}

		public function __construct($Gateway){
			parent::__construct($this->buildSchema(), $Gateway);
			$this->setEntityClass('Session');
		}

	}	
?>
	
<?PHP // core/session.class.php
class Session extends GDSEntity{

}
?>
	
	

Our Session class is simply an extension of the GDS\Entity class, and the SessionStore class simply defines the structure of a Session entity, and sets Session as the type of object to submit and receive from Datastore. Again, we will explore datastore more later in this series. Note also, that in the constructor of our SessionStore, we need to pass in a $Gateway, which is another job for the Google Datastore library.

Now that we have our Session and SessionStore classes defined, lets have a look at how we can use them to complete our datastore read/write/delete functions.

<?PHP // core/sessionhandler.class.php

	class CustomSessionHandler{
	
		private $Store;
		private $Cache;

		private $ID;
		private $Life;
	
		public function __construct($Store, $Cache){
			$this->Store = $Store;
			$this->Cache = $Cache;
		}
	
		public function readCache($ID){
			$Session = $this->Cache->get($ID);
			return $Session;
		}

		public function writeCache($ID, $Data){
			$this->Cache->set($ID, $Data, 0, $this->Life);
		}

		public function deleteCache($ID){
			$this->Cache->delete($ID);
		}
	
		public function readStore($ID){
			$Session = $this->Store->fetchByName($ID);
			if($Session == null){
				return '';
			} else{
				return $Session->Data;
			}
		}

		public function writeStore($ID, $Data){
			$Session = new Session();
			$Session->setKeyName($ID);
			$Session->Data = $Data;
			$Session->LastRequest = new DateTime();
			$Session->ExpireTime = new DateTime('+'.$this->Life.' seconds');
			$this->Store->upsert($Session);
		}

		public function deleteStore($ID){
			$Session = new Session();
			$Session->setKeyName($ID);
			$this->Store->delete(array($Session));
		}

	
	}
	
?>

To write to our store, we have created a writeStore() function, that takes in the session ID, and the session Data. To write it to the store, we first create an instance of our Session class. We then set our ID using $Session->setKeyName(), which will set the ID or Key in datastore to the one we have provided. We then set the session’s Data, Last Request and ExpireTime values. The next step, calls on our SessionStore object to upsert (update/insert) our Session object. That’s all there is to it, set our values, and a simple upsert command.

To read from the store, using our readStore() function, we call on the SessionStores fetchByName() function, and provide our session ID. Once we have our session, we can then return just the data we need, which is found in $Session->Data. You will note in our readStore() function, we return an empty string if no data is found. This is because the default PHP session handler returns an empty string if no valid session data is found.

To delete from datastore, using our deleteStore() function, we need to provide a Session entity with the same ID to our SessionStore’s delete method. We could lookup the current session, and then send it back to our session store to delete, but in this case it is just as simple to create a new Session, set its KeyName to our session ID, and then call the Store’s delete function.

Now that we have added support for both memcache and datastore, let’s add in the session handling functions we need to provide to PHP’s session handler.

<?PHP // core/sessionhandler.class.php

class CustomSessionHandler{
	
	private $Store;
	private $Cache;
	
	private $ID;
	private $Life;
	
	public function __construct($Store, $Cache){
		$this->Store = $Store;
		$this->Cache = $Cache;
	}
	
	public function open($path, $name){
		$this->Life = 900;
	}
	
	public function close(){
		$this->garbage();
	}
	
	public function read($ID){
		//Try reading from cache first
		$Data = $this->readCache($ID);
		//Returns false if no data found
		if(!$Data){
			$Data = $this->readStore($ID); //returns '' if no data found, which is expected for session handling
		}
		return $Data;
	}
	
	public function write($ID, $Data){
		//Write to cache
		$this->writeCache($ID, $Data);
		//Write to store
		$this->writeStore($ID, $Data);
	}
	
	public function destroy($ID){
		//Delete from cache
		$this->deleteCache($ID);
		
		//Delete from store
		$this->deleteStore($ID);
	}
	
	public function garbage(){
		//No garbage collection available for cache
		
		//Query Keys
		$Garbage = $this->Store->fetchAll("SELECT * FROM Session WHERE ExpireTime < @now", ['now' => newDateTime()]);
		$this->Store->delete($Garbage);
	}
	
	public function readStore($ID){
		$Session = $this->Store->fetchByName($ID);
		if($Session == null){
			return '';
		} else{
			return $Session->Data;
		}
	}
	
	public function writeStore($ID, $Data){
		$Session = new Session();
		$Session->setKeyName($ID);
		$Session->Data = $Data;
		$Session->LastRequest = new DateTime();
		$Session->ExpireTime = new DateTime('+'.$this->Life.' seconds');
		$this->Store->upsert($Session);
	}
	
	public function deleteStore($ID){
		$Session = new Session();
		$Session->setKeyName($ID);
		$this->Store->delete(array($Session));
	}
	
	public function readCache($ID){
		$Session = $this->Cache->get($ID);
		return $Session;
	}
	
	public function writeCache($ID, $Data){
		$this->Cache->set($ID, $Data, 0, $this->Life);
	}
	
	public function deleteCache($ID){
		$this->Cache->delete($ID);
	}
	
}
	
?>

First, we added the open function, and inside we have simply set our Life value to 900 seconds, which is 15 minutes. This means if a user is inactive for 15 minutes, they will be logged out. The open function is normally where you would create a database connection, but in our case, this is already handled by our SessionStore object.

Our close() function, simply calls our garbage() function to clean up any expired sessions. You would normally use this to tidy up your database connections. But again, our SessionStore object handles our connection.

The read() function is where things start to get interesting. PHP will call this function and provide the session ID to us. Our goal is to load our session from memcache, as it is the quickest option, and if we cannot find the session there, we then search our datastore. Memcache will return false if it does not find the ID we request, so by calling $Data = $this->readCache($ID), we will either have a $Data variable containing false, or containing our data. A quick if() check will tell us if we need to search our dataStore. If we do need to, we then call our readStore function. We now should have a $Data variable which contains our session data, or an empty string. We can return this as is, as PHP expects an empty string if no data was found.

In our write function, we simply call on both our writeCache() and writeStore() functions, as we want to store our session data in both locations, cache for speed, and store for redundancy.

Our destroy function calls our deleteCache() and deleteStore() functions to ensure the session is removed in both places.

Our garbage function then uses a simple query on our SessionStore, to find all expired sessions that remain in the store. These sessions are then passed back to the SessionStore’s delete() method to be removed from the store. We cannot perform garbage collection on memcache, as memcache has no search functionality. When we created our session in memcache, we set an expire time of 900 seconds. Memcache will automatically purge expired content periodically. As we cannot guarantee that expired sessions do not exists, we will have to check that sessions are valid when authenticating users later on!

If you look at our SessionHandler’s construct() method, you will see we have dependencies! And if you look at our SessionStore’s constructor you will see even more dependencies!!! Let’s now add these classes to our Container class, so the dependencies can be handled for us.

// core/container.class.php

	public static function newGateway($Namespace){
		return new GDSGatewayProtoBuf(null, $Namespace);
	}
	
	public static function newSessionStore(){
		return new SessionStore(self::newGateway('Session'));
	}
	
	public static function newCache(){
		return new Memcache;
	}
	
	public static function newSessionHandler(){
		return new CustomSessionHandler(self::newSessionStore, self::newCache());
	}

We have now set up each of our classes, and their dependencies in our Container class. The newGateway function sets up a new connection gateway to our Datastore, and allows us to specify a namespace. We will discuss this further when we look at datastore, but for now we have just set the namespace to Session.

All we have left to do now, is to set our application to use our session handler. We can do this in our App class.

// core/app.class.php

	public function __construct($Request){
		$this->setSessionHandler();
		
		$this->URL = $this->parseUrl($Request);
		
		if(isset($this->URL[0])){
			if(class_exists($this->URL[0])){
				$this->Controller = $this->URL[0];
			}
		}
		$ControllerName = 'new'.$this->Controller;
		$this->Controller = self::$ControllerName();
		
		if(isset($this->URL[1])){
			if(method_exists($this->Controller, $this->URL[1])){
				$this->Method = $this->URL[1];
			}
		}
		
		$this->Params = $URL ? array_values($URL) : [];
		
		call_user_func([$this->Controller, $this->Method], $this->Params);
		
	}
	
	private function setSessionHandler(){
		$SessionHandler = self::newSessionHandler();
		session_set_save_handler(
			array(&$SessionHandler, 'open'),
			array(&$SessionHandler, 'close'),
			array(&$SessionHandler, 'read'),
			array(&$SessionHandler, 'write'),
			array(&$SessionHandler, 'destroy'),
			array(&$SessionHandler, 'garbage')
		);
		session_start();
	}

We have added a setSessionHandler() function to our App class, and we have used it to call PHP’s session_set_save_handler() function in order to set our custom sessionhandler functions. We have then called this function within our App class’s construct() method. We now have custom session handling! And it will be automatically set up for us each time we create an instance of our App class.

We can now be assured that our sessions will be available across instances of our application and across multiple servers, and if sessions are purged from memcache for any reason, we will fall back on our datastore records.

You may notice, I have not included any PHPUnit tests for our custom session handler. The reason for this is we cannot easily connect to the datastore emulator provided by the Google App Engine SDK from the command line. As a result, we will need to create a mock datastore class to provide our own emulation of our datastore connections. We will look at this later in the series when we get a little bit more involved in working with datastore.