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. À la fin, vous saurez accéder aux bases de données en PHP grâce à PDO (PHP Data Objects), ouvrir une connexion à une base de données, lire des lignes et construire des objets PHP à partir de ces données.
Connexion à la base de données
Les bases de PhpMyAdmin
-
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 (bâtiment K, premier étage). -
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, essayez 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.
-
Créez une table
utilisateur(sans majuscule) possédant 3 champs :loginBaseDeDonneesde typeVARCHARet de taille 64 (il s’agit de la longueur maximale), défini comme la clé primaire (champ Index puis sélectionnerPrimaryet valider la boîte de dialogue qui s’ouvre).nomBaseDeDonneesde typeVARCHARet de taille 64.prenomBaseDeDonneesde typeVARCHARet de taille 64.
Important : Pour faciliter la suite du TD, mettez à la création de toutes vos tables
InnoDBcomme moteur de stockage, etutf8mb4_unicode_cicomme interclassement (ce sont les règles utilisées pour comparer et trier les chaînes de caractères, notamment en tenant compte des accents et de la casse).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).
-
Insérez des données en utilisant l’onglet
Insérerde PhpMyAdmin. -
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. Nous en profitons aussi pour éviter une mauvaise pratique courante : écrire un mot de passe en clair dans un fichier versionné par Git. Les informations de connexion seront donc placées dans un fichier à part, explicitement exclu du suivi de version.
-
Commencez par créer un dossier
tds-php/TD2dans l’explorateur de fichier, puis ouvrez ce dossier dans PHPStorm. -
Créez un fichier
ConfigurationBaseDeDonnees.ini. Un fichier.iniest un simple fichier texte contenant des pairescle = valeur, une par ligne, que PHP sait lire nativement.Voici à quoi correspondent les clés à renseigner :
-
nomHote: adresse du serveur qui héberge la base de données. Quand on crée une base de données en local, il s’agit généralement delocalhost, mais dans notre cas, on souhaite utiliser le serveur de base de données mis à disposition à l’IUT (auquel vous venez de vous connecter), doncwebinfo.iutmontp.univ-montp2.fr. -
nomBaseDeDonnees: le nom de la base de données à laquelle on souhaite accéder. Quand on crée une base de données en local, c’est le développeur qui choisit son nom. Comme nous utilisons le serveur de bases de données de l’IUT, une base de données vous est attribuée (vous ne pouvez pas en créer vous-même). Cette base de données est nommée comme votre login (celui que vous avez utilisé pour vous connecter à phpMyAdmin). -
port: numéro de port correspondant au service de base de données sur le serveur. Par défaut pour une base de donnéesMySQL, ce port est 3306, mais sur le serveur de l’IUT, le port à utiliser est3316. -
login: afin d’accéder au système de gestion de bases de données (SGBD), il faut généralement un compte. Quand on crée une base de données en local, on crée aussi des utilisateurs, ou on peut aussi utiliser un compte administrateur par défaut nommé root. Sur le serveur de l’IUT, il faut utiliser votre login (celui que vous avez utilisé pour vous connecter à phpMyAdmin). -
motDePasse: le mot de passe du compte de l’utilisateur. À l’IUT, il s’agit du mot de passe que vous avez utilisé pour vous connecter à phpMyAdmin plus tôt.
Voici le squelette du fichier
ConfigurationBaseDeDonnees.ini, à compléter avec vos propres informations :nomHote = a_remplir nomBaseDeDonnees = a_remplir port = a_remplir login = a_remplir motDePasse = a_remplirRemarque : Certains caractères ont un sens spécial dans un fichier
.iniet peuvent, s’ils ne sont pas protégés, tronquer votre mot de passe, le transformer silencieusement, voire faire échouer la lecture de tout le fichier :;(démarre un commentaire),=(sépare la clé de la valeur), ainsi que$,?,{,},|,&,~,!,(,),^(interprétés comme des opérateurs ou utilisés pour l’interpolation de variables). Un mot de passe qui serait exactement l’un des motsnull,yes,no,true,false,on,off,none(mots réservés) serait lui aussi silencieusement remplacé par une autre valeur.Pour éviter tous ces problèmes, entourez systématiquement votre mot de passe (et plus généralement toute valeur) de guillemets doubles dans le fichier
.ini:motDePasse = "mon mot de passe;secret"Si le mot de passe contient lui-même un guillemet double ou une barre oblique inverse, faites-les précéder d’une barre oblique inverse :
\"pour"et\\pour\. -
-
Ce fichier contient désormais votre mot de passe en clair : il ne doit jamais être versionné avec Git. Créez, à la racine de votre dépôt
tds-php, un fichier.gitignore(ou complétez-le, s’il existe déjà) en y ajoutant la ligne :ConfigurationBaseDeDonnees.iniCette ligne indique à Git d’ignorer tout fichier nommé
ConfigurationBaseDeDonnees.ini, quel que soit le dossier du dépôt où il se trouve. Ainsi, même avecgit add ., ce fichier ne sera jamais ajouté au suivi de version.Vérifiez avec
git statusqueConfigurationBaseDeDonnees.inin’apparaît pas parmi les fichiers proposés au commit. -
Enregistrez votre travail à l’aide de
git addetgit 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.
- Commençons par établir une connexion à la base de données. Créez un fichier
ConnexionBaseDeDonnees.phpdéclarant une classeConnexionBaseDeDonnees, 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).
- un attribut
-
Dans le constructeur, nous allons initialiser l’attribut
$pdoen lui assignant un objetPDO. Procédons par étapes :-
Récupérez le contenu du fichier
ConfigurationBaseDeDonnees.inigrâce à la fonctionparse_ini_file, qui renvoie un tableau associatif indexé par les clés du fichier.ini:$configurationBaseDeDonnees = parse_ini_file('ConfigurationBaseDeDonnees.ini', false, INI_SCANNER_RAW);Le troisième argument
INI_SCANNER_RAWindique àparse_ini_filede lire les valeurs telles quelles, sans interpréter les caractères spéciaux vus précédemment (opérateurs, interpolation, mots réservés). C’est le mode recommandé pour lire un fichier de configuration contenant des informations sensibles comme un mot de passe.
Remarque : même avecINI_SCANNER_RAW, un point-virgule;non protégé démarre toujours un commentaire : continuez donc à entourer vos valeurs de guillemets doubles dès qu’elles contiennent des caractères spéciaux.Créez ensuite les variables
$nomHote,$port,$nomBaseDeDonnees,$loginet$motDePasseen lisant les entrées correspondantes du tableau$configurationBaseDeDonnees(par exemple$configurationBaseDeDonnees['nomHote']). -
Pour créer la connexion à notre base de données, il faut utiliser le constructeur de
PDOde la façon suivantenew PDO("mysql:host=$nomHote;port=$port;dbname=$nomBaseDeDonnees", $login, $motDePasse);Stockez ce nouvel objet
PDOdans l’attribut$pdode l’objet. -
Testons dès à présent notre nouvelle classe. Créez le fichier
testConnexionBaseDeDonnees.phpsuivant. Vérifiez que l’exécution detestConnexionBaseDeDonnees.phpne 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.
- Mettez à jour votre classe
ConnexionBaseDeDonneespour qu’elle suive le design pattern Singleton. - Mettez à jour
testConnexionBaseDeDonnees.phpet vérifiez que tout marche bien. - Déclarez que l’attribut
$instanceest de typeConnexionBaseDeDonnees.
L’IDE indique un problème : L’attribut$instanceest initialisé ànull, qui n’est pas de typeConnexionBaseDeDonneesen PHP (contrairement à Java), mais de typenull.
Corrigez ce problème en indiquant le type?ConnexionBaseDeDonneespour l’attribut$instance. En effet,?ConnexionBaseDeDonneesest un raccourci pour le typeConnexionBaseDeDonnees|null, qui veut direConnexionBaseDeDonneesounull.
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
// charset=utf8mb4 sert à ce que toutes les chaines de caractères
// en entrée et sortie de MySQL utilisent dans l'encodage UTF-8
$this->pdo = new PDO("mysql:host=$nomHote;port=$port;dbname=$nomBaseDeDonnees;charset=utf8mb4", $login, $motDePasse);
// 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 :
- La méthode
query($SQL_request)de la classePDO- 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).
- La
méthode
fetch()de la classePDOStatements’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 (une ligne de la réponse) 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. Si la requête renvoie plusieurs lignes (plusieurs entrées), il faut exécuterfetchautant de fois que nécessaire pour traiter chaque entrée. On peut aussi passer par une boucleforeachcomme nous le verrons bientôt.
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
-
Créez un fichier
lireUtilisateurs.php. -
Incluez le fichier contenant la classe
ConnexionBaseDeDonneespour pouvoir se connecter à la base de données. -
Appelez la fonction
queryde l’objetPDOConnexionBaseDeDonnees::getPdo()en lui donnant la requête SQL. Stockez sa réponse dans une variable$pdoStatement. -
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 entrées :
loginBaseDeDonnees,prenomBaseDeDonneesetnomBaseDeDonnees(les champs de la base de données).0,1et2qui correspondent aux champs de la base de données dans l’ordre. Ces entrées sont donc un peu redondantes.
Utilisez l’un des affichages de débogage (par exemple
var_dump) pour afficher ce tableau. -
Créez un
$utilisateurde classeUtilisateurà l’aide de$utilisateurFormatTableauen appelant le constructeur. Affichez l’utilisateur en utilisant la méthode adéquate deUtilisateur. Copiez le fichiertds-php/TD1/Utilisateur.phpdanstds-php/TD2pour pouvoir utiliser la classeUtilisateurdans le TD2. -
On souhaite désormais afficher tous les utilisateurs dans la base de données. On pourrait faire une boucle
whilesurfetchtant 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’interfaceIterable.
Utilisez la boucle
foreachdanslireUtilisateurs.phppour afficher tous les utilisateurs. - chaque tour de boucle agit comme si on avait fait un fetch
-
Si ce n’est pas déjà fait, reprenez votre code pour inclure une structure HTML classique (
<html>,<head>,<body>…) et présenter plus proprement les utilisateurs (vous pouvez vous inspirer de ce que vous aviez fait lors du TD1). -
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.
- Isolez le code qui construit l’objet
Utilisateurà partir du tableau donné parfetch(e.g.$utilisateurFormatTableau) dans une méthodepublic static function construireDepuisTableauSQL(array $utilisateurFormatTableau) : Utilisateur { // ... } -
Créez une fonction statique
recupererUtilisateurs()dans la classeUtilisateurqui ne prend pas d’arguments et renvoie le tableau d’objets de la classeUtilisateurcorrespondant à la base de données. Le type de retour de cette fonction estarray.Rappel : On peut rajouter facilement un élément “à la fin” d’un tableau avec
$tableau[] = "Nouvelle valeur"; -
Mettez à jour
lireUtilisateurs.phppour appeler directementrecupererUtilisateurs(). - Maintenant que vous avez bien compris où les noms de colonnes (
loginBaseDeDonnees,prenomBaseDeDonnees, …) de la tableutilisateurinterviennent dans le tableau$utilisateurFormatTableau, nous allons leur redonner des noms plus classiques :- Changer les noms des colonnes pour
login,prenometnom. Pour ceci, dans PhpMyAdmin, cliquez sur l’onglet “Structure” de la tableutilisateur, puis “Modifier” sur chaque colonne. - Modifiez le code PHP à l’endroit où interviennent ces noms de colonnes.
- Changer les noms des colonnes pour
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 :
-
PDO::FETCH_ASSOC: Chaque entrée SQL est un tableau indexé par les noms des champs de la table de la base de données ; -
PDO::FETCH_NUM: Chaque entrée SQL est un tableau indexé par le numéro de la colonne commençant à 0 ; -
PDO::FETCH_BOTH(valeur par défaut si on ne donne pas d’argument$fetchStyle) : combinaison dePDO::FETCH_ASSOCetPDO::FETCH_NUM. Ce format retourne un tableau indexé par les noms de colonnes et aussi par les numéros de colonnes, commençant à l’index 0, comme retournés dans le jeu de résultats -
PDO::FETCH_OBJ: Chaque entrée SQL est un objet dont les noms d’attributs sont les noms des champs de la table de la base de données ; -
PDO::FETCH_CLASS: De même quePDO::FETCH_OBJ, chaque entrée SQL est un objet dont les noms d’attributs sont les noms des champs de la table de la base de données. Cependant, on peut dans ce cas spécifier le nom de la classe des objets. Pour ce faire, il faut avoir au préalable déclaré le nom de la classe avec la commande suivante :$pdoStatement->setFetchMode( PDO::FETCH_CLASS, 'class_name');Note : Ce format qui semble très pratique a malheureusement un comportement problématique :
- il crée d’abord une instance de la classe demandée (sans passer par le constructeur !) ;
- il écrit les attributs correspondants aux champs de la base de données (même s’ils sont privés ou n’existent pas !) ;
- puis il appelle le constructeur sans arguments.
Dans les TDs, nous vous recommandons d’utiliser au choix :
- le format par défaut
PDO::FETCH_BOTHen appelantfetch()sans arguments, - le format
PDO::FETCH_ASSOCpour ne pas avoir d’entrées redondantes (e.gloginBaseDeDonneeset0).
Dans ce cas, appelez$pdoStatement->setFetchMode(PDO::FETCH_ASSOC)avant d’appelerfetch().
Remarques finales
Identifiants exposés
Le mot de passe de connexion à la base de données est une information sensible. S’il est écrit en clair dans un fichier versionné par Git, et que ce dépôt est un jour partagé ou rendu public, votre mot de passe (et potentiellement l’accès à toute la base de données) se retrouve exposé à n’importe qui. Il ne faut donc jamais committer un vrai mot de passe dans un dépôt Git, même privé.
C’est pourquoi, dans ce TD, nous avons isolé les informations sensibles (hôte, login, mot de
passe…) dans un fichier de configuration dédié, ConfigurationBaseDeDonnees.ini, explicitement
ignoré par Git via .gitignore. Une autre approche courante, que vous pourrez rencontrer dans
d’autres projets, consiste à fournir ces informations via des variables d’environnement lues au
moment de l’exécution.
PhpMyAdmin
Il faut distinguer trois choses : la base de données, le serveur MySQL, et phpMyAdmin. La base de données est l’endroit où les informations sont réellement stockées : tables, colonnes et enregistrements. Le serveur MySQL est le logiciel qui gère cette base de données, reçoit les requêtes SQL et les exécute. phpMyAdmin est seulement une interface web d’administration qui permet d’interagir plus facilement avec MySQL, par exemple pour visualiser les tables, créer des données ou exécuter des requêtes, mais il ne remplace ni la base de données ni le serveur lui-même.