Sessions in PHP (advanced concepts)

Disclaimer: Only some advanced concepts of PHP sessions are considered here. If you have not worked with PHP sessions before, please refer to PHP Manual for examples of work with sessions. Unfortunately the basic concepts are out of scope of this article.

The HTTP protocol is stateless (so it does not have any built-in mechanism to store variables/array etc. between site page loads). Still in PHP we often need variables/arrays from one site page to be also available at another page of the same site. To make it possible we need to use some special mechanism. Usually sessions or cookies are used.

Let us consider what sessions and cookies are and what is the difference between them.

Since sessions usually use cookies, we would briefly consider cookies first and sessions after them.

Cookies

Cookies are well explained by the PHP Manual. We would not repeat it here. We would list only some common facts to understand the cookie storage limitations.

Cookies are stored by the browser in text files at the client-side. This type of storage has some limitations:

  • Storing any sensitive data (e.g. passwords) in cookies is highly not recommended. Anyone who has access to the user computer could extract the data since cookies are stored at the user computer hard drive.
  • Cookies could be turned off in the browser by user. So we can not rely on the browser have to cookie-functionality available.
  • Size of cookies is limited. So you can store only small amounts of data in cookies.
  • After being set, cookies would be available in the array $_COOKIE only after the page reload.

Cookies are set by the function setcookie() (or by setrawcookie()).

Cookie data are transferred between browser and server by means of HTTP headers. Structure of the HTTP document is strictly defined: first all headers are sent, then new line, then the body of HTTP document. It means HTTP headers are always sent before any other data at the top of HTTP document.

Since setcookie() sends HTTP headers, it must be called before any output is sent to the browser (at least if you do not use Output Buffering).

Simple example of setting a cookie

Let us consider setting a cookie by a subdomain for the main domain and all its subdomains:

Let us set a cookie 'mycookie' with value '5' in the file page1.php located at the subdomain my1 of a domain example.com (i.e. my1.example.com/page1.php):

<?php
// No output should go to the browser before this line 
// Even a single space would cause a "headers already sent" error
setcookie('mycookie''5'time() + 3600'/''.example.com');

var_dump($_COOKIE['mycookie']); // will output NULL - we need to reload the page to see the cookie

Now let us read the cookie at a page of the main domain example.com/page2.php (page2.php is located at the the domain example.com, not its subdomain):

<?php
print_r
($_COOKIE['mycookie']); // outputs '5'

Now let us read the cookie at a page of another subdomain my2.example.com/page3.php (page3.php is located at the subdomain my2 of domain example.com):

<?php
print_r
($_COOKIE['mycookie']); // outputs '5'

To delete a cookie we must use setcookie() with the same parameters as for setting it except we set cookie value to boolean false or empty string and/or expiration date in the past:

setcookie('mycookie'''time() - 3600'/''.example.com'); 

Sessions

Session storage has 2 sides:

At the client side only the session ID is stored.
At the server side all the session-related data (variables, arrays etc.) are stored.

Any session is uniquely identified for a user by session name and session ID.

Session name and session ID

Session ID could be stored in a cookie (recommended) or transferred between pages as an URL parameter. Transferring the session ID as an URL parameter is not recommended for security reasons.

Still the process of passing or not passing session ID in a URL is controlled by the following parameters (this is an example of turning on these parameters in .htaccess):
php_flag session.use_trans_sid Off
php_flag session.use_cookies Off
php_flag session.use_only_cookies On

Session name - the default name is "PHPSESSID" (if not defined otherwise with the directive session.name in php.ini, .htaccess, httpd.conf, right in the script or even in the Windows registry).

Session name could be get or set with session_name(). Normally session_name() is called before session_start().

If you are using session_name() to set some particular session name, you need to be very careful:
Basically you could have several sessions with different names in the system. So if at one page you set the session name to some particular value, and at another page forget to do that (which would result in the default name being used) these would be 2 different sessions. And you could wonder for some time why session variables set at one page are not available at the other.

