Pourquoi faire ça ?
Il y a plusieurs raisons à chercher à stocker ses sessions en base de données :
- même si pour ma part je suis à l’abri de ça, la première raison peut-être si vous faites du load-balancing : par défaut, les sessions sont stockées dans des fichiers dans des sombres recoins du serveur. Prenons donc le cas suivant : votre utilisateur visite votre site, créé une session parce qu’il s’est par exemple loggé sur l’interface privée pour passer une commande de missiles nucléaires (en cherchant, il doit bien y avoir des sites de B2T – Buyer to Terrorist – autre que ebay), rempli bien son panier, et au moment de valider sa commande il est basculé sur l’autre serveur pour gérer l’affluence. Manque de chance, sa session n’est pas sur l’autre serveur, et votre utilisateur doit se reloger et refaire son panier.. Plutôt moyen. En ayant tout dans une seule base de données, le problème est réglé, vos terroristes sont content.
- plus abordable pour le petit développeur, si tout est en base de données, on peut en une requête SQL afficher dans l’interface d’admin le nombre d’utilisateurs connectés.
- de la même façon, on peut aller analyser plus simplement le comportement de ses visiteurs suivant ce que l’on stocke dans la variable session.
- suivant les quelques bench que j’ai pu voir ça et là , pour les sites à forte affluence les requêtes SQL sont plus rapides que les accès fichier au delà d’un certain seuil (info à prendre telle quelle, j’ai pas de graphique pour l’instant).
- parce que ça fait hype dans les soirées geeks de dire que tes sessions tu les stocks en BDD
Et c’est dur ?
CREATE TABLE `ws_sessions` ( `session_id` varchar(255) binary NOT NULL default '', `session_expires` int(10) unsigned NOT NULL default '0', `session_data` text, PRIMARY KEY (`session_id`) ) TYPE=InnoDB;
class session { // session-lifetime var $lifeTime; // mysql-handle var $dbHandle; function open($savePath, $sessName) { // get session-lifetime $this--->lifeTime = get_cfg_var("session.gc_maxlifetime");
// open database-connection
$dbHandle = @mysql_connect("server","user","password");
$dbSel = @mysql_select_db("database",$dbHandle);
// return success
if(!$dbHandle || !$dbSel)
return false;
$this->dbHandle = $dbHandle;
return true;
}
function close() {
$this->gc(ini_get('session.gc_maxlifetime'));
// close database-connection
return @mysql_close($this->dbHandle);
}
function read($sessID) {
// fetch session-data
$res = mysql_query("SELECT session_data AS d FROM ws_sessions
WHERE session_id = '$sessID'
AND session_expires > ".time(),$this->dbHandle);
// return data or an empty string at failure
if($row = mysql_fetch_assoc($res))
return $row['d'];
return "";
}
function write($sessID,$sessData) {
// new session-expire-time
$newExp = time() + $this->lifeTime;
// is a session with this id in the database?
$res = mysql_query("SELECT * FROM ws_sessions
WHERE session_id = '$sessID'",$this->dbHandle);
// if yes,
if(mysql_num_rows($res)) {
// ...update session-data
mysql_query("UPDATE ws_sessions
SET session_expires = '$newExp',
session_data = '$sessData'
WHERE session_id = '$sessID'",$this->dbHandle);
// if something happened, return true
if(mysql_affected_rows($this->dbHandle))
return true;
}
// if no session-data was found,
else {
// create a new row
mysql_query("INSERT INTO ws_sessions (
session_id,
session_expires,
session_data)
VALUES(
'$sessID',
'$newExp',
'$sessData')",$this->dbHandle);
// if row was created, return true
if(mysql_affected_rows($this->dbHandle))
return true;
}
// an unknown error occured
return false;
}
function destroy($sessID) {
// delete session-data
mysql_query("DELETE FROM ws_sessions WHERE session_id = '$sessID'",$this->dbHandle);
// if session was deleted, return true,
if(mysql_affected_rows($this->dbHandle))
return true;
// ...else return false
return false;
}
function gc($sessMaxLifeTime) {
// delete old sessions
mysql_query("DELETE FROM ws_sessions WHERE session_expires < ".time(),$this->dbHandle);
// return affected rows
return mysql_affected_rows($this->dbHandle);
}
}
$session = new session();
session_set_save_handler(array(&$session,"open"),
array(&$session,"close"),
array(&$session,"read"),
array(&$session,"write"),
array(&$session,"destroy"),
array(&$session,"gc"));
session_start();
// etc...Et avec Symfony, c’est gérable ?
Mais oui, et en plus c’est tout simple (merci à Alister Stead pour son billet là dessus) !
Création de la table
Il faut une table qui va nous stocker ça. Voici la table à rajouter dans votre schema.yml pour Doctrine :
Session:
columns:
id:
type: string(32)
primary: true
notnull: true
session_data:
type: clob(65535)
notnull: true
session_time:
type: integer
notnull: trueOn fait son doctrine:build-all et hop ça, c’est fait !
Configuration de Symfony
Il ne vous reste plus qu’Ã configurer Symfony pour utiliser cette table pour stocker les sessions. Ajoutez ceci dans la section all du fichier apps/***/config/factories.yml :
storage:
class: sfPDOSessionStorage
param:
db_table: session # Table où seront stockées les sessions
db_id_col: id # Clé primaire
db_data_col: session_data # Champ qui contiendra les données
db_time_col: session_time # Champ qui contiendra la date de la sessions
database: doctrine # Nom de la connection à la base de données à utiliserEt voilà c’est fini, allez sur votre appli, assurez d’aller dans une section où les sessions sont utilisées et allez jeter un oeil à votre base de données ;)
Dernier truc sympa, vous pouvez aussi aisément séparer les sessions de vos appli frontend et backend : si par exemple la session de votre frontend ne sert qu’à enregistrer des infos sur vos visiteurs comme par exemple la dernière recherche effectuée, pourquoi polluer cette table avec les sessions des administrateurs du site ?
Have fnu coding!
Références
- Documentation de la function session_set_save_handler sur php.net
- Classe de Stalker pour gérer les sessions en PHP avec MySQL
- Version Symfony de la part d’Alister Stead
- Page de Symfony 1.4 sur les factories (section storage)










En fonction de la taille des session (nom ou quantité des données stockés) on pourrait même envisager d’utiliser des tables HEAP plutôt qu’INODB. Et permettrait d’avoir des réponse très réactives au détriment d’une faible capacités de données
Par contre, une chose à savoir et que je n’ai pas vu dans l’article, c’est que, contrairement à la gestion de session standard, session_set_save_handle ne gère pas nativement les locks c’est a vous de l’implémenter. Ce qui signifie qu’un utilisateur qui lance en simultané 2 scripts (appels ajax par exemple ou 2 onglets) peuvent potentiellement perdre des données, par exemple :
- Appel A lit la session (a=1, b=2)
- Appel B lit la session (a=1, b=2)
- A modifie la session (a=5) et enregistre le tout (a=5, b=2)
- B modifie la session (b=6) et enregistre le tout (a=1, b=6)
-> on a perdu la modification du script A
C’est a vous d’implémenter dans la partie open et close, et veiller a ce que la session ne reste pas verrouillée lorsqu’un script plante brutalement.
Encore un point, les loadbalancer permettent généralement de lire les cookies pour router les requêtes d’un même utilisateur vers le même front. Mais il est vrai que dans le cas ou un front s’arrête, la session est perdu et l’utilisation des session en base est grandement bénéfique.
Bonsoir,
Merci pour ces précisions, je n’avais pas pensé au type de la table (bien qu’effectivement les benchs que j’ai pu voir – que je ne retrouve bien sûr plus – mettaient ceci en avant). Je ne promet rien, mais je vais essayer de faire un benchmark des différentes solutions.
Le point que tu soulignes – le fait que le session_set_save_handle ne gère pas les locks – n’est pas dans l’article pour la simple et bonne raison que je l’ignorais, merci de l’explication !
Enfin, je n’ai pas d’expérience sur le load balancing, juste quelques notions, alors merci aussi pour ta dernière précision :)