TD2 – La persistance des données en PHP Base de données, PDO

Dans le TD1 vous avez appris à créer des classes et à instancier des objets de ces classes. Mais, comme vous l’avez constaté, la durée de vie des objets ainsi créés ne dépassait pas la durée de l’exécution du programme.

Dans ce TD, nous allons apprendre à rendre les objets persistants, en les sauvegardant dans une base de données. Ainsi, il sera possible de retrouver les objets d’une visite à l’autre du site web.

Connexion à la base de données

Les bases de PhpMyAdmin

  1. Connectez vous à votre base de données MySQL, à l’aide de l’interface PhpMyAdmin http://webinfo.iutmontp.univ-montp2.fr/my Le login est votre login IUT et votre mot de passe initial est votre numéro INE (avec les lettres en majuscule).
    Si cela ne marche pas, c’est que vous n’êtes probablement pas inscrit administrativement. Dans ce cas, demandez à votre chargé de TD ou allez voir le service informatique (même bureau que le secrétariat au bâtiment 4).

  2. Changez votre mot de passe (Page d’accueil > Paramètres généraux > Modifier le mot de passe) et reconnectez-vous. Si vous n’arrivez pas à vous connecter après avoir changé le mot de passe, essayer avec un autre navigateur ou bien videz le cache du navigateur (Ctrl+F5).

    Attention : N’utilisez pas un de vos mots de passe usuels, car nous allons bientôt écrire ce mot de passe dans un fichier qui sera sans doute vu par le professeur ou votre voisin.
    Donc vous avez deux possibilités :

    • (recommandé) Créez un mot de passe aléatoire à l’aide de https://www.random.org/passwords/ par exemple. Écrivez dès maintenant ce mot de passe dans un fichier.
    • Ou choisissez quelque chose de simple et de pas secret.
  3. Créez une table utilisateur (sans majuscule) possédant 3 champs :

    • loginBaseDeDonnees de type VARCHAR et de longueur maximale 64, défini comme la clé primaire (Index : Primary)
    • nomBaseDeDonnees de type VARCHAR est de longueur maximale 64.
    • prenomBaseDeDonnees de type VARCHAR est de longueur maximale 64.

    Important : Pour faciliter la suite du TD, mettez à la création de toutes vos tables InnoDB comme moteur de stockage, et utf8_general_ci comme interclassement (c’est l’encodage des données, et donc des accents, caractères spéciaux…).

    Attention : Les noms des champs sont comme des noms de variables, ils ne doivent pas contenir d’accents. Par ailleurs, et contrairement à Oracle, MySQL est sensible à la casse (minuscules/majuscules).

  4. Insérez des données en utilisant l’onglet Insérer de PhpMyAdmin.

  5. Dans la suite du TD, pensez à systématiquement tester vos requêtes SQL dans PhpMyAdmin avant de les inclure dans vos pages PHP.

Fichier de configuration en PHP

