- TP : WF3 Zoo
- Exercice 1
- Exercice 2
- Exercice 3
- Exercice 4
- Exercice 5
- Exercice 6
- Exercice 7 : améliorer le INSERT en préparant la requête
- Exercice 8 : Créer une page d'édition d'un animal (UPDATE)
- Exercice 9 : Gérer le DELETE d'un animal
- Exercice 10 : Authentification
- Pistes d'améliorations
- PDO : Afficher la requête qu'un prepare/execute a créé afin de la débuguer
- PDO : Afficher les erreurs dans PDO
- Gestion d'erreurs : Gérer les exceptions avec try/catch
- Authentification : Bloquer les pages d'ajout, modification, suppression
- Authentification : Hasher les mots de passe
- Upload de fichiers
- 1. Indiquer au formulaire d'accepter l'upload de fichiers
- 2. Ajouter un champ
input:file
- 3. Récupérer le fichier uploadé dans le fichier de traitement
- 4. Effectuer les validations éventuelles (taille, format...)
- 5. Effectuer les traitements éventuels (renommer, resizer...)
- 6. Déplacer le fichier de son emplacement temporaire à son emplacement final que l'on aura défini
- 7. Enregistrer le nom du fichier en base de données avec l'élément auquel on rattache le fichier
Faire le découpage du template en "partials" qui seront inclus dans les pages index.php
et show.php
grâce à include()
. Le découpage sera le suivant :
navbar.php
: barre de navigationjumbotron.php
: section avec la classejumbotron
footer.php
: balise footer
Créer la base de données du projet : wf3zoo
et la table suivante. Remplir la table créée avec cinq entrées.
ANIMAL
------
id PK INT AI
espece VARCHAR(150)
nom VARCHAR(70)
taille INT()
poids INT()
date_de_naissance DATETIME
pays_origine VARCHAR(50)
sexe TINYINT
- Récupérer les données de la table
Animal
et les afficher dans la page d'accueil. - Faire un lien vers la page
show.php
en passant en paramètre GET la cléid
et en valeur l'id
de l'animal.
Documentation : Cards Bootstrap
<?php
$bdd = new PDO('mysql:host=localhost;dbname=NOMDELABDD;charset=utf8;port=3306', 'loginBdd', 'passwordBdd');
$request = "SELECT * FROM movies";
$response = $bdd->query($request);
$movies = $response->fetchAll(PDO::FETCH_ASSOC);
?>
// ...
<?php foreach ($movies as $movie) : ?>
<h1><?= $movie['title'] ?></h1>
<?php endforeach ?>
- Dans
show.php
, en utilisant une requête SQL adaptée pour récupérer l'animal choisi, affichez les données de l'animal.
// Quand on récupère 1 seul élément en BDD, on utilise plutôt :
$movie = $response->fetch(PDO::FETCH_ASSOC);
- Créez les fichiers suivants :
add.php
(il contiendra le formulaire de création d'un animal)create.php
(il contiendra le traitement du formulaire)
Créez un formulaire dans add.php
contenant les champs nécessaires à la création d'un animal. Ce formulaire aura la méthode POST
et l'action create.php
:
<form method="post" action="create.php">
// ...
<input class="btn btn-primary" type="submit" value="Créer un animal">
</form>
Note : n'oubliez pas les attributs
name
dans les balisesinput
,textarea
ouselect
!
Dans create.php
:
- Récupérez les données en POST et composez la requête dans
$request
permettant d'enregistrer un nouvel animal. - Exécutez la avec
$bdd->query($request)
. - Ensuite, redirigez l'utilisateur vers la page
index.php
grâce à la ligne suivante:
header('Location: index.php');
En utilisant query()
, on se rend compte que concaténer sa requête avec les paramètres POST est très contraignant ! La requête devient lourde à écrire et est quasiment impossible à maintenir.
Vous allez plutôt utiliser une requête préparée pour effectuer l'INSERT. Les requêtes préparées ont deux avantages :
- Elles permettent une requête bien plus lisible et naturelle plutôt qu'en utilisant la concaténation
- Elles permettent d'échapper nos paramètres, c'est à dire de supprimer les caractères spéciaux afin d'éviter que l'utilisateur ne saisisse de requêtes SQL (hein, quoi ? Mon animal ne peut pas s'appeler
Simba'); DROP TABLE Animal; --
peut être ? )
Voici le format :
$request = "SELECT * FROM movies";
$response = $bdd->prepare($request);
$response->execute();
$movies = $response->fetchAll(PDO::FETCH_ASSOC);
$request = "SELECT * FROM movies WHERE id = :id";
$response = $bdd->prepare($request);
$response->execute([
'id' => $_GET['id']
]);
$movies = $response->fetchAll(PDO::FETCH_ASSOC);
On déchiffre ça :
/**
* Dans ma requête, au lieu de concaténer $_GET['id'], je met une "pseudo-variable" nommée :id.
* Je peux mettre autant de pseudo-variables que je veux.
* Je peux les nommer comme je le souhaite, (:id, ou encore :idMovieChoisi...)
*/
$request = "SELECT * FROM movies WHERE id = :id";
/**
* Comme ma requête telle qu'elle n'est maintenant plus vraiment valable en MySQL
* (à cause des pseudo-variables), je n'exécute plus avec query() mais je prépare mon $bdd
* à exécuter la requête.
*
* Ça permet à $bdd de s'attendre à recevoir de vraies variables pour remplacer les pseudo-variables.
*/
$response = $bdd->prepare($request);
/**
* On remplace donc effectivement les pseudo-variables par les vraies variables !
*/
$response->execute([
'id' => $_GET['id'],
'pseudo_variable' => $variable_a_inserer
]);
- Créer une page
edit.php
- Dans
index.php
, créer un lien versedit.php
en passant la cléid
contenant en valeur le champID
de l'animal (exactement comme pourshow.php
) - Dans
edit.php
, copiez-collez le formulaire deadd.php
- Préremplissez le formulaire avec les données de l'animal récupérées comme vous le faites dans
show.php
.
Note - Pour préremplir des champs d'un formulaire HTML :
- Préremplir un champ
input
de formulaire avec l'attributvalue
:
<input id="editFormNom" type="text" class="form-control" value="Donnée préremplie">
- Présélectionner un champ
select
de formulaire avec l'attributselected
:
<select name="" id="" class="form-control">
<option value="">France</option>
<option value="" selected>Allemagne</option>
</select>
- Précocher un champ
input:checkbox
ouinput:radio
de formulaire avec l'attributchecked
:
<label for="checkbox1">Checkbox 1</label>
<input type="checkbox" name="test" id="checkbox1">
<label for="checkbox">Checkbox 2</label>
<input type="checkbox" name="test" id="checkbox" checked>
- Traitez le formulaire dans un nouveau fichier
update.php
en utilisant une requête préparée.
-
Créer une page
confirmDelete.php
-
Dans
index.php
, créer un lien versconfirmDelete.php
en passant la cléid
contenant en valeur le champID
de l'animal (exactement comme pourshow.php
) -
Dans
confirmDelete.php
, récupérez les données de l'animal choisi et ajoutez un texte de confirmation de suppression de l'animal qui doit afficher :Êtes-vous sûr de vouloir supprimer l'animal *NomDeLanimal* de l'espèce *EspèceDeLanimal* ? Oui / Non (boutons)
-
Lien sur le bouton "Non" : redirection vers la page d'accueil
-
Lien sur le bouton "Oui" : redirection vers une page
delete.php
en passant la cléid
contenant en valeur le champID
de l'animal -
Dans
delete.php
, traiter la suppression de l'animal
Nous allons créer un mini système d'authentification afin de devoir se loguer pour accéder aux pages modifiantes (insert, update, delete). Le système fonctionnera ainsi :
- Les pages
index.php
etshow.php
restent publiques - Les pages
create.php
,add.php
,update.php
,edit.php
,confirmDelete.php
,delete.php
ne seront accessibles que si je suis connecté.
Il faut donc :
- Créer une table
User
- Créer un formulaire de création de compte
- Créer un formulaire de connexion
- Faire comprendre à notre application que l'utilisateur est dans un état
logué
afin de tester l'accès aux pages.
Créez la table suivante :
User
----
id INT PK AI
email VARCHAR(70)
password VARCHAR(150)
created_at DATETIME DEFAULT=CURRENT_TIMESTAMP
Note : la valeur par défaut
CURRENT_TIMESTAMP
se renseigne lors de la création d'une table avec MySQL et se remplira automatiquement lors d'unINSERT
avec la date actuelle.
Dans une page signUp.php
, créez un formulaire de connexion permettant de renseigner les champs email
et password
.
- Le formulaire contiendra 3 champs:
email
,password
etconfirmPassword
. - L'action du formulaire redirigera vers
signUpTraitement.php
. - Dans
signUpTraitement.php
: si les mots de passe sont identiques, insérez le nouvel utilisateur en récupérant les champsemail
etpassword
puis affichezVotre compte a bien été créé !
. Dans le cas contraire, affichez une erreurLes mots de passe ne correspondent pas.
- Dans une page
login.php
, créez un formulaire de connexion permettant de renseigner les champsemail
etpassword
. - Dans une page
loginTraitement.php
, faites une requêteSELECT
qui ira chercher un utilisateur avec la clauseWHERE
, en cherchant à la fois dans le champemail
et à la fois dans le champpassword
. - Si un utilisateur a été trouvé, affichez à la suite dans
loginTraitement.php
:Vous êtes bien connecté !
. Sinon, affichezErreur d'authentification.
.
4. Faire comprendre à notre application que l'utilisateur est dans un état logué
afin de tester l'accès aux pages
Pour cela, nous avons besoin de créer une variable qui soit accessible dans toute l'application et qui nous permette de savoir si l'utilisateur est logué ou non ! Pour ce faire, allons utiliser la supergloable $_SESSION
.
Les variables de sessions sont des variables qui existent pour chaque visiteur et qui peuvent être différentes entre chaque visiteur. Ce sont des variables qui contiendront des informations propres à la visite : des données à retenir comme un panier, la dernière page visitée, les erreurs de formulaire ou... l'authentification !
Pour activer l'usage des sessions dans PHP, il faut avoir un session_start()
au début de chaque fichier utilisant les sessions.
ATTENTION !!! Il faut vraiment que ce soit vraiment tout, tout, tout au début !
Pour cela, créez le fichier suivant : config.php
et mettez dedans :
<?php
session_start();
Ensuite, dans tous les autres fichiers de l'application, importez avec require_once
le fichier config.php
. Ça y est, les sessions sont activées !
Pour les tester, essayez le code suivant :
Dans index.php
, n'importe où :
$_SESSION['essai_session'] = "ça marche !";
Dans n'importe quel autre fichier, par exemple add.php
, faites un var_dump($_SESSION)
. La clé essai_session
devrait apparaître.
Les sessions sont donc un autre moyen de transmettre des données, cette-fois ci non pas de page en page mais à l'ensemble de l'application sans avoir à préciser à quelle page envoyer les données, là où
$_GET
et$_POST
ne fonctionnent que de page à page.
Pour mettre pratique, modifiez loginTraitement.php
et créez une variable de session contenant l'utilisateur :
$user = $response->fetch(PDO::FETCH_ASSOC);
$_SESSION['user'] = $user;
Ensuite, dans la navbar:
- Testez si
$_SESSION['user']
existe - Si oui, affichez :
Bienvenue user@email.com !
Sur le même principe, en fonction de si l'utilisateur est logué ou non, affichez les liens suivants :
- Non connecté (
$_SESSION['user']
n'existe pas) :- "Connectez-vous" (vers
login.php
) - "Créer un compte" (vers
signUp.php
)
- "Connectez-vous" (vers
2 Connecté : ($_SESSION['user']
existe) :
- "Bienvenue, example@gmail.com !" (avec bien sûr l'email de l'utilisateur)
- "Déconnexion" (un lien qui irait vers logout.php
)
Dans logout.php
(n'oubliez pas d'importer session_start()
dedans aussi), écrivez la ligne suivante :
session_destroy();
Puis redirigez vers la page d'accueil. session_destroy()
permet de... détruire la session ! Si tout se passe bien, les liens dans la navbar créés au point (4) ci-dessus, doivent s'afficher tels que l'utilisateur est déconnecté.
Une requête préparée est composée de deux éléments :
- La requête composée de pseudo-variables
- Le tableau de données remplaçant les pseudo-variables
Si on souhaite voir la requête effectivement lue par MySQL, on ne peut var_dump aucun de ces deux éléments ! En effet, le (1) est une pseudo-requête, le (2) n'est qu'une liste de valeurs.
On va utiliser une méthode dédiée au débug des requêtes :
$statement->debugDumpParams();
Un var_dump($statement->debugDumpParams());
devrait nous retourner quelque chose comme la string suivante :
SQL: [180] INSERT INTO Animal (espece, nom, taille, poids, date_de_naissance, pays_origine, sexe) VALUES (:espece, :nom, :taille, :poids, :date_de_naissance, :pays_origine, :sexe) Sent SQL: [178] INSERT INTO Animal (espece, nom, taille, poids, date_de_naissance, pays_origine, sexe) VALUES ('ijsdmfj', 'jljselfjlsm', '7689', '67989', '1991-03-23', 'sdjhfs', '0') Params: 7 Key: Name: [7] :espece paramno=-1 name=[7] ":espece" is_param=1 param_type=2 Key: Name: [4] :nom paramno=-1 name=[4] ":nom" is_param=1 param_type=2 Key: Name: [7] :taille paramno=-1 name=[7] ":taille" is_param=1 param_type=2 Key: Name: [6] :poids paramno=-1 name=[6] ":poids" is_param=1 param_type=2 Key: Name: [18] :date_de_naissance paramno=-1 name=[18] ":date_de_naissance" is_param=1 param_type=2 Key: Name: [13] :pays_origine paramno=-1 name=[13] ":pays_origine" is_param=1 param_type=2 Key: Name: [5] :sexe paramno=-1 name=[5] ":sexe" is_param=1 param_type=2
Bien qu'elle soit compliquée à lire, on peut isoler le texte après Sent SQL:
jusqu'à Params
qui contient la requête effectivement envoyée à MySQL :
INSERT INTO Animal (espece, nom, taille, poids, date_de_naissance, pays_origine, sexe) VALUES ('ijsdmfj', 'jljselfjlsm', '7689', '67989', '1991-03-23', 'sdjhfs', '0')
Et voilà ! Copiez cette requête trouvée dans MySQL Workbench afin de pouvoir l'éditer et la modifier dans l'éditeur Workbench pour pouvoir corriger les erreurs.
Nous allons faire en sorte que PDO nous affiche les erreurs MySQL directement dans les pages en PHP. Lors de la création de $bdd
, on peut configurer PDO ainsi :
$bdd = new PDO('mysql:host=localhost;dbname=wf3zoo;charset=utf8;port=8889', 'root', 'root', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
En ajoutant cet array de paramètres à la fin, PDO nous affichera dorénavant les erreurs SQL d'une requête !
Gérer les erreurs d'un script va nous permettre de décider ce que l'on fait en cas d'erreur : afficher l'erreur sous une plus jolie forme, rediriger l'utilisateur... Par exemple, nous allons prendre les erreurs de PDO et les afficher dans un echo
puis terminer le script.
Voici le code de base d'un try/catch :
try {
// code à exécuter
}
catch(Exception $e) {
echo "Il ya eu une erreur : " . $e->getMessage(); // on affiche le message d'erreur
die; // on arrête le script immédiatement
}
Le bloc try/catch
("essaie de faire ce code, sinon attrape l'erreur") nous permet d'avoir une gestion des erreurs qu'un bloc de code pourrait être ammené à emmettre. En l'occurrence, la classe PDO (rappel : $bdd
est un objet de la classe PDO
) renvoie des erreurs que nous pouvons attraper et gérer au besoin !
try {
$request = "INSERT INTO Animal (espece, nom, taille, poids, date_de_naissance, pays_origine, sexe)
VALUES (:espece, :nom, :taille, :poids, :date_de_naissance, :pays_origine, :sexe)";
$response = $bdd->prepare($request);
$response->execute([
'espece' => $_POST['espece'],
'nom' => $_POST['nom'],
'taille' => $_POST['taille'],
'poids' => $_POST['poids'],
'date_de_naissance' => $_POST['date_de_naissance'],
'pays_origine' => $_POST['pays_origine'],
'sexe' => $_POST['sexe'],
]);
}
catch(Exception $e) {
echo "Il ya eu une erreur : " . $e->getMessage();
die;
}
Et voilà : plutôt qu'une erreur en orange avec XDebug, nous avons géré comment afficher l'erreur.
Dans les pages correspondant aux actions de INSERT
, UPDATE
, DELETE
(donc : create.php
, add.php
, update.php
, edit.php
, confirmDelete.php
, delete.php
), vous allez tester si l'utilisateur est connecté. Si ça n'est pas le cas, redirigez-le en page d'accueil :
// Si $_SESSION['user'] n'existe PAS (attention au point d'exclamation)
if (!isset($_SESSION['user'])) {
header('Location: index.php');
}
Pour le moment, nos mots de passe sont visibles en clair dans la base de données, ce qui est loin d'être une bonne chose ! Il suffit que notre base de données se fasse pirater d'une manière ou d'une autre (par des hackers venus de l'extérieur ou simplement des personnes ayant accès à la base de données) pour voir les mots de passes de tous nos utilisateurs dans la nature. Et ça n'arrive pas qu'aux autres ! Des grands noms comme Sony, Dropbox, Nintendo, Adobe (une bonne liste ici) se font hacker, personne n'est donc à l'abri. Par contre, on peut limiter les dégâts en protégeant les données sensibles comme les mots de passe.
Vous pouvez vérifier si une de vos adresses e-mail se trouve dans des listes de comptes hackés sur le site Have I Been Pwned.
Concrètement, l'idée est de hasher nos mots de passe, c'est à dire de les transformer grâce à un algorithme choisie en une chaîne de caractère indéchiffrable dans l'autre sens. Par exemple, avec SHA-2, un algorithme à l'origine concu par la NSA :
Mot de passe | Hash en SHA 512 |
---|---|
bonjour |
3041edbcdd46190c0acc504ed195f8a90129efcab173a7b9ac4646b92d04cc80005acaa3554f4b1df839eacadc2491cb623bf3aa6f9eb44f6ea8ca005821d25d |
Bonjour |
c447dff0d671f62ad580b255b64f7a8f6a30d1b828569cee08b7c861239f8d4856ef38a1166718b045a9713876336c1f623619f6a78fc891d48d0b98c703def3 |
BONJOUR |
c65afee89066dfe6d50ee8b9d4d95f6f26fe7a9395e5791cd15076a67d1725fb6f9fe30d8e27d1be1fc0c1cc3bf3b584a327443eaf330e3c05676520149f3683 |
On remarque qu'en plus de n'avoir aucun moyen de repasser du mot de passe hashé au mot de passe d'origine, on ne peut pas non plus faire d'analyse de fréquence sur les mots de passe hashés (c'est à dire, comparer les récurrences afin de dire quelque chose comme "8a90129e
correspond à la lettre A" ).
En pratique :
- Création de compte : L'utilisateur va tapper son mot de passe comme habituellement. Par contre, à l'enregistrement, on passera le mot de passe dans
password_hash()
pour le INSERT. - Login : L'utilisateur va tapper son mot de passe comme habituellement. Par contre, à la lecture, on utilisera
password_verify()
qui comparera le mot de passe saisi et la version hashée.
Pour mettre en place cela :
-
Changez dans le
execute()
qui gère l'INSERT INTO User
, la ligne "password" de la façon suivante :"password" => password_hash($_POST["password"]),
-
Changez le
SELECT * FROM Users WHERE...
qui récupère le user de la façon suivante :SELECT * FROM User WHERE email = :email
-
En effet, nous n'allons chercher que par e-mail dorénavant.
-
Ensuite, vérifiez si l'utilisateur a saisi un bon mot de passe grâce à :
$user = $response->fetch(PDO::FETCH_ASSOC); if ( password_verify($_POST['user'], $user['password']) ) { // l'utilisateur est connecté }
-
Et voilà !
Pour uploader un fichier, on va :
- Indiquer au formulaire d'accepter l'upload de fichiers
- Ajouter un champ
input:file
- Récupérer le fichier uploadé dans le fichier de traitement
- Effectuer les validations éventuelles (taille, format...)
- Effectuer les traitements éventuels (renommer, resizer...)
- Déplacer le fichier de son emplacement temporaire à son emplacement final que l'on aura défini
- Enregistrer le nom du fichier en base de données avec l'élément auquel on rattache le fichier
Modifiez le formulaire en rajoutant l'attribut enctype
:
<form action="" method="" enctype="multipart/form-data">
<input type="file" name="photo_animal">
// Les fichiers issus des champs input:file d'un formulaire avec enctype="multipart/form-data" se retrouvent dans $_FILES :
var_dump($_FILES);
// Notre fichier se retrouve dans :
$_FILES['photo_animal']
On peut avoir des informations sur le fichier grâce à :
$photoAnimal = $_FILES['photo_animal'];
$tailleDuFichier = $photoAnimal['size']; //
$pathinfoData = pathinfo($photoAnimal);
$nomDuFichier = $pathinfoData['filename'];
$extensionDuFichier = $pathinfoData['extension'];
Grâce à ces informations-là, vous pouvez valider si l'extension est valide, si la taille est valide, si le nom de fichier est valide...
Pour être certains d'avoir des noms de fichiers uniques, nous allons renommer nos fichiers de la façon suivante grâce à la fonction PHP uniqid()
:
NOM_DU_FICHIER-ID_UNIQUE.EXTENSION
Ce qui transformerait Simba.png
en Simba-84d3fgj3d.png
:
$photoAnimal = $_FILES['photo_animal']; // on récupère le fichier
$pathinfoData = pathinfo($photoAnimal); // on récupère les infos du chemin du fichier
$nomDuFichier = $pathinfoData['filename']; // on récupère le nom de fichier
$extensionDuFichier = $pathinfoData['extension']; // on récupère l'extension du fichier
$nouveauNomDuFichier = $nomDuFichier . '-' . uniqid() . '.' . $extensionDuFichier; // on compose le nouveau nom
Le fichier est pour le moment dans un emplacement temporaire ($_FILES['photo_animal']['tmp_name']
). Déplaçons-le dans le dossier uploads
de notre projet, et donnons-lui le nouveau nom :
move_uploaded_file($photoAnimal['tmp_name'], __DIR__ . '/uploads/' . $nouveauNomDuFichier );
Note : La constante magique
__DIR__
permet d'avoir le chemin absolu vers le fichier qui est en train d'exécuter le script. C'est très utile pour gérer les chemins de fichiers en PHP et savoir où on en est !
- Modifiez votre base de données et ajoutez un champ
file VARCHAR(50)
. - Modifiez l'
INSERT
d'origine et ajoutez le nouveau champ. Modifiez également leexecute()
:
execute([
//...
'file' => $nouveauNomDeFichier,
])