The session name is the name of the cookie variable which will be set at the client computer. As any cookie it is set (by the HTTP server, e.g. Apache) via an HTTP header like:
Set-Cookie: PHPSESSID=42f71a169274754a3f341f107db563cf; path=/

This header tells the browser to set a cookie with the name "PHPSESSID" and value "42f71a169274754a3f341f107db563cf" for the whole domain (path=/). No lifetime for this cookie is set in the header.

After this the browser sends an HTTP header back to the server (on each page request):
Cookie: PHPSESSID=42f71a169274754a3f341f107db563cf

You could see that in these examples that the cookie is set with the name which is the session name and the value which is the session ID.

Session ID - a string value which uniquely identifies the current user session (in the examples above it is equal to 42f71a169274754a3f341f107db563cf.).

Session ID could be get or set with session_id(). Normally session_id() is called before session_start().

If you would like to see how the session cookie is set in your browser, you could do it in browser Developer Tools. Under Windows in Google Chrome and Mozilla FireFox the Developer Tools panel could be opened by pressing Ctrl+Shift+I on the keyboard. Then you could go to tab Network, click on any file on the left and click on tabs Headers or Cookies which would get available.

In the earlier versions of Mozilla FireFox the add-on Live HTTP Headers was very popular for viewing HTTP headers.

Session lifetime

Default session cookie must live indefinitely till the browser gets closed.

The session liftime in this case is solely defined by the session.gc_maxlifetime setting.

But if several systems work on the same server and store session data in the same place, the script with minimum session.gc_maxlifetime will clean session data for all systems. Please see this note (also see some explanation here).

By default PHP stores session data in the file system. With file-based session handler, files are deleted by garbage collector by their access times (atime). So it would only if file system keeps track of access times (e.g. Windows FAT did not).

Also since garbage collector is run with some probability, session with a session cookie (lifetime=0) could live longer than session.gc_maxlifetime defines. In Debian/Ubuntu the garbage collector works by Cron. Please also see below for more details.

Session lifetime at the client side

Session life time at the client side (in browser) is defined by the session cookie lifetime. Which is defined by the setting session.cookie_lifetime in php.ini. By default it is equal to 0 which means "until the browser is closed". But you could set the session cookie lifetime to some fixed value (in seconds).

Please note: Setting nonzero value for session.cookie_lifetime could be not safe. This is because the session cookie will live even after all browser windows are closed (for the given period of time).

If despite this warning you still decide to set session.cookie_lifetime to a nonzero value, you could do it in php.ini, .htaccess, httpd.conf, right in the script or even in the Windows registry.

E.g. in .htaccess you could set it like:
php_value session.cookie_lifetime 3600
which would make the session cookie lifetime equal to 1 hour.

Session lifetime at the server side

By default PHP session data is stored in files at the server (you could store session data in a database of course - please see the sections right below).

When a session expires, these files should be deleted by the garbage collector.

The garbage collector is run randomly on session start. It means if e.g the site has very few visitors, the files with session data could be left at the server for very long time despite the session has expired long ago.

How the garbage collector is run is defined by the following constants in php.ini:

session.gc_maxlifetime - defines the number of seconds after which the session data at the server is considered as "garbage".

The default value for session.gc_maxlifetime is 1440. Which means the session data is considered as "garbage" after 24 minutes.

session.gc_probability and session.gc_divisor together define the probability with which the garbage collector is run on session start. The probability would be equal to session.gc_probability/session.gc_divisor (e.g. 1/100=0.01).

All 3 constants could be redefined in php.ini, .htaccess, httpd.conf, right in the script or under Windows even in the Windows registry.

E.g. session.gc_maxlifetime could be redefined in .htaccess as:
php_value session.gc_maxlifetime 18000
which would set the maximum lifetime of the session at the server to 5 hours (=1800 seconds).

In Debian/Ubuntu operating systems this default PHP mechanism of garbage collector work is turned off. The garbage collector is run by Cron.