Pour avoir un code portable, il est préférable de séparer les informations du serveur du reste du code PHP.

  1. Commencez par créer un dossier tds-php/TD2 dans l’explorateur de fichier, puis ouvrez ce dossier dans PHPStorm.

  2. Créez un fichier ConfigurationBaseDeDonnees.php. Ce fichier contiendra une classe ConfigurationBaseDeDonnees possédant un attribut statique $configurationBaseDeDonnees comme suit (changez bien sûr les a_remplir).

    Notes :

    • Où doit-on enregistrer une page Web ? (Souvenez-vous du TD précédent)
    • Qu’est-ce qu’un attribut ou une méthode statique ? (Cours de Programmation Orientée Objet de l’an dernier ; voir aussi les compléments)
    <?php
    class ConfigurationBaseDeDonnees {
       
      static private array $configurationBaseDeDonnees = array(
        // Le nom d'hote est webinfo a l'IUT
        // ou localhost sur votre machine
        // 
        // ou webinfo.iutmontp.univ-montp2.fr
        // pour accéder à webinfo depuis l'extérieur
        'nomHote' => 'a_remplir',
        // A l'IUT, vous avez une base de données nommee comme votre login
        // Sur votre machine, vous devrez creer une base de données
        'nomBaseDeDonnees' => 'a_remplir',
        // À l'IUT, le port de MySQL est particulier : 3316
        // Ailleurs, on utilise le port par défaut : 3306
        'port' => 'a_remplir',
        // A l'IUT, c'est votre login
        // Sur votre machine, vous avez surement un compte 'root'
        'login' => 'a_remplir',
        // A l'IUT, c'est le même mdp que PhpMyAdmin
        // Sur votre machine personelle, vous avez creez ce mdp a l'installation
        'motDePasse' => 'a_remplir'
      );
       
      static public function getLogin() : string {
        // L'attribut statique $configurationBaseDeDonnees 
        // s'obtient avec la syntaxe ConfigurationBaseDeDonnees::$configurationBaseDeDonnees 
        // au lieu de $this->configurationBaseDeDonnees pour un attribut non statique
        return ConfigurationBaseDeDonnees::$configurationBaseDeDonnees['login'];
      }
       
    }
    ?>
    
  3. Pour tester notre classe ConfigurationBaseDeDonnees, créons un fichier testConfigurationBaseDeDonnees.php que l’on ouvrira dans le navigateur.

    Souvenez-vous le TD dernier : Quelle est la bonne et la mauvaise URL pour ouvrir une page PHP ?

    <?php
      // On inclut les fichiers de classe PHP pour pouvoir se servir de la classe ConfigurationBaseDeDonnees. 
      // require_once évite que ConfigurationBaseDeDonnees.php soit inclus plusieurs fois, 
      // et donc que la classe ConfigurationBaseDeDonnees soit déclaré plus d'une fois. 
      require_once 'ConfigurationBaseDeDonnees.php';
    
      // On affiche le login de la base de donnees
      echo ConfigurationBaseDeDonnees::getLogin();
    ?>
    
  4. Complétez ConfigurationBaseDeDonnees.php avec des méthodes statiques getNomHote(), getPort(), getNomBaseDeDonnees() et getPassword(). Testez ces méthodes dans testConfigurationBaseDeDonnees.php.

    Remarque : Notez qu’en PHP, on appelle une méthode statique à partir du nom de la classe comme en Java, mais en utilisant :: au lieu du . en Java. Souvenez-vous que les méthodes dynamiques (c’est-à-dire pas static) s’appellent avec -> en PHP.

  5. Enregistrez votre travail à l’aide de git add et git commit. Nous comptons sur vous pour penser à faire cet enregistrement régulièrement.

Initialiser un objet PDO

Pour se connecter à une base de données en PHP on utilise une classe fournie avec PHP qui s’appelle PDO (Php Data Object). Cette classe va nous fournir de nombreuses méthodes très utiles pour manipuler n’importe quelle base de donnée.

  1. Commençons par établir une connexion à la base de données. Créez un fichier ConnexionBaseDeDonnees.php déclarant une classe ConnexionBaseDeDonnees, qui possédera
    • un attribut private PDO $pdo,
    • un constructeur sans argument qui ne fait rien pour l’instant (à générer avec PhpStorm),
    • un accesseur (getter) getPdo() à l’attribut $pdo (à générer avec PhpStorm).
  2. Dans le constructeur, nous allons initialiser l’attribut $pdo en lui assignant un objet PDO. Procédons par étapes :

    1. Pour créer la connexion à notre base de donnée, il faut utiliser le constructeur de PDO de la façon suivante

      new PDO("mysql:host=$nomHote;port=$port;dbname=$nomBaseDeDonnees",$login,$motDePasse);
      

      Stockez ce nouvel objet PDO dans l’attribut $pdo.

    2. Le code précédent a besoin que les variables $nomHote, $port, $nomBaseDeDonnees, $login et $motDePasse contiennent les chaînes de caractères correspondant à l’hôte, au nom, au login et au mot de passe de notre base de données. Créez donc ces variables avant le new PDO en récupérant les informations à l’aide des fonctions de la classe ConfigurationBaseDeDonnees.

    3. Comme notre classe ConnexionBaseDeDonnees dépend de ConfigurationBaseDeDonnees.php, ajoutez un require_once 'ConfigurationBaseDeDonnees.php' au début du fichier.

    4. Testons dès à présent notre nouvelle classe. Créez le fichier testConnexionBaseDeDonnees.php suivant. Vérifiez que l’exécution de testConnexionBaseDeDonnees.php ne donne pas de messages d’erreur.

      <?php
      require_once "ConnexionBaseDeDonnees.php";
      
      // On affiche un attribut de PDO pour vérifier  que la connexion est bien établie.
      // Cela renvoie par ex. "webinfo.iutmontp.univ-montp2.fr via TCP/IP"
      // mais surtout pas de message d'erreur
      // SQLSTATE[HY000] [1045] Access denied for user ... (mauvais mot de passe)
      // ou
      // SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed (mauvais nom d'hôte)
      $model = new ConnexionBaseDeDonnees();
      echo $model->getPdo()->getAttribute(PDO::ATTR_CONNECTION_STATUS);
      ?>
      

