Usages avancés des sessions avec la POO

Le mardi 21 septembre 2010 dans PHP / AFUP, Bases de données

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 :

  1. Configurer le nom de la session,
  2. Configurer la session en fonction des valeurs transmises dans le tableau d'options passé en second paramètre,
  3. 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 classe SessionStorage,
  • 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 principal index.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.

Commentaires

Posté par imars - Il y'a environ 6 months

Merci très complet je vais faire des tests pour mettre en pratique ce que tu viens de rédiger.
Merci encore pour le tuto !!!

Posté par looky - Il y'a environ 4 months

Merci pour ce tuto,
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

Laisser un commentaire

Votre commentaire