E.g. under Ubuntu 14.04 session.gc_probability set to 0 in php.ini. And as it could be seen in
/etc/cron.d/php5
the garbage collector is run every 30 minutes by Cron.

Storing session data in databases

By default session data is stored in files. This is not considered the most efficient mechanism and used mostly on low load sites. A better practice is to store session data in a database which is considered below.

Storing session data in Redis

Redis is a key/value NoSQL database which is very performance-oriented. Due to the database high efficiency, it is good for storing session data.

The instructions below are given for Ubuntu 14.04.

Please note: You are supposed to be an experienced Ubuntu user or system administrator since you would need to assemble a few things from sources here.

To store session data in Redis first you need to install Redis at your server.
sudo apt-get update
sudo apt-get install redis-server
Personally I prefer to assemble it from sources (could be recommended for experienced Ubuntu users only!) as described here and here (please substitute Redis version in the articles with the version you need). It could be done to get more recent version of Redis than available from the standard Ubuntu repository.

Also I normally set these 3 options in /etc/redis/redis.conf:
bind 127.0.0.1
databases 1
appendonly yes
and restart redis server.
Please note: While everything should work without the 2nd and 3rd options, the bind option
bind 127.0.0.1
is really necessary. Redis is open for external connections by default. So without this bind option, anyone on the Internet would be able to connect to your Redis database and not only read your users session data (and any other data you ever store in Redis) but also change this data in any way. Of course it is a very good idea apart from the bind option also to close all the incoming ports with a firewall (under Ubuntu ufw could be a simple choice).

Now you have Redis but you have no means to connect to it from PHP. To make it possible to work with Redis from PHP you could use e.g. phpredis (this library is assembled from sources as described here).

After this you could make PHP store session data in Redis as described here. On a single server I normally do it in php.ini like this:
session.save_handler = redis
session.save_path = "tcp://localhost:6379?weight=1"
Do not forget to restart php5-fpm (if you use Nginx) or Apache (if PHP is used as a module) for the php.ini changes to take place.

As we discussed before, session data at an Ubuntu 14.04 server is deleted by the garbage collector run by Cron every 30 miuntes. With sessions in Redis, this data seems to be cleaned exactly (almost exactly - about 1 second delay is still possible) after session.gc_maxlifetime seconds have passed. Apparently Redis expire() or expireAt() function is used by the session handlers to do that (Redis has this built-in feature to expire keys after given number of seconds if one of these functions is used).

Storing session data in Memcached

Simple instructions for Ubuntu 14.04 could be like this:
sudo su
apt-get update
apt-get install memcached
apt-get install php5-memcache

Make sure that in the file /etc/memcached.conf the line
-l 127.0.0.1
is NOT commented.

Now enable session Memcached handler in php.ini:
session.save_handler = memcache
session.save_path = "tcp://localhost:11211"

And if Apache with PHP installed as a module used, restart Apache:
service apache2 restart
Or if Nginx with php5-fpm is used, restart the php5-fpm service:
service php5-fpm restart

Also you could read this very good article about Memcached installation under Ubuntu 12.04.

Please note: Normally all data in Memcached is kept in RAM and does not persist on server reboot. So if you use Memcahed session handler, please be ready to lose your session data on server reboot (e.g. all your logged in users could get logged out suddenly).

Storing session data in MySQL

To store session data in MySQL you need to implement custom session handlers in PHP

A very simple example of MySQL session handler is given here. It is given just to make it possible to grasp the idea. But in your system you would definitely need a solution more complicated than this.

Also you would need at least PHP 5.4 to run the example. Before this version it was necessary to create separate handler functions and register them with session_set_save_handler() instead of implementing the interface SessionHandlerInterface.