Patron de conception Singleton

Comme cela n’a pas de sens d’avoir plusieurs connexions à la base de données, nous allons utiliser le patron de conception Singleton. Il sert à assurer qu’il n’y ait qu’une et une seule instance possible de la classe ConnexionBaseDeDonnees dans l’application (et donc une seule connexion).

Voici le squelette d’un singleton :

class ConnexionBaseDeDonnees {
    private static $instance = null;

    private PDO $pdo;

    public static function getPdo(): PDO {
        return ConnexionBaseDeDonnees::getInstance()->pdo;
    }

    private function __construct () {
        // Code du constructeur
    }

    // getInstance s'assure que le constructeur ne sera 
    // appelé qu'une seule fois.
    // L'unique instance crée est stockée dans l'attribut $instance
    private static function getInstance() : ConnexionBaseDeDonnees {
        // L'attribut statique $instance s'obtient avec la syntaxe ConnexionBaseDeDonnees::$instance 
        if (is_null(ConnexionBaseDeDonnees::$instance))
            // Appel du constructeur
            ConnexionBaseDeDonnees::$instance = new ConnexionBaseDeDonnees();
        return ConnexionBaseDeDonnees::$instance;
    }
}

Remarque : Quand un attribut est statique, il s’accède par une syntaxe NomClasse::$nomVar comme indiqué précédemment.

  1. Mettez à jour votre classe ConnexionBaseDeDonnees pour qu’elle suive le design pattern Singleton.
  2. Mettez à jour testConnexionBaseDeDonnees.php et vérifiez que tout marche bien.
  3. Déclarez que l’attribut $instance est de type ConnexionBaseDeDonnees.
    L’IDE indique un problème : L’attribut $instance est initialisé à null, qui n’est pas de type ConnexionBaseDeDonnees en PHP (contrairement à Java), mais de type null.
    Corrigez ce problème en indiquant le type ?ConnexionBaseDeDonnees pour l’attribut $instance. En effet, ?ConnexionBaseDeDonnees est un raccourci pour le type ConnexionBaseDeDonnees|null, qui veut dire ConnexionBaseDeDonnees ou null.

Gestion des erreurs

Nous allons maintenant améliorer la gestion des erreurs de PDO.

Pour avoir plus de messages d’erreur de PDO et qu’il gère mieux l’UTF-8, mettez à jour la connexion dans ConnexionBaseDeDonnees en remplaçant $this->pdo = new PDO(...); par

// Connexion à la base de données            
// Le dernier argument sert à ce que toutes les chaines de caractères 
// en entrée et sortie de MySql soient dans le codage UTF-8
$this->pdo = new PDO("mysql:host=$nomHote;port=$port;dbname=$nomBaseDeDonnees", $login, $motDePasse,
                     array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));

// On active le mode d'affichage des erreurs, et le lancement d'exception en cas d'erreur
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Opérations sur la base de données

Voyons maintenant comment les objets PDO servent à effectuer des requêtes SQL. Nous allons nous servir de deux méthodes fournies par PDO :

  1. La méthode query($SQL_request) de la classe PDO
    • prend en entrée une requête SQL (chaîne de caractères)
    • et renvoie la réponse de la requête dans une représentation interne pas immédiatement lisible (un objet PDOStatement).
  2. La méthode fetch() de la classe PDOStatement s’appelle sur les réponses de requêtes et renvoie la réponse de la requête dans un format lisible par PHP. Plus précisément, elle renvoie une entrée SQL formatée comme un tableau. Ce tableau est indexé par les noms des champs de la table de données, et aussi par les numéros des champs. Les valeurs du tableau sont celles de l’entrée SQL.

