Cet article a été rédigé pour le numéro de septembre du magazine PHP Solutions et s'intéresse aux sessions PHP. Le mécanisme natif des sessions a été largement présentés jusqu'à maintenant dans ce magazine depuis son existence. L'objectif de ce nouvel article consiste à expliquer comment encapsuler la session PHP dans un objet métier et comment rendre le code flexible afin d'adapter le stockage des sessions dans une base de données MySQL.
Aujourd'hui, la grande majorité des sites dynamiques écrits en PHP utilisent le mécanisme natif des sessions. Cet outil permet de persister entre deux requêtes HTTP distinctes des informations propres à l'utilisateur courant. La configuration par défaut de PHP stocke les données sur le serveur dans des fichiers textes. Dans certaines situations, ce moyen de stockage des sessions n'est pas le plus approprié et peut se voir remplacé par un moteur de stockage différent, une base de données par exemple.
L'objectif de ce cours consiste dans un premier temps à développer une approche orientée objet des sessions natives de PHP afin de simplifier leur utilisation. Puis nous chercherons à étendre ce mécanisme en vue de stocker les sessions dans une base de données MySQL sans avoir à modifier l'API principale en profondeur.
Veuillez noter cependant que cet article ne couvre pas les bases du système natif des sessions. Le numéro du mois de juin 2010 de ce magazine dédie un article complet à ce sujet et nous ne pouvons que vous inviter à le lire attentivement avant de vous plonger dans les prochaines lignes.
Rappel sur les sessions PHP
Le mécanisme des sessions PHP a été instauré dans le langage dans le but de proposer un moyen de persister des informations entre les requêtes HTTP. En effet, le protocole HTTP est sans état (stateless) et fonctionne en mode non connecté. Cela signifie que deux requêtes HTTP successives émises depuis la même adresse IP sont complètement indépendantes l'une de l'autre.
La reconnaissance de l'utilisateur entre deux requêtes HTTP est réalisée au moyen d'un cookie de session contenant un jeton unique d'identification en guise de valeur. Selon la configuration de PHP (session.use_cookies, session.use_only_cookies et session.use_trans_id), l'identifiant de session peut également être propagé par l'intermédiaire d'une variable complémentaire dans l'url (GET).
En terme d'API, PHP fournit une vingtaine de fonctions
(http://fr.php.net/manual/en/ref.session.php) utiles à la configuration et la
manipulation des sessions. A cette liste de fonctions s'ajoute le tableau
superglobal $_SESSION chargé d'accueillir les données de session. Ce tableau
est accessible en lecture comme en écriture, et est automatiquement initialisé
au démarrage du script.
Le listing 1, page1.php, donne un exemple d'usage simple de l'API des
sessions. La première ligne appelle la fonction session_name() avec le
paramètre phpsolutions qui fixe le nom de la session, et donc du cookie de
session. Par défaut, le nom de la session est fixée avec la valeur PHPSESSID.
Ensuite, la fonction session_start() démarre (ou restaure) la session courante
tandis que la ligne suivante, session_regenerate_id(), force PHP à recréer un
nouvel identifiant de session afin d'éviter une éventuelle fixation de la
session. Enfin la dernière ligne déclare une variable de session persistante, color, dans le tableau $_SESSION et l'initialise avec la valeur blue.
<?php // listing 1 session_name('phpsolutions'); session_start(); session_regenerate_id(); $_SESSION['color'] = 'blue';
Le listing 2, page2.php, réalise les mêmes opérations que dans le morceau de
code de la page 1. La différence se situe à la dernière ligne qui lit la valeur
de la variable de session color et l'affiche sur la sortie standard. Bien sûr,
les trois premières lignes communes aux deux fichiers devraient être mutualisées
dans un fichier séparé ou dans une fonction utilisateur afin d'éviter la
duplication du code.
<?php // listing 2 session_name('phpsolutions'); session_start(); session_regenerate_id(); echo $_SESSION['color'];
La plupart des développeurs PHP interrompent leur usage des sessions PHP à ces quelques lignes de code. Ces dernières font le travail qu'on leur demande et le font bien. Néanmoins, cette approche procédurale du code peut s'avérer contraignante lorsque l'application se compose de plusieurs centaines de fichiers et des milliers de lignes de code.
En effet, comment s'assurer que le code fonctionne bien ? Comment assurer sa réutilisation dans un autre projet ? Comment déplacer le stockage des sessions vers un autre système comme une base de données relationnelle ? Pour répondre à toutes ces questions, il convient de transformer l'approche procédurale du code en approche orientée objet. En somme, il s'agit de percevoir la session comme un objet métier PHP sur lequel s'appliquent des actions.
L'objectif de la seconde partie de ce cours consiste à développer une classe PHP représentant une session PHP. Celle-ci encapsulera les actions nécessaires à la configuration de la session, la lecture de variable ainsi que la fixation de variable.
Vers une approche orientée objet
Comme son nom l'indique, l'approche orientée objet nécessite de faire appel à
des objets. Par définition, on dit qu'un objet est une instance d'une classe.
Dit autrement, une classe est une sorte de moule à partir duquel sont issus des
objets. Il s'agit donc de créer une classe SessionStorage chargée d'encapsuler
la définition de la session courante.
Une session se définit également par un jeu de propriétés et d'actions. Les
propriétés sont les attributs d'un objet, c'est-à-dire les variables internes de
l'objet. La classe SessionStorage, présentée au listing 3, accueille trois
propriétés :
$name, une chaîne contenant le nom de la session (ex:phpsolutions),$useCookies, une valeur booléenne indiquant si la session courante transmet l'identifiant de session par un cookie,$options, un tableau servant à accueillir un ensemble d'options de configuration de la session.
La classe SessionStorage du listing 3 encapsule également un certains nombre
de méthodes remplissant chacune un besoin particulier. Les paragraphes qui
suivent présentent une à une ces méthodes. Notez que le code de la classe
SessionStorage est largement inspiré de celui qui constitue l'une des classes
du framework Open Source Symfony.
<?php class SessionStorage { static protected $isStarted = false; protected $name; protected $useCookies = true; protected $options = array(); public function __construct($name, array $options = array()) { $this->name = $name; $this->useCookies = (boolean) ini_get('session.use_cookies'); $cookie = session_get_cookie_params(); $this->options = array_merge(array( 'session_auto_start' => true, 'session_cache_limiter' => 'nocache', 'session_cache_expire' => 180, 'session_save_path' => null, 'session_id' => null, 'session_cookie_lifetime' => $cookie['lifetime'], 'session_cookie_path' => $cookie['path'], 'session_cookie_domain' => $cookie['domain'], 'session_cookie_secure' => $cookie['secure'], 'session_cookie_httponly' => isset($cookie['httponly']) ? $cookie['httponly'] : false ), $options); if (!ini_get('session.auto_start')) { session_name($this->name); if ($this->options['session_id']) { session_id($this->options['session_id']); } if ($this->options['session_save_path']) { session_save_path($this->options['session_save_path']); } if ($this->options['session_cache_limiter']) { session_cache_limiter($this->options['session_cache_limiter']); } if ('nocache' !== $this->options['session_cache_limiter'] && $this->options['session_cache_expire']) { session_cache_expire($this->options['session_cache_expire']); } if ($this->useCookies) { session_set_cookie_params( $this->options['session_cookie_lifetime'], $this->options['session_cookie_path'], $this->options['session_cookie_domain'], $this->options['session_cookie_secure'], $this->options['session_cookie_httponly'] ); } if ($this->options['session_auto_start'] && !self::$isStarted) { session_start(); session_regenerate_id(); self::$isStarted = true; } } } public function destroy() { $destroyed = session_destroy(); if ($destroyed) { if ($this->useCookies) { setcookie($this->name, '', time() - 42000, $this->options['session_cookie_path'], $this->options['session_cookie_domain'], $this->options['session_cookie_secure'], $this->options['session_cookie_httponly'] ); } $this->clear(); } return $destroyed; } public function write($key, $value) { $_SESSION[$key] = $value; } public function read($key) { return array_key_exists($key, $_SESSION) ? $_SESSION[$key] : null; } public function remove($key) { if (null !== $value = $this->read($key)) { unset($_SESSION[$key]); } return $value; } }
La première méthode, __construct(), est le constructeur de la classe. Elle a
pour double rôle de construire l'objet à l'appel du mot-clé new, mais surtout
d'initialiser ce dernier. Dans le cadre de notre session, le constructeur
réalise synthétiquement les trois étapes suivantes :
- Configurer le nom de la session,
- Configurer la session en fonction des valeurs transmises dans le tableau d'options passé en second paramètre,
- Démarrer ou restaurer la session.
La méthode read(), comme son nom l'indique, sert à lire une variable de
session depuis le tableau $_SESSION. La fonction array_key_exists() s'assure
ici que le nom de la variable de session passée en paramètre de la méthode
read() se trouve bien dans le tableau. Le test est important afin d'éviter de
lever un avertissement au cas où l'on essaierait d'atteindre un index inexistant
dans le tableau $_SESSION.
L'écriture de nouvelles valeurs dans la session est assuré par la méthode
write() qui accepte deux paramètres : le nom de la variable de session et sa
valeur. La méthode remove() offre la possibilité de supprimer une variable de
session en lui passant en paramètre le nom de cette dernière.
Enfin, la méthode destroy() a la responsabilité de détruire la session. Cette
méthode ne se limite pas seulement à l'appel de la fonction session_destroy().
En effet, elle vérifie également si l'identifiant de session est propagé par un
cookie. Si c'est le cas, un cookie vide et expiré est envoyé au navigateur.
Mise en pratique
A présent la classe SessionStorage est prête. Il convient de la tester à
l'aide d'un petit exemple pratique. Il s'agit d'une application web composée
d'une page web, index.php, divisée en deux parties : un formulaire et un
paragraphe de texte. Le formulaire demande à l'utilisateur de choisir sa couleur
favorite tandis que le paragraphe se charge d'afficher cette dernière.
Commençons tout d'abord par créer un dossier PHPSolutions/ sur la machine.
Celui-ci contiendra la structure initiale de l'application décrite ci-dessous :
- Le dossier
src/accueille la classeSessionStorage, - Le dossier
tmp/stocke les fichiers des sessions. Ce dossier doit donc être accessible en lecture comme en écriture par PHP, - Le dossier
www/abrite les tous les fichiers accessibles depuis un navigateur web comme le script principalindex.php.
Une fois l'architecture de l'application en place, il faut configurer le serveur
web Apache afin d'empêcher l'accès aux dossiers src/ et tmp/ depuis un
navigateur web. En résumé, il s'agit de définir le dossier www/ comme étant la
racine web du projet. C'est une bonne pratique de développement web de séparer
les fichiers accessibles publiquement de ceux qui ne doivent pas l'être. Ne
rendez publiques que les fichiers que vous souhaitez atteindre depuis votre
navigateur web. Tous les autres doivent être placés au niveau supérieur afin de
les protéger des utilisateurs mal intentionnés.
Pour y parvenir, il suffit de définir un nouvel hôte
virtuel (virtual host) dans le fichier de configuration httpd.conf
d'Apache. Dans les versions récentes d'Apache, il s'agit du fichier
httpd-vhosts.conf dont l'inclusion est parfois mise en commentaire à la fin du
fichier httpd.conf.
Le listing 4 présente le code de l'hôte virtuel à placer à la fin du fichier
httpd.conf avant de redémarrer le serveur Apache. Les chemins absolus de
l'hôte virtuel sont bien évidemment à remplacer par ceux équivalents au système
hébergeant l'application. La directive ServerName définit le nom de domaine à
utiliser pour atteindre le fichier index.php du répertoire www/. Les
directives AllowOverride et Allow from all indiquent respectivement que la
configuration Apache du répertoire www/ peut être surchargée à l'aide d'un
fichier .htaccess et que ce dossier est accessible par tout le monde.
# listing 4 <VirtualHost *:80> ServerName www.phpsolutions.local DocumentRoot "/Users/Hugo/Developpment/PHPSolutions/www" DirectoryIndex index.php <Directory "/Users/Hugo/Developpment/PHPSolutions/www"> AllowOverride All Allow from All </Directory> </VirtualHost>
Le domaine www.phpsolutions.local est un domaine local, c'est à dire qu'il
pointe sur l'adresse IP local 127.0.0.1. Il faut donc indiquer à la machine
comment résoudre ce nom de domaine localement. Pour ce faire, il suffit
d'ajouter la ligne du listing 5 au fichier hosts. Ce fichier se trouve dans le
dossier /etc sur les environnements Linux et dans
C:\Windows\System32\drivers\etc sur les plateformes Microsoft.
# listing 5 127.0.0.1 www.phpsolutions.local
Enfin, le listing 6 donne le code du fichier www/index.php. Ce fichier se
compose de deux parties : le code PHP qui traite la requête utilisateur et la
session, puis le code HTML destiné à l'affichage des données.
La première ligne de ce fichier inclut la définition de la classe
SessionStorage afin de pouvoir l'instancier quelques lignes plus bas. Le bloc
de lignes suivantes déclare une fonction utilisateur isValidColor() qui a pour
role de s'assurer que la valeur passée en paramètre est une couleur attendue
parmi la liste de valeurs possibles.
A partir de la ligne 9, la classe de session est instanciée afin de produire un
objet $session dont le nom du cookie de session est phpsolmag. L'option
session_save_path quant à elle définit le dossier dans lequel les fichiers des
sessions seront écrits. Ici, il s'agit du répertoire tmp/ de notre
application.
Ensuite, la fonction filter_input() récupère la valeur de la variable POST
color issue du formulaire. Si aucune valeur n'a été transmise, alors la
variable $color contiendra la valeur null par défaut.
Puis, le dernier bloc conditionnel s'assure que la valeur de la couleur
transmise en PHP est valide. Si c'est le cas, on fait appel à la session pour y
stocker la valeur de la couleur dans la variable de session favorite_color.
Une redirection est ensuite déclenchée à l'aide de la fonction header().
Enfin, la dernière ligne du bloc de code PHP tente de récupérer la valeur de la
variable de session favorite_color. Si la valeur existe alors elle est
affichée dans la partie HTML, sinon un message demande à l'utilisateur de bien
vouloir choisir sa couleur préférée parmi la sélection proposée.
<?php // listing 6 require __DIR__.'/../src/SessionStorage.php'; function isValidColor($color) { return in_array($color, array( 'rouge', 'vert', 'bleu', 'violet', 'orange', 'noir' )); } $session = new SessionStorage('phpsolmag', array( 'session_save_path' => __DIR__.'/../tmp' )); $color = filter_input(INPUT_POST, 'color'); if (isValidColor($color)) { $session->write('favorite_color', $color); header('Location: http://www.phpsolutions.local/index.php'); exit; } $color = $session->read('favorite_color'); ?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>Bienvenue</title> </head> <body> <form action="index.php" method="post"> <fieldset> <legend>Personnalisation</legend> <div> <label for="color">Couleur favorite</label> <select id="color" name="color"> <option value="rouge">Rouge</option> <option value="vert">Vert</option> <option value="bleu">Bleu</option> <option value="violet">Violet</option> <option value="orange">Orange</option> </select> <button type="submit">Ok</button> </div> </fieldset> </form> <?php if (!$color) : ?> <p> Vous n'avez pas encore choisi de couleur. </p> <?php else: ?> <p> Votre couleur favorite est le <strong><?php echo $color ?></strong>. </p> <?php endif ?> </body> </html>
Pour tester l'application, il suffit de se rendre dans un navigateur et de
saisir l'url http://www.phpsolutions.local.
Cette petite application prouve le fonctionnement de l'objet SessionStorage.
Le mécanisme des sessions a été entièrement réécrit à l'aide du classe PHP. Mise
à part la syntaxe orientée objet, cette nouvelle manière de percevoir la session
sous forme d'un objet métier apporte de nombreux avantages.
Le premier avantage est sans aucun doute la réutilisabilité du code. En effet,
il suffit de copier le fichier SessionStorage.php dans les différents projets
afin de pouvoir bénéficier de cette nouvelle API plus verbeuse et plus facile
d'utilisation.
D'autre part, grâce à la programmation orientée objet, le code est désormais
complètement testable grâce à tests unitaires. Il aurait ainsi été possible
d'écrire une série de tests unitaires dans PHPUnit pour s'assurer que les objets
SessionStorage se comportent bien comme on s'y attend.
Enfin, l'implémentation technique (le tableau $_SESSION et toutes les
fonctions session_*()) a été entièrement masquée par une API de plus haut
niveau grâce à la classe et ses méthodes. Par conséquent, tous les comportements
implémentés ici peuvent être redéfinis ou surchargés et c'est d'ailleurs tout le
sujet de la partie suivante. En effet, il s'agira de translater le stockage des
sessions vers une base de données SQLite sans nécessiter une importante mise à
jour du code de l'application.
Etendre le mécanisme de stockage des sessions
Aujourd'hui, les développeurs web doivent faire face à de plus en plus de contraintes lorsqu'ils travaillent pour un client. Ils doivent en effet prendre en compte l'existant de ce dernier. En effet, avec la professionnalisation et l'industrialisation des développements web, il n'est plus rare d'avoir à intégrer une application web dans un environnement technique existant et parfois très contraignant. Il convient donc d'adopter des stratégies au niveau du code source pour s'assurer que ce dernier s'adaptera lui même à l'environnement technique. Par exemple, écrire une application capable de se connecter à une base de données MySQL ou PostGreSQL sans avoir à toucher tout le reste du code.
Contraintes et limites des sessions natives
Le combat est aussi le même pour les sessions. Dans la majorité des cas, le mécanisme natif de gestion des sessions de PHP suffit à lui même car l'application web fonctionne sur un seul serveur web. Cependant, la donne change si l'on est amené à installer une seconde machine LAMP capable d'absorber une partie de la charge.
Imaginons simplement un répartiteur de charge qui redirige arbitrairement la
première requête de l'utilisateur sur le premier serveur car à ce moment là, il
est moins sollicité. La session de l'utilisateur est alors ouverte et le fichier
de session sauvegardé sur le serveur 1. La seconde requête est déclenchée mais
elle est cette fois-ci redirigée sur le serveur 2. Comment ce dernier peut-il
récupérer les données de session alors qu'elles se trouvent sur le premier
serveur ? C'est embêtant... Une solution simple consiste à monter un système de
fichier de partage et de configurer la constante de PHP session.save_path des
deux serveurs afin qu'elle pointe dans ce dernier.
A partir des contraintes soulevées, il semble tout à fait judicieux de réaliser un système capable d'adapter le stockage des sessions sans nécessiter de grosse intervention sur le code source de l'application. Pour y parvenir, la meilleure solution consiste à étendre et redéfinir le comportement original des sessions afin de stocker les sessions dans une base de données MySQL.
Préparer la base de données
Avant de s'attaquer au code, il convient tout d'abord de créer une nouvelle base
de données php_solutions. Cette tâche peut être accomplie dans un gestionnaire
de base de données MySQL graphique comme PHPMyAdmin ou bien MySQL Query Browser.
Vous pouvez également utiliser la ligne de commande MySQL comme le montre le
listing 7.
# Listing 7 $ mysql -u root -p $ Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 9 Server version: 5.1.49 Source distribution Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved. This software comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to modify and redistribute it under the GPL v2 license Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>create databse php_solutions;
Maintenant que la base de données est créée, l'étape suivante consiste à créer
la table php_session destinée à accueillir les données de session. Cette table
est constituée de quatre champs: id, sess_id, sess_data et sess_time. Le
listing 8 donne en langage SQL la définition complète de la table. C'est ce que
code que vous devez exécuter dans votre gestionnaire de base de données afin de
créer physiquement la table dans la base de données.
Le champ id constitue la clé primaire de la table. Sa valeur est auto
incrémentée et gérée par le moteur MySQL. La colonne sess_id est une chaîne de
longueur variable (varchar) stockant l'identifiant de session. La valeur est
obligatoire et être unique dans toute la table. Le champ sess_data est une
chaîne multiligne accueillant la chaîne sérialisée des données de session.
Enfin, le champ sess_time est un entier non signé obligatoire stockant la
valeur de la dernière date de mise à jour des données de session sous la forme
d'un timestamp Unix. La base de données est prête à accueillir les sessions
grâce à la nouvelle classe MySQLSessionStorage dévoilée dans la partie
suivante.
DROP TABLE IF EXISTS `php_session`; CREATE TABLE `php_session` ( `id` integer(11) NOT NULL AUTO_INCREMENT, `sess_id` varchar(50) NOT NULL UNIQUE, `sess_data` text NOT NULL, `sess_time` integer(14) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
La classe MySQLSessionStorage
L'objectif à présent consiste à développer un nouvel adapteur capable translater
le mécanisme natif des sessions vers une base de données MySQL. Pour y parvenir,
il convient de réaliser une nouvelle classe MySQLSessionStorage qui étend la
classe SessionStorage et redéfinit les comportements natifs. Le listing 9
présente l'intégralité de cette classe.
<?php // listing 9 require __DIR__.'/SessionStorage.php'; class MySQLSessionStorage extends SessionStorage { protected $dbh; public function __construct($name, PDO $dbh, array $options = array()) { $this->dbh = $dbh; // Table that stores session data $options = array_merge(array( 'db_table' => 'php_session', 'db_id_col' => 'sess_id', 'db_data_col' => 'sess_data', 'db_time_col' => 'sess_time' ), $options); $options['session_auto_start'] = false; parent::__construct($name, $options); // Ask PHP to use this class to handle sessions instead of // its native behavior session_set_save_handler( array($this, 'sessionOpen'), array($this, 'sessionClose'), array($this, 'sessionRead'), array($this, 'sessionWrite'), array($this, 'sessionDestroy'), array($this, 'sessionGC') ); session_start(); } public function sessionClose() { return true; } public function sessionOpen($path = null, $name = null) { return true; } public function sessionDestroy($id) { // get table/column $db_table = $this->options['db_table']; $db_id_col = $this->options['db_id_col']; // delete the record associated with this id $sql = 'DELETE FROM '.$db_table.' WHERE '.$db_id_col.'= ?'; try { $stmt = $this->dbh->prepare($sql); $stmt->bindParam(1, $id, PDO::PARAM_STR); $stmt->execute(); } catch (PDOException $e) { throw new Exception(sprintf('Unable to destroy the session. Message: %s', $e->getMessage())); } return true; } public function sessionGC($lifetime) { // get table/column $db_table = $this->options['db_table']; $db_time_col = $this->options['db_time_col']; // delete the record associated with this id $sql = 'DELETE FROM '.$db_table.' WHERE '.$db_time_col.' < '.(time() - $lifetime); try { $this->dbh->query($sql); } catch (PDOException $e) { throw new Exception(sprintf('Unable to clean expired sessions. Message: %s', $e->getMessage())); } return true; } public function sessionRead($id) { // get table/columns $db_table = $this->options['db_table']; $db_data_col = $this->options['db_data_col']; $db_id_col = $this->options['db_id_col']; $db_time_col = $this->options['db_time_col']; try { $sql = 'SELECT '.$db_data_col.' FROM '.$db_table.' WHERE '.$db_id_col.'=?'; $stmt = $this->dbh->prepare($sql); $stmt->bindParam(1, $id, PDO::PARAM_STR, 255); $stmt->execute(); $sessionRows = $stmt->fetchAll(PDO::FETCH_NUM); if (1 === count($sessionRows)) { return $sessionRows[0][0]; } else { // session does not exist, create it $sql = 'INSERT INTO '.$db_table.'('.$db_id_col.', '.$db_data_col.', '.$db_time_col.') VALUES (?, ?, ?)'; $stmt = $this->dbh->prepare($sql); $stmt->bindParam(1, $id, PDO::PARAM_STR); $stmt->bindValue(2, '', PDO::PARAM_STR); $stmt->bindValue(3, time(), PDO::PARAM_INT); $stmt->execute(); return ''; } } catch (PDOException $e) { throw new Exception(sprintf('Unable to read session data. Message: %s', $e->getMessage())); } } public function sessionWrite($id, $data) { // get table/column $db_table = $this->options['db_table']; $db_data_col = $this->options['db_data_col']; $db_id_col = $this->options['db_id_col']; $db_time_col = $this->options['db_time_col']; $sql = 'UPDATE '.$db_table.' SET '.$db_data_col.' = ?, '.$db_time_col.' = '.time().' WHERE '.$db_id_col.'= ?'; try { $stmt = $this->dbh->prepare($sql); $stmt->bindParam(1, $data, PDO::PARAM_STR); $stmt->bindParam(2, $id, PDO::PARAM_STR); $stmt->execute(); } catch (PDOException $e) { throw new Exception(sprintf('Unable to write session data. Message: %s', $e->getMessage())); } return true; } }
La nouvelle classe MySQLSessionStorage accueille à présent un constructeur et
six nouvelles méthodes: sessionClose(), sessionOpen(), sessionGC(),
sessionRead(), sessionWrite() et sessionDestroy(). Le constructeur accepte
une instance de PDO en argument et configure diverses options de la session. Les
six méthodes restantes sont appelées automatiquement par le gestionnaire des
sessions PHP avant et après l'exécution du script. C'est la fonction
session_set_save_handler() de PHP qui est responsable de redéfinir toutes les
comportements internes du gestionnaire de session.
Le gestionnaire d'événements des sessions
L'API des sessions de PHP offre une manière simple et intelligente de redéfinir
entièrement les mécanismes internes de gestion des sessions. La fonction
session_set_save_handler() (http://fr.php.net/session-set-save-handler)
remplit parfaitement ce besoin. La documentation officielle indique que cette
fonction accepte six paramètres obligatoires. Chacun de ces paramètres est une
fonction utilisateur de rappel ou bien un tableau contenant une instance et le
nom de la méthode publique correspondante à appeler.
Le premier paramètre définit la fonction d'ouverture de la session. Cette
fonction reçoit toujours deux paramètres : le chemin vers le dossier de stockage
des sessions ainsi que le nom de la session. Ici, c'est la méthode
sessionOpen() qui est configurée. Elle renvoie toujours true.
Le second paramètre définit la fonction de fermeture de la session. Cette
fonction agit comme un destructeur de classe, et est exécutée lorsque le script
se termine. Dans la classe MySQLSessionStorage, c'est la méthode
sessionClose() qui sera appelée. Elle renvoie toujours true.
Le troisième paramètre de la fonction session_set_save_handler() configure la
fonction de lecture de la session. La fonction configurée accepte l'identifiant
de session comme paramètre et doit obligatoirement une chaîne contenant les
données de session. Il s'agit d'une chaîne sérialisée servant à initialiser le
tableau de session $_SESSION au démarrage de la session. Si aucune donnée de
session n'existe, alors la chaîne retournée doit être vide.
La méthode sessionRead() tente tout d'abord de récupérer la session en cours
depuis la table php_session grâce à une requête SQL SELECT intégrant
l'identifiant de session passé en paramètre. S'il existe un enregistrement,
alors la méthode retourne le contenu de la colonne sess_data qui correspond à
la chaîne sérialisée. Si aucun tuple n'est rapatrié, alors la méthode initialise
une nouvelle session en base de données en créant un nouvel enregistrement dans
la table. Remarquez ici l'utilisation de PDO et des requêtes préparées afin
d'éviter tout risque d'injection SQL.
Le quatrième paramètre permet de définir la fonction de rappel appelée au moment
de la sauvegarde des données de session. Cette fonction est appelée par PHP
après l'exécution du script et reçoit deux paramètres : l'identifiant de session
ainsi que la chaîne sérialisée du tableau de session. Ici, c'est la méthode
sessionWrite() qui se charge de remplir ce besoin. Cette dernière met à jour
l'enregistrement correspondant à la session dans la table php_session à l'aide
d'une requête SQL UPDATE.
Le cinquième paramètre de la fonction session_set_save_handler() définit
ensuite la fonction de destruction de la session. Cette fonction accepte
l'identifiant de session en paramètre. Le comportement de destruction est
automatiquement exécuté lorsque la fonction session_destroy() est invoquée.
Dans notre exemple, la méthode sessionDestroy() sera appelée lorsque la
méthode destroy() de l'objet de session est appelée. La méthode
sessionDestroy() détruit la session en supprimant l'enregistrement
correspondant dans la table php_session.
Enfin, le dernier paramètre définit la fonction de nettoyage des sessions
expirées. Cette dernière accepte un unique paramètre la durée de vie maximale
d'une session, exprimée en secondes. La méthode sessionGC() de la classe
MySQLSessionStorage se charge de supprimer de la table php_session tous les
enregistrements dont la valeur de la colonne sess_time correspond à une date
expirée.
La prochaine et dernière partie de cet article s'article autour des tests de cette nouvelle classe avec notre précédente application.
Test de la classe MySQLSessionStorage
La dernière étape de ce cours avancé sur les sessions consiste à tester la nouvelle classe MySQLSessionStorage avec notre précédente application. Pour y parvenir, il convient de modifier les premières lignes du fichier index.php comme le montre le listing 10.
<?php require __DIR__.'/../src/MySQLSessionStorage.php'; function isValidColor($color) { return in_array($color, array( 'rouge', 'vert', 'bleu', 'violet', 'orange', 'noir' )); } $dbh = null; try { $dbh = new PDO( 'mysql:dbname=php_solutions;host=127.0.0.1', 'root', '', array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'") ); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo 'Unable to connect to MySQL:<br/>'; echo $e->getMessage(); die; } $session = new MySQLSessionStorage('phpsolmag', $dbh); $color = filter_input(INPUT_POST, 'color'); if (isValidColor($color)) { $session->write('favorite_color', $color); header('Location: http://www.phpsolutions.local/index.php'); exit; } ?> <!-- suite du code ... -->
Le bloc try { } catch { } ouvre une connexion sur la base de données MySQL
php_solutions en instanciant un objet PDO. Le nom d'utilisateur MySQL est ici
root et le mot de passe est vide. En cas d'erreur, une exception de type
PDOException est interceptée et interrompt le cours normal du script. Si la
connexion sur la base de données est validée, alors la classe
MySQLSessionStorage est instanciée. Le reste du code de l'application reste
exactement le même que précédemment.
Les listings 11 et 12 prouvent respectivement le fonctionnement de
l'application. A l'ouverture de la page www.phpsolutions.local, une nouvelle
session est enregistrée en base de données avec l'identifiant
ljn1sbiuhr481829qk4jfcfoh0. Comme aucune couleur n'a été sélectionnée, il
n'existe pas encore de donnée de session dans la colonne sess_data. L'envoi du
formulaire avec vert comme valeur de couleur favorite entraîne la mise à jour
de la session dans la base de données. La colonne sess_data se voit attribuée
la valeur présentée dans le listing 12.
# Listing 11 mysql> select id, sess_id, sess_data, sess_time from php_session; +----+----------------------------+-----------+------------+ | id | sess_id | sess_data | sess_time | +----+----------------------------+-----------+------------+ | 1 | ljn1sbiuhr481829qk4jfcfoh0 | | 1282167188 | +----+----------------------------+-----------+------------+ 1 row in set (0.01 sec)
--
# Listing 12 mysql> select id, sess_id, sess_data from php_session; +----+----------------------------+----------------------------+ | id | sess_id | sess_data | +----+----------------------------+----------------------------+ | 1 | ljn1sbiuhr481829qk4jfcfoh0 | favorite_color|s:4:"vert"; | +----+----------------------------+----------------------------+ 1 row in set (0.00 sec)
Conclusion
Cet article a été l'occasion de découvrir des usages avancés du mécanisme des sessions de PHP en s'appuyant sur une approche orientée objet. Cette dernière a apporté un certain nom nombre de bienfaits au code source de l'application. En effet, le code de l'application est à présent plus facile à utiliser, à maintenir et à faire évoluer. De plus, le code écrit ici est à présent entièrement testable à l'aide d'un framework de tests unitaires comme PHPUnit mais il est aussi et surtout réutilisable. Enfin, le code s'adapte tout aussi aisément à un gestionnaire de session différent. Par conséquent, il sera désormais possible de basculer au choix sur des sessions natives ou bien vers une base de données MySQL.
En se basant sur les exemples présentés et grâce aux concepts de la programmation orientée objet (héritage, implémentation d'interfaces, classes abstraites...), il convient de créer de nouvelles classes capables d'adapter de nouveaux gestionnaires de session. En effet, la création des adapteurs pour d'autres bases de données relationnelles (SQLite, PostGreSQL, Oracle...) devient trivial. Bien entendu, il ne s'agit pas que de se limiter à ces systèmes. Un stockage en mémoire vive avec Memcache ou bien dans des bases NoSQL (CouchDB, MongoDB, Cassandra...) sont des alternatives complètement envisageables.



Posté par imars - Il y'a environ 6 months
Merci encore pour le tuto !!!
Posté par looky - Il y'a environ 4 months
mais j'ei un petit soucis et je vois pas l'erreur. J'ai cette erreur à l'écran:
Fatal error: Uncaught exception 'Exception' with message 'Unable to read session data. Message: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sess_time' in 'field list'' in C:\xampp\htdocs\code\src\MySQLSessionStorage.class.php:132 Stack trace: #0 [internal function]: MySQLSessionStorage->sessionRead('lpq5d9r1rd22eg9...') #1 C:\xampp\htdocs\code\src\MySQLSessionStorage.class.php(38): session_start() #2 C:\xampp\htdocs\code\index.php(30): MySQLSessionStorage->__construct('phpsolmag', Object(PDO)) #3 {main} thrown in C:\xampp\htdocs\code\src\MySQLSessionStorage.class.php on line 132
Fatal error: Exception thrown without a stack frame in Unknown on line 0