First create a table in MySQL to store session data:
CREATE TABLE `session` (
  `id` CHAR(32) NOT NULL,
  `data` BLOB,
  `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
The following script contains a class implementing SessionHandlerInterface. It defines a simple MySQL-based session handler. The script must be included at the very top of every page where sessions are necessary:

<?php
// These lines should be somewhere in a config file
define('DB_HOST''localhost');
define('DB_USER''myusername');
define('DB_PASSWORD''mypassword');
define('DB_DATABASE''mydatabase');

// Define a class 
class MySqlSessionHandler implements SessionHandlerInterface {
    
    protected 
$_mysqli;
    
    
/**
     * Initializes session. In our simple example just opens th database connection here.
     * 
     * @param string $savePath - the path to store/retrive the session - we ignore it here.
     * @param string $sessionName - session name - in this simple example we ignore it too.
     * @return boolean true on success or false on failure
     */
    
function open($savePath$sessionName) {
        
        
$this->_mysqli = @new mysqli(DB_HOSTDB_USERDB_PASSWORDDB_DATABASE);
        if(
$this->_mysqli->connect_error) {
            return 
false// you would probably want to add some error logging here
        
}
        
        return 
true;

    }
    
    
/**
     * Normally closes the current session.
     * In this simple example we are just closing the previously opened database connection.
     * 
     * @return boolean true on success or false on error.
     */
    
public function close() {
        return 
$this->_mysqli->close();
    }
    
    
/**
     * Writes session data $sessionData to a row identified by $sessionId.
     * 
     * @param string $sessionId - session id
     * @param string $sessionData - session data (already serialized by PHP)
     * @return boolean true on success or false on error.
     */
    
public function write($sessionId$sessionData) {

        
$sessionId $this->_mysqli->real_escape_string($sessionId);
        
$sessionData $this->_mysqli->real_escape_string($sessionData);

        
$q "REPLACE INTO session (id, data) VALUES ('$sessionId', '$sessionData')";
        
        
// you could probably want to add logging of $mysqli->error here on false before returning the result
        
return $this->_mysqli->query($q); 

    }

    
/**
     * We read from MySQL data which has been written there by method write().
     * 
     * @param string $sessionId - session id
     * @return string - a string of data found in the database by $sessionId 
     *                           (empty string if nothing has been found)
     */
    
public function read($sessionId) {
        
        
$q "SELECT data FROM session WHERE id = '" $this->_mysqli->real_escape_string($sessionId) . "'";
        
$res $this->_mysqli->query($q);
        if(
false === $res) {
            return 
''// you would probably want to add logging of $mysqli->error here
        
}
        
$row $res->fetch_assoc();
        
        
$res->free();

        return (string) 
$row['data'];
        
    }


    
/**
     * Destroys a session.
     * In our case, deletes a row from the table `session` uniquely identified by session id $sessionId.
     * 
     * @param string $sessionId - session id
     * @return boolean true on success or false on error.
     */
    
public function destroy($sessionId) {

        
$q "DELETE FROM session WHERE id='" $this->_mysqli->real_escape_string($sessionId) . "'";
        
        
// you could probably want to add logging of $mysqli->error here on false before returning the result
        
return $this->_mysqli->query($q); 

    }

    
/**
     * Cleans up expired sessions.
     * 
     * @param string $maxlifetime - sessions that have not updated for the last maxlifetime seconds
     *                                  will be removed
     * @return boolean true on success or false on error.
     */
    
public function gc($maxlifetime) {

        
$maxlifetime = (int) $maxlifetime;
        
$q "DELETE FROM session WHERE DATE_ADD(`time`, INTERVAL $maxlifetime SECOND)<NOW()";
        
        
// you could probably want to add logging of $mysqli->error here on false before returning the result
        
return $this->_mysqli->query($q); 

    }

}
    
session_set_save_handler(new MySqlSessionHandlertrue);
session_start();

Of course since session_start() sends HTTP headers (since session cookie as any cookie is sent via HTTP headers), this script must be executed before any output has been sent to the browser. Or (with a cookie-based session and without Output Buffering) you would get "headers already sent" error.

Back to Contents