Faire une requête SQL sans paramètres

Commençons par la requête SQL la plus simple, celle qui lit tous les éléments d’une table (utilisateur dans notre exemple) :

SELECT * FROM utilisateur
  1. Créez un fichier lireUtilisateurs.php

  2. Incluez le fichier contenant la classe ConnexionBaseDeDonnees pour pouvoir se connecter à la base de données.

  3. Appelez la fonction query de l’objet PDO ConnexionBaseDeDonnees::getPdo() en lui donnant la requête SQL. Stockez sa réponse dans une variable $pdoStatement.

  4. Comme expliqué précédemment, pour lire les réponses à des requêtes SQL, vous pouvez utiliser

    $utilisateurFormatTableau = $pdoStatement->fetch()
    

    qui, dans notre exemple, renvoie un tableau avec 6 cases :

    • loginBaseDeDonnees, prenomBaseDeDonnees et nomBaseDeDonnees (les champs de la base de données).
    • 0, 1 et 2 qui correspondent aux champs de la base de données dans l’ordre. Ces cases sont donc un peu redondantes.

    Utilisez l’un des affichages de débogage (e.g. var_dump) pour afficher ce tableau.

  5. Créez un $utilisateur de classe Utilisateur à l’aide de $utilisateurFormatTableau en appelant le constructeur. Affichez l’utilisateur en utilisant la méthode adéquate de Utilisateur. Copiez le fichier tds-php/TD1/Utilisateur.php dans tds-php/TD2 pour pouvoir utiliser la classe Utilisateur dans le TD2.

  6. On souhaite désormais afficher tous les utilisateurs dans la base de données. On pourrait faire une boucle while sur fetch tant qu’on n’a pas parcouru toutes les entrées de la base de données.

    Heureusement, il existe une syntaxe simplifiée qui fait exactement cela :

    foreach($pdoStatement as $utilisateurFormatTableau){
       // ...
    }
    

    Note :

    • chaque tour de boucle agit comme si on avait fait un fetch
      $utilisateurFormatTableau = $pdoStatement->fetch()
      
    • on peut faire foreach car PDOStatement implémente l’interface Traversable. C’est similaire à Java qui permettait la boucle for(xxx : yyy) pour les objets implémentant l’interface Iterable.

    Utilisez la boucle foreach dans lireUtilisateurs.php pour afficher tous les utilisateurs.

  7. Avez-vous pensé à enregistrer régulièrement votre travail sous Git ?

Nous allons maintenant isoler le code qui retourne tous les utilisateurs et en faire une méthode de Utilisateur.

  1. Isolez le code qui construit l’objet Utilisateur à partir du tableau donné par fetch (e.g. $utilisateurFormatTableau) dans une méthode
    public static function construireDepuisTableauSQL(array $utilisateurFormatTableau) : Utilisateur {
    // ...
    }
    
  2. Créez une fonction statique recupererUtilisateurs() dans la classe Utilisateur qui ne prend pas d’arguments et renvoie le tableau d’objets de la classe Utilisateur correspondant à la base de données.

    Rappel : On peut rajouter facilement un élément “à la fin” d’un tableau avec

    $tableau[] = "Nouvelle valeur";
    
  3. Mettez à jour lireUtilisateurs.php pour appeler directement recupererUtilisateurs().

  4. Maintenant que vous avez bien compris où les noms de colonnes (loginBaseDeDonnees, prenomBaseDeDonnees, …) de la table utilisateur interviennent dans le tableau $utilisateurFormatTableau, nous allons leur redonner des noms plus classiques :
    1. Changer les noms des colonnes pour login, prenom et nom. Pour ceci, dans PhpMyAdmin, cliquez sur l’onglet “Structure” de la table utilisateur, puis “Modifier” sur chaque colonne.
    2. Modifiez le code PHP à l’endroit où interviennent ces noms de colonnes.

Format de retour de fetch()

Rappelons que la méthode fetch($fetchStyle) s’appelle sur les réponses de requêtes et renvoie la réponse de la requête dans un format lisible par PHP. Le choix du format se fait avec la variable $fetchStyle. Les formats les plus communs sont :

Dans les TDs, nous vous recommandons d’utiliser au choix :