Dans ce cours, on travaille avec PDO (PHP Data Objects). C'est un outil pour travailler avec les bases de données. (comme pour la pelle qui est un outil pour creuser, il y a plusieurs outils pour travailler avec des bases de données, mais nous n'utiliserons dans ce cours que PDO).
Pour ce mémento, on utilise une base de donnée appelée mcu
qui contient des données sur des films marvel. On retrouve le script SQL ici.
C'est l'acronyme des 4 fonctionnalités générales qu'on retrouve dans beaucoup d'applications en informatiques, puisque c'est les seuls possibilités d'intéragir avec des données.
- Create: créer
- Read: lire
- Update: mettre à jour/modifier
- Delete: supprimer
Exemple de première utilisation (code repris de php.net et valeurs adaptées):
//Identifiants pour la DBB
$user = "ICT-151";
$pass = "Pa\$\$w0rd";
try {
$dbh = new PDO('mysql:host=localhost;dbname=mcu', $user, $pass);
foreach ($dbh->query('SELECT * from actors') as $row) {
var_dump($row);
}
$dbh = null;
} catch (PDOException $e) {
print "Error!: " . $e->getMessage() . "<br/>";
die();
}
Avec CMDER ou tout autre shell:
C:\Users\john\Documents\Github\ICT-151-Sandbox (master -> origin) //on vient sur DOCUMENTROOT du serveur.
λ php -f index.php //fichier php à exécuter
Array
(
[id] => 1
[0] => 1
[actornumber] => 1
[1] => 1
[lastname] => Ruffalo
[2] => Ruffalo
[firstname] => Mark
[3] => Mark
[birthdate] => 1967-11-22
[4] => 1967-11-22
[nationality] => USA
[5] => USA
)
Array
(
[id] => 2
[0] => 2
[actornumber] => 2
[1] => 2
[lastname] => Holland
[2] => Holland
[firstname] => Tom
[3] => Tom
[birthdate] => 1996-06-01
[4] => 1996-06-01
[nationality] => UK
[5] => UK
)
Résultat: Il exécute le fichier php et affiche donc les deux enregistrements trouvés dans la BDD.
Mais c'est un gros problème d'avoir les identifiants en clair dans le code et de les publier sur Github ! Pas très pratique aussi, si on doit changer les identifiants dans plein de fichiers différent, quand ils changent.
C'est pour cette raison qu'on va faire un fichier séparé .const.php
qui contient les constantes pour la connexion à la BDD, mais on peut aussi y stocker d'autres données qui doivent rester en local ou qui serve au développement. Le fichier commence par un .
. C'est une convention pour les fichiers cachés.
<?php
/**
* Projet: ICT-151-Sandbox
* Filename: Identifiants de login
* Author: Samuel Roland
* Creation date: 07.02.2020
*/
$user = "ICT-151";
$pass = "Pa\$\$w0rd";
$dbhost = "localhost";
$dbname = "mcu";
?>
Ensuite:
- on ignore le fichier en l'ajoutant au
.gitignore
pour ne pas l'envoyer sur Git. - on le récupère le contenu du fichier par un
require .const.php;
en haut de la fonction. Attention à ne pas utiliserrequire_once
puisque plusieurs fonctions vont en avoir besoin.
Le problème qui arrive maintenant est que les développeurs qui travaillent avec nous ne savent pas qu'on a fait ce fichier séparé puisque ce fichier n'est pas envoyé sur Github. (sauf si il lise le require .const.php;
mais ils ne savent pas ce qu'il y a dedans précisément). Pour résoudre ce problème, on fait une copie du fichier nommée .const.php.example
avec les variables mais sans valeurs. Ils pourront ensuite dupliquer le fichier et le renommer, et remplir les valeurs pour arriver au même point que nous. Le *.example
à la fin du nom du fichier est souvent utilisé dans le développement et donc facilement compréhensibles par d'autres développeurs.
Exemple de contenu du fichier:
<?php
/**
* Projet: ICT-151-Sandbox
* Filename: Identifiants de login
* Author: Samuel Roland
* Creation date: 07.02.2020
*/
//TODO: renommer le fichier en .const.php et remplir les valeurs pour la database
$user = "";
$pass = "";
$dbhost = "";
$dbname = "";
?>
Pour faire une requête il y a 4 étapes:
On crée un objet PDO de cette manière:
$dbh = new PDO('mysql:host=' . $dbhost . ';dbname=' . $dbname, $user, $pass);
-
La requête SQL dans une string:
$query = "SELECT id, lastname, firstname FROM filmmakers";
-
Préparer la requête = envoyer au serveur web (vérification de type sécuritaire):
$statment = $dbh->prepare($query);
-
On peut éxecuter la requête:
$statment->execute();
-
Aller chercher tous les résultats:
$queryResult = $statment->fetchAll();
-
Ou un seul résultat:
$queryResult = $statment->fetch();
fetchAll()
retourne un tableau de tableaux associatifs. tandis que fetch()
retourne un tableau associatif qui ne contient donc qu'un seul enregistrement.
Visuellement ca donne ca si on teste fetch()
et fetchAll()
pour un seul enregistrement. Ca ne fait pas beaucoup de sens d'utiliser fetchAll() puisqu'il faudra utiliser 2 dimensions au lieu d'une seule.
Un fetch()
sur plusieurs enregistrements prendra uniquement le premier enregistrement...
ATTENTION particularité.
Pour ne pas avoir un tableau indexé et associatif (créé par fetch()
ou fetchAll()
) en même temps (toutes les données étant donc à double), il faut mettre un paramètre aux méthodes qui dit le type de tableau qu'il doit retourner. Ces paramètres sont des constantes internes de PDO. On les atteind de la manière suivante PDO::NomConstante
Une petite liste de possibilités très utiles:
PDO::FETCH_ASSOC
pour avoir un tableau associatif uniquementPDO::FETCH_NUM
pour avoir un tableau indexé uniquement (partant de index 0)
Changement:
$queryResult = $statment->fetchAll();
en
$queryResult = $statment->fetchAll(PDO::FETCH_ASSOC);
source: https://www.php.net/manual/en/pdostatement.fetch
Afin de se simplifier la vie mais aussi pour sécuriser l'application contre les injections SQL, on utilise des paramètres SQL. Ces paramètres seront remplacés par leurs valeurs durant l'éxecution de la méthode execute()
. Explication détaillée.
Au lieu de faire comme nous l'avons vu jusqu'à maintenant:
function getOneUser($email)
{
try {
$dbh = getPDO();
$query = "SELECT * FROM users where users.email =$email";
$statment = $dbh->prepare($query);
$statment->execute();
$queryResult = $statment->fetch(PDO::FETCH_ASSOC);
$dbh = null;
return $queryResult;
} catch (PDOException $e) {
echo "Error!: " . $e->getMessage() . "\n";
return null;
}
}
On va changer le $email
en un paramètre appelé email
. On va mettre un :
avant pour signifier que c'est un paramètre SQL. (un peu comme le $
signifie que c'est une variable). Puis dans le paramètre de $statment->execute()
on va mettre un tableau associatif dont une des clés s'appelle email
.
function getOneUser($email)
{
try {
$dbh = getPDO();
$query = "SELECT * FROM users where users.email =:email";
$statment = $dbh->prepare($query);
$statment->execute(["email" => $email]);
$queryResult = $statment->fetch(PDO::FETCH_ASSOC);
$dbh = null;
return $queryResult;
} catch (PDOException $e) {
echo "Error!: " . $e->getMessage() . "\n";
return null;
}
}
Pour que le remplacement des paramètres par leur données fonctionne, il faut que le paramètre ait le même nom qu'une des clés du tableau associatif fourni comme paramètre (php) de execute(), et aussi que la clé existe dans le tableau (forcément).
En cas d'erreur (clés inexistantes ou pas toutes les clés):
PHP Warning: PDOStatement::execute(): SQLSTATE[HY093]: Invalid parameter number: parameter was not defined in C:\...
et la requête échoue.
Attention, les paramètres SQL ne peuvent être utilisé que pour des valeurs dans la requête et non sur des noms de tables ou des noms de colonnes.
La requête suivante ne fonctionnera pas:
$query = "SELECT * FROM :tablename";
Source: Stackoverflow
Les tests unitaires permettent de tester le bon fonctionnement de chaque fonction séparément (unitaire = on teste qu'une seule fonction). Dans ce cours, on fait des tests unitaires des fonctions du modèle et on lance les tests depuis un shell donc sans passer par un navigateur car pas besoin de mode graphique.
IMPORTANT: Pour faire des tests unitaires, on a besoin de données dont on est le seul à modifier, et surtout on a besoin de pouvoir "connaître" les données. En effet, si on veut tester une fonction qui récupère un utilisateur, comment vérifier les différentes informations si on les connait pas ? On a besoin d'accéder à la base de données avec un client SQL (ou par un autre moyen) et pouvoir constater que l'utilisateur 2355
a les mêmes informations que ce que nous donne le résultat de notre fonction, par exemple.
Une fois qu'on a une fonction (changée le code précédent dans une fonction):
function getAllItems() //prendre tous les éléments
{
require_once '.const.php';
try {
$dbh = new PDO('mysql:host=' . $dbhost . ';dbname=' . $dbname, $user, $pass);
$query = "SELECT filmmakersnumber, lastname, firstname FROM filmmakers"; //écrire la requête
$statment = $dbh->prepare($query); //préparer la requête
$statment->execute(); //éxecuter la requête
$queryResult = $statment->fetchAll(); //aller chercher le résultat
$dbh = null;
return $queryResult;
} catch (PDOException $e) {
print "Error!: " . $e->getMessage() . "<br/>";
return null;
}
}
on peut faire un test unitaire simple:
//Test unitaire de la fonction getAllItems:
$items = getAllItems();
if (count($items) == 4) {
echo "OK !!";
} else {
echo "BUG ...";
}
Voici des explications d'une proposition de structure et de critères pour des tests basiques, pour des fonctions CRUD:
- un titre "Test de la fonction getUsers()"
- Préparer des données si besoin.
- Utiliser la fonction qu'on va tester, pour faire une action de CRUD
- Tester si le résultat est celui souhaité en vérifiant certains critères
- Affichage d'une erreur ou que le test a réussi.
On préférera faire les tests dans l'ordre suivant: Read, Create, Update, Delete, ce qui permet de lire des données, créer un nouvel élément, le modifier, puis le supprimer. Ainsi à la fin des tests, le contenu de la base de données n'aura pas été modifié.
Pour simplifier, et ne pas constamment parler d'éléments, on va tester ici un modèle CRUD sur des filmmakers. Ces fonctions et tests ont été réalisés et on les trouve dans le repos (le modèle crud.php, le test unitaire testcrud.php)
Read
- Fonction: Compter tous les filmmakers
- tester que le comptage vaut bien le nombre total de filmmakers. (total = valeur brute/fixe)
- Fonction: Lire tous les filmmakers:
- tester si il y a bien le même nombre de filmmakers que donne le count()
- Fonction: Lire un filmmaker (par un identifiant: id ou champ unique)
- Tester que tout le filmmaker lu a des champs qui ont les valeurs attendues.
- Tester que la fonction retourne null si on demande un élément qui n'existe pas.
Create
- Fonction: Créer un filmmaker
- Vérifier que la fonction retourne bien le filmmaker créé (avec son id en plus). donc que ce n'est pas null
- Vérifier qu'il y a un élément de plus que avant la création. (ne pas oublier de compter le nombre avant de créer).
- Vérifier que le nouveau filmmaker (lu avec "Lire un élément" appelé
$readback
) a les même valeurs que celui créé ($filmMakerTest
donc celui que l'on a écrit à la main avant de lancer la création) ou que celui retourné ($newfilmmaker
). Au lieu de tester toutes les informations l'une après l'autre, il suffit de faireempty(array_diff($newfilmmaker, $readback))
qui devra retournertrue
si il n'y a pas de différence.
Update
- Fonction: Mettre à jour un filmmaker
- Regarder que la requête fonctionne
- Après la mise à jour de champs d'un filmmaker, relire le bon filmmaker et vérifier que tous les champs modifiés ont été mise à jour. On peut le faire aussi en vérifiant qu'il n'y a pas de différence entre le filmmaker lu et le filmmaker qu'on a mis à jour (
empty(array_diff($readback, $filmmakertoupdate))
)
Delete
- Fonction: Supprimer un filmmaker
- Regarder que la requête fonctionne (arrive bien au bout)
- Regarder qu'il y a un filmmaker de moins qu'avant la suppression. (en comptant avant et après)
- Tenter de relire le filmmaker supprimé. Si on le trouve c'est qu'il n'a pas été supprimé.
Ces différentes vérifications, sont données comme exemple pour un CRUD basique. Dans un CRUD plus complet on aura aussi par exemple Lire tous les films qu'un réalisateur donné a réalisé. On aura donc d'autres vérifications en plus: vérifier que tous les films ont bien été fait par le réalisateur donné, ... Les vérifications proposées sont pour la base, mais sur des fonctions plus complexes on pourra vérifier d'autres critères en plus.
Oui cest possible ! Enfin disons que le résultat généré est affiché en mode console. Donc pas vraiment fait pour une vue. Par contre pour des tests, la gestion du serveur ou de la base de donnée, c'est pratique.
Pour ne pas utiliser d'IDE, on peut lancer le serveur de la manière suivante.
La commande est construite ainsi: php -S hote:port
. pour -S
pensez à "Start". Voyons voir en pratique ce que ca donne.
- se placer à la racine du site
cd C:/Users/John/Documents/AppWeb/
- taper
php -S localhost:8080
. le serveur démarre et affiche les erreurs en cas de problèmes. - ouvrir un navigateur web à l'adresse:
localhost:8080
et on accède au site !
- se placer à la racine du site
cd C:/Users/John/Documents/AppWeb/
- savoir dans quel sous dossier se trouve le fichier de test qu'on veut lancer.
- lancer le fichier avec la commande
php -f <testfile.php>
ouphp -f <unitTests/testfile.php>
si il est placé dans un sous-dossier.
ATTENTION.
Si il y a des chemins de fichiers dans le code (pour rechercher des données d'un fichier .json par ex.), les liens relatifs par rapport à la racine du site pourrait poser problème si on execute depuis le dossier unitTests
puisque les liens seront relatifs au dossier du shell.
Pour ne pas devoir changer 2 fois tous les liens relatifs, il est possible et conseillé de lancer les tests depuis le dossier du fichier index.php
(donc la racine du site) ou du fichier qui est appelé en premier et donc d'où les liens relatifs partent. Par la suite, il suffit de pointer le fichier de test d'un sous-dossier, par exemple php -f unitTests/testfile.php
Alors ce n'est pas directement dans un shell, mais comme ca concerne les commandes système on est pas très loin.
la fonction exec($cmd)
permet de lancer une commande système stockée dans $cmd
.
Un cas concret d'utilisation serait de restaurer la base de données avant de lancer tous les tests unitaires, à l'aide d'un fichier SQL.
$cmd = "mysql -u $user -p$pass < Restore-MCU-PO-Final.sql"; //commande système pour restaurer la base de données.
exec($cmd);
Littéralement: Developpement conduit/dirigé/guidé par des tests.
Principe de développement où on commence par faire les tests puis on fait le code de ce qui est testé (une fonction par exemple), et on code jusqu'à que le test fonctionne. Le développement est donc guidé par des tests.
Ce qu'on fait là avec ces fonctions du modèle est en fait très répétitif! On ne change que la requête, les données en paramètres de la fonction, les données du execute() et changer en fetch()
ou fetchAll()
et ce que la fonction retourne. On ne respecte donc pas du tout la règle DRY (Don't Repeat Yourself).
On a donc les éléments suivants qui sont fixes:
-
require_once '.const.php';
+$dbh = new PDO('mysql:host=' . $dbhost . ';dbname=' . $dbname, $user, $pass);
. On le remplace par :function getPDO() //create the PDO object { require '.const.php'; //récuperer les identifiants return new PDO('mysql:host=' . $dbhost . ';dbname=' . $dbname, $user, $pass); //créer un objet PDO }
-
le
$statment = $dbh->prepare($query);
-
le
$statment->execute();
qui ne bouge pas peu importe si il y a des données ou pas en paramètre. -
le
$dbh = null;
-
le
try
et le contenu ducatch ...
try { } catch (PDOException $e) { echo "Error!: " . $e->getMessage() . "\n"; return null; }
Bon maintenant qu'on voit une bonne partie est fixe, on arrive sur la question de "Comment faire pour gérer les différents cas de fetch()
, fetchAll()
, données ou pas, return ou pas, etc."
Voici comment on peut faire, même si on ne peut pas faire qu'une seule fonction.
$query = "UPDATE filmmakers SET filmmakers: "