TD6 – Architecture MVC avancée 2/2 Vues modulaires, filtrage, formulaires améliorés
Nous continuons de développer notre site-école de covoiturage. En attendant de pouvoir gérer les sessions d’utilisateur, nous allons développer l’interface “administrateur” du site.
Ce TD présuppose que vous avez fini le TD précédent.
Amélioration du routeur
On veut ajouter un comportement par défaut du routeur qui est contenu dans le
contrôleur frontal. Nous allons faire en sorte qu’un utilisateur qui arrive sur
controleurFrontal.php
voit la même page que s’il était arrivé sur
controleurFrontal.php?action=afficherListe
.
Action par défaut
-
Si aucun paramètre n’est donné dans l’URL, initialisons la variable
action
avec la chaîne de caractères"afficherListe"
danscontroleurFrontal.php
. Utilisez la fonctionisset($_GET['action'])
qui teste si la variable$_GET['action']
a été initialisée, ce qui est le cas si et seulement si une variableaction
a été donnée dans l’URL. -
Testez votre site en appelant
controleurFrontal.php
sans action.
Note : De manière générale, il ne faut jamais lire la case d’un tableau
avant d’avoir vérifié qu’elle était bien définie avec un isset(...)
sous peine
d’avoir des erreurs Undefined index : ...
.
Désormais, la page http://localhost/tds-php/TD6/web/controleurFrontal.php doit marcher sans paramètre (http://webinfo.iutmontp.univ-montp2.fr/~votre_login/TD6/web/controleurFrontal.php si vous hébergez le site sur le serveur de l’IUT).
Vérification de l’action
On souhaite que le routeur vérifie que action
est le nom d’une méthode de
ControleurUtilisateur.php
avant d’appeler cette méthode. Sinon, nous renverrons
vers une page d’erreur.
-
Créez une action
afficherErreur(string $messageErreur = "")
dans le contrôleur utilisateur qui affiche la vue d’erreursrc/vue/utilisateur/erreur.php
contenant le message d’erreur Problème avec l’utilisateur :$messageErreur
, ou juste Problème avec l’utilisateur si le message est vide. Pour ce faire, il faudra adapter la vueerreur.php
créée dans les TDs précédents. -
Modifiez le code du routeur pour implémenter la vérification de l’action. Si l’action n’existe pas, appelez l’action
afficherErreur
.Notes :
- Vous pouvez récupérer le tableau des méthodes visibles d’une classe avec
la fonction
get_class_methods()
et tester si une valeur appartient à un tableau avec la fonctionin_array
. get_class_methods()
prend en argument une chaine de caractères contenant le nom de la classe qualifié, c.-à-d. avec lenamespace
.
- Vous pouvez récupérer le tableau des méthodes visibles d’une classe avec
la fonction
-
Modifiez tous les appels à
afficherVue
vers'utilisateur/erreur.php'
pour qu’ils utilisentafficherErreur
avec un message adéquat.
Séparation des données et de leur persistance
Une bonne pratique de la programmation orientée objet est de suivre des
principes de conception, notamment SOLID.
Vous en avez entendu parler l’an dernier en cours de Développement Orienté Objet et vous allez également
les aborder dans le cours Qualité de développement de Semestre 3. Le S
de SOLID signifie
Single responsibility principle (ou principe de responsabilité unique en
français) : chaque classe doit faire une seule tâche.
Actuellement, notre classe ModeleUtilisateur
gère 2 tâches : la gestion des
utilisateurs et leur persistance dans une base de donnée. Ceci est contraire aux
principes SOLID. Plus concrètement, si on veut enregistrer un utilisateur
différemment plus tard (dans une session, dans un fichier, via un appel d’API,
ou avec une classe mock pour des tests), cela impliquera beaucoup de
réécriture de code.
Nous allons séparer les méthodes gérant la persistance des données des autres méthodes propres aux utilisateurs (méthodes métiers). Voici le diagramme de classe UML modifié que nous allons obtenir à la fin de cette section :
Notez que dans le schéma UML ci-dessus :
ModeleUtilisateur
est scindé en deux classesUtilisateurRepository
etUtilisateur
.UtilisateurRepository
etUtilisateur
ont changé de dossier et denamespace
par rapport àModeleUtilisateur
.ajouter
est maintenant une méthode statique qui prend unUtilisateur
en argument.- La classe
UtilisateurRepository
dépend deUtilisateur
, mais pas l’inverse (d’où la direction du lien de dépendance entre les deux classes).
En termes de fichiers, nous aurons l’arborescence suivante après nos modifications :
Le dossier Repository
gère la persistance des données. Le nom Repository
est
le nom du patron de conception que l’on utilise et que l’on retrouve dans les
outils professionnels (ORM Doctrine par exemple).
-
Renommez la classe
ModeleUtilisateur
enUtilisateur
.
Utilisez le refactoring de PhpStorm : Clic droit sur le nom de la classe > Refactor > Rename. -
Créez deux dossiers
DataObject
etRepository
dansModele
. - Créez une classe
UtilisateurRepository
dans le dossierRepository
avec lenamespace
correspondant (App\Covoiturage\Modele\Repository
). Déplacez les méthodes suivantes deUtilisateur
dansUtilisateurRepository
:recupererUtilisateurs
recupererUtilisateurParLogin
ajouter
: À transformer en une méthode statique prenant en paramètre un objet de typeUtilisateur
. Cet objet sera l’utilisateur à ajouter. Utilisez donc les getters de cetUtilisateur
afin de retrouver les données à insérer dans la requête SQL de la méthodeajouter
.construireDepuisTableauSQL
: changez si nécessaire le corps de la fonction afin qu’un objetUtilisateur
soit correctement retourné.- éventuellement
recupererTrajetsCommePassager
: À transformer en une méthodepublic static
prenant en paramètre un objet de typeUtilisateur
dont les getters vous serviront dans les données à insérer dans la requête SQL.
Mettez à jour l’appel de cette fonction dansUtilisateur
.
Pensez également à adapter le code des autres fonctions de la classe
UtilisateurRepository
afin qu’elles appellent correctement la méthodeconstruireDepuisTableauSQL
. -
Déplacer
Utilisateur
dans le dossierDataObject
etConnexionBaseDeDonnees
dansRepository
.Attention si vous utilisez le drag & drop de PhpStorm, vous allez avoir des mauvaises surprises car les
namespace
risquent de ne pas se mettre à jour correctement…
La façon correcte de le faire : Clic droit sur le nom de la classe > Refactor > Move Class > Indiquer lenamespace
correspondant.Vérifiez que votre code correspond à celui indiqué dans le diagramme de classe évoqué précédemment.
- Faites remarcher les actions une par une :
afficherListe
:recupererUtilisateurs
appartient à la classeUtilisateurRepository
désormais.
afficherDetail
:recupererUtilisateurParLogin
appartient à la classeUtilisateurRepository
.
creerDepuisFormulaire
:ajouter
etrecupererUtilisateurs
appartiennent à la classeUtilisateurRepository
désormais.ajouter
sera maintenant statique et prendra en argument un objet de la classeUtilisateur
; les getters deUtilisateur
servent à construire la requête SQL.
CRUD pour les utilisateurs
CRUD est un acronyme pour Create/Read/Update/Delete, qui sont les quatre opérations de base de toute donnée. Nous allons compléter notre site pour qu’il implémente toutes ces fonctionnalités. Lors des TDs précédents, nous avons implémenté nos premières actions :
- Read – afficher tous les utilisateurs : action
afficherListe
- Read – afficher les détails d’un utilisateur : action
afficherDetail
- Create – afficher le formulaire de création d’un utilisateur : action
afficherFormulaireCreation
- Create – créer un utilisateur dans la BDD : action
creerDepuisFormulaire
Nous allons compléter ces opérations avec la mise à jour et une version améliorée de la suppression.
Action supprimer
Nous souhaitons ajouter l’action supprimer
aux utilisateurs. Pour cela :
-
Écrivez dans
UtilisateurRepository
une méthode statiquesupprimerParLogin($login)
qui prend en entrée le login à supprimer (pensez à utiliser les requêtes préparées dePDO
). -
Créez une vue
src/vue/utilisateur/utilisateurSupprime.php
qui affiche “L’utilisateur de login$login
a bien été supprimé”, suivi de la liste des utilisateurs en appelant la vueliste.php
(de la même manière queutilisateurCreee.php
). -
Écrivez l’action
supprimer
du contrôleur d’utilisateur pour que- il supprime l’utilisateur dont le login est passé en paramètre dans l’URL,
- il affiche la vue
utilisateurSupprime.php
en utilisant le mécanisme de vue générique, et en donnant en paramètres les variables nécessaires dans la vue.
-
Enrichissez la vue
liste.php
pour ajouter des liens HTML qui permettent de supprimer un utilisateur.Aide : Procédez par étape. Écrivez d’abord un lien fixe dans votre vue, puis la partie qui dépend de l’utilisateur.
-
Testez le tout. Quand la fonctionnalité marche, appréciez l’instant.
Actions afficherFormulaireMiseAJour
et mettreAJour
Nous souhaitons ajouter l’action afficherFormulaireMiseAJour
, qui affiche le
formulaire de mise à jour, aux utilisateurs. Pour cela :
-
Créez une vue
src/vue/utilisateur/formulaireMiseAJour.php
qui affiche un formulaire identique à celui deformulaireCreation.php
, mais qui sera prérempli par les données de l’utilisateur courant. Voici quelques points à prendre en compte avant de se lancer :-
La vue reçoit un objet
Utilisateur $utilisateur
qui servira à remplir le formulaire avec ses attributs. -
L’attribut
value
de la balise<input>
permet de préremplir un champ du formulaire. Utilisez l’attribut HTMLreadonly
de<input>
pour que l’internaute ne puisse pas changer le login. - Rappel : Vous souhaitez envoyer l’information
action=mettreAJour
en plus des informations saisies lors de l’envoi du formulaire. La bonne façon de faire pour un formulaire de méthodeGET
est d’ajouter un champ caché<input type='hidden' name='action' value='mettreAJour'>
-
Pensez bien à échapper vos variables PHP avant de les écrire dans l’HTML et dans les URL.
- Astuce optionnelle : La vue
formulaireMiseAJour.php
peut être raccourcie en utilisant la syntaxe<?= $loginHTML ?>
qui est équivalente à
<?php echo $loginHTML; ?>
-
-
Écrivez l’action
afficherFormulaireMiseAJour
du contrôleur d’utilisateur pour qu’il affiche le formulaire prérempli. Nous ne passerons que le login de l’utilisateur via l’URL ; les autres informations seront récupérées dans la BDD viaUtilisateurRepository
.
Vérifiez que l’actionafficherFormulaireMiseAJour
affiche bien le formulaire. -
Ajoutons les liens manquants. Enrichissez la vue
liste.php
pour ajouter des liens HTML qui permettent de mettre à jour un utilisateur. Ces liens pointent donc vers le formulaire de mis-à-jour prérempli.
-
Maintenant, passons à l’action
mettreAJour
qui effectue la mise à jour dans la BDD.Créez la vue
src/vue/utilisateur/utilisateurMisAJour.php
pour qu’elle affiche “L’utilisateur de login$login
a bien été mis à jour”. Affichez en dessous de ce message la liste des utilisateurs mise à jour (à la manière deutilisateurSupprime.php
etutilisateurCreee.php
). -
Ajoutez à
UtilisateurRepository
une méthode statiquemettreAJour(Utilisateur $utilisateur)
. Cette méthode est proche deajouter(Utilisateur $utilisateur)
, à ceci près qu’elle utilise une requête SQLUPDATE
et que son type de retour estvoid
. En effet, on va considérer qu’une mise à jour se passe toujours correctement. -
Créez l’action
mettreAJour
du contrôleur d’utilisateur. Cette action instancie l’utilisateur avec les données provenant du formulaire, appelle ensuiteUtilisateurRepository::mettreAJour
, puis affiche la vuesrc/vue/utilisateur/utilisateurMisAJour.php
.Remarque : N’utilisez pas la méthode
construireDepuisTableauSQL
même si cela marcherait actuellement. En effet, la fonctionconstruireDepuisTableauSQL
est codée pour recevoir une réponse à une requête SQL au format tableau, et non les données d’un formulaire, ce qui sera notamment différent pour les trajets… -
Testez le tout. Quand la fonctionnalité marche, appréciez de nouveau l’instant.
Gérer plusieurs contrôleurs
Maintenant que notre site propose une gestion minimale des utilisateurs (Create / Read / Update / Delete), notre objectif est d’avoir une interface similaire pour les trajets. Dans ce TD, nous allons dans un premier temps rendre notre MVC d’utilisateurs plus générique. Cela nous permettra de l’adapter plus facilement aux trajets dans un second temps.
Dans le routeur du contrôleur frontal
Pour l’instant, nous n’avons travaillé que sur le contrôleur utilisateur. Nous
souhaitons maintenant ajouter le contrôleur trajet. Pour
gérer tous les contrôleurs à partir de notre page d’accueil unique controleurFrontal.php
,
nous avons besoin d’appeler le bon contrôleur dans le routeur.
Désormais, nous devons donc spécifier le contrôleur demandé dans le query
string. Par exemple, l’ancienne page controleurFrontal.php?action=afficherListe
du contrôleur
utilisateur devra s’obtenir avec controleurFrontal.php?controleur=utilisateur&action=afficherListe
.
-
Définissez une variable
controleur
danscontroleurFrontal.php
en récupérant sa valeur à partir de l’URL, et en mettant le contrôleur utilisateur par défaut.Aide : Ce bout de code est similaire à celui concernant
action
danscontroleurFrontal.php
. -
On souhaite créer le nom de la classe à partir de
controleur
. Par exemple, quand$controleur="utilisateur"
, nous souhaitons créer une variable$nomDeClasseControleur
qui vaut"App\Covoiturage\Controleur\ControleurUtilisateur"
.
Créez la variable$nomDeClasseControleur
à l’aide de la fonctionucfirst
(UpperCase FIRST letter) qui sert à mettre en majuscule la première lettre d’une chaîne de caractère. -
Testez si la classe de nom
$nomDeClasseControleur
existe à l’aide de la fonctionclass_exists
et appelez l’actionaction
de la classe$nomDeClasseControleur
le cas échéant. Autrement appelez l’actionafficherErreur
deControleurUtilisateur
. -
Testez votre code en appelant vos anciennes pages du contrôleur utilisateur.
Attention : les liens URL de vos différentes vues risquent de ne plus fonctionner. Si oui, trouvez pourquoi et corrigez.
Début du nouveau contrôleur
Maintenant que notre routeur dans le contrôleur frontal est en place, nous
pouvons créer de nouveaux contrôleurs. Pour avoir un aperçu de l’étendu du
travail, commençons par créer l’action afficherListe
de Trajet
.
-
Créez deux classes
DataObject/Trajet.php
etRepository/TrajetRepository.php
(indépendamment de la classeTrajet
que vous avez fait dans les TDs 2 & 3) -
À partir de votre classe
Trajet
des TDs 2 & 3, copiez/collez :- dans
DataObject/Trajet.php
: les attributs, les getter, les setter et le constructeur. - dans
Repository/TrajetRepository.php
: les fonctionsconstruireDepuisTableauSQL($trajetTableau)
,recupererTrajets()
etrecupererPassagers()
.
- dans
- Corrigeons les appels aux méthodes dans
TrajetRepository.php
:Utilisateur::recupererUtilisateurParLogin
→UtilisateurRepository::recupererUtilisateurParLogin
Utilisateur::construireDepuisTableauSQL
→UtilisateurRepository::construireDepuisTableauSQL
Trajet::construireDepuisTableauSQL
→TrajetRepository::construireDepuisTableauSQL
- changez la signature de la fonction
recupererPassagers
pourstatic public function recupererPassagers(Trajet $trajet): array
et corrigez le tableau de valeurs donné à la requête préparée.
$trajet->recupererPassagers()
→TrajetRepository::recupererPassagers($trajet)
Si vous aviez codé l’attribut
trajetsCommePassager
deUtilisateur
au TD3 :- Dans
UtilisateurRepository.php
:
Trajet::construireDepuisTableauSQL
→TrajetRepository::construireDepuisTableauSQL
- Dans
Utilisateur.php
: importez la classeTrajet
dansUtilisateur.php
(utilisé au niveau du PHPDoc du getter et du setter de l’attributtrajetsCommePassager
).
-
Créez une vue
src/vue/trajet/liste.php
similaire à celle des utilisateurs (en commentant les liens pour l’instant).
Idem pourtrajet/erreur.php
. -
Créez un contrôleur
controleur/ControleurTrajet.php
similaire à celui des utilisateurs qui reprend les méthodesafficherListe()
,afficherVue()
etafficherErreur()
(et commente éventuellement les autres méthodes).
Importez les classes nécessaires.Astuce : Vous pouvez utiliser la fonction de remplacement (
Ctrl+R
sous PHPStorm) pour remplacer tous lesutilisateur
partrajet
. En cochantPréserver la casse
(Preserve case
), vous pouvez faire en sorte de respecter les majuscules lors du remplacement. - Testez votre action en appelant l’action
afficherListe
du contrôleurTrajet
(qui est accessible dans la barre de menu de votre site normalement).
Modèle générique
L’implémentation du CRUD pour les trajets est un code très similaire à celui pour les utilisateurs. Nous pourrions donc copier/coller le code des utilisateurs et changer les (nombreux) endroits nécessaires, mais cela contredirait le principe DRY que vous connaissez depuis l’an dernier.
Création d’un modèle générique
Pour éviter la duplication de code et la perte d’un temps conséquent à
développer le CRUD pour chaque nouveau type d’objet, nous allons mettre en commun le
code autant que possible. Commençons par abstraire les 2 classes métiers Utilisateur
et Trajet
.
Créer une classe abstraite AbstractDataObject
dans le dossier DataObject
.
Faites hériter les autres classes de ce répertoire de AbstractDataObject
pour
correspondre au diagramme de classe ci-dessus (mot clé extends
comme en Java).
Également, nous allons abstraire les classes Repository de façon à obtenir le schéma suivant :
Nous allons détailler ces changements dans les prochaines sections.
Déplaçons de UtilisateurRepository
vers un modèle générique AbstractRepository
toutes les requêtes SQL qui ne sont pas spécifiques aux utilisateurs.
Commençons par la fonction recupererUtilisateurs()
de UtilisateurRepository
. Les seules
différences entre recupererUtilisateurs()
et recupererTrajets()
sont le nom de la table
et le nom de la classe des objets en sortie. Voici donc comment nous allons
faire pour avoir un code générique :
-
Créez une nouvelle classe abstraite
abstract class AbstractRepository
et faites hériter la classeUtilisateurRepository
deAbstractRepository
. - Pour qu’on puisse migrer la fonction
recupererUtilisateurs()
deUtilisateurRepository
versAbstractRepository
, il faudrait que cette dernière puisse accéder au nom de la table. Pour cela, elle va demander à toutes ses classes filles de posséder une méthodegetNomTable()
.
Ajoutez donc une méthode abstraitegetNomTable()
dansAbstractRepository
protected abstract function getNomTable(): string;
et une implémentation de
getNomTable()
dansUtilisateurRepository
.Question : pourquoi la visibilité de cette fonction est
protected
?
Réponse (surlignez le texte caché à droite): Pour rendre accessible cette méthode uniquement à la classe AbstractRepository et à ses classes filles. -
Déplacez la fonction
recupererUtilisateurs()
deUtilisateurRepository
versAbstractRepository
en la renommantrecuperer()
.Astuce : sur PhpStorm le moyen le plus simple pour déplacer la fonction serait Clic droit sur la déclaration de la méthode > Refactor > Move Members > Indiquer
AbstractRepository
comme classe de destination. De même pour le renommage, pensez à utiliser le refactoring. -
Utilisez
getNomTable()
dans la requête SQL derecuperer()
. PuisquegetNomTable()
est une méthode dynamique, enlevez lestatic
derecuperer()
./** * @return AbstractDataObject[] */ public function recuperer(): array
- De même,
AbstractRepository
va demander à toutes ses classes filles de posséder une méthodeconstruireDepuisTableauSQL($objetFormatTableau)
.- Ajoutez donc une méthode abstraite dans
AbstractRepository
protected abstract function construireDepuisTableauSQL(array $objetFormatTableau) : AbstractDataObject;
- Enlevez le
static
de la signature de la fonctionconstruireDepuisTableauSQL()
deUtilisateurRepository
. - Passez tous les appels à
construireDepuisTableauSQL()
deUtilisateurRepository
en appel de méthode d’instance (dynamique) avec(new UtilisateurRepository())->construireDepuisTableauSQL($objetFormatTableau);
Ceci construit un objet anonyme afin de pouvoir appeler les fonctions dynamiques de
UtilisateurRepository
. - Pensez à vérifier que l’implémentation de la méthode
construireDepuisTableauSQL()
deUtilisateurRepository
déclare bien le type de retourUtilisateur
(sous-classe deAbstractDataObject
).
- Ajoutez donc une méthode abstraite dans
-
Corrigez l’action
afficherListe
duControleurUtilisateur
pour faire appel à la méthode d’instancerecuperer()
deUtilisateurRepository
avec :(new UtilisateurRepository())->recuperer();
L’action
afficherListe
du contrôleur utilisateur doit remarcher.Attention : pour les autres actions le code ne marche plus pour l’instant car la migration des appels statiques vers des appels dynamiques n’est pas encore terminée…
- Mettez à jour tous vos appels à
recupererUtilisateurs()
(ourecuperer()
si la méthoderecupererUtilisateurs()
a été correctement renommé par le refactoring de la question 3).
- Faites de même pour
TrajetRepository
:- commentez
recupererTrajets()
, construireDepuisTableauSQL()
passe depublic static
àprotected
. Mettez aussi à jour ses appels.- implémentez
getNomTable()
, TrajetRepository
doit hériter deAbstractRepository
.- l’appel à
UtilisateurRepository::construireDepuisTableauSQL(...)
n’est plus statique.
- commentez
- Corrigez l’action
afficherListe
duControleurTrajet
pour faire appel à la méthoderecuperer()
deTrajetRepository
. L’action doit remarcher.
Action afficherDetail
Pour faciliter les actions afficherDetail
des différents contrôleurs, nous allons créer
une fonction recupererParClePrimaire($valeurClePrimaire)
générique dans AbstractRepository
qui permet de faire une recherche par clé primaire dans une table.
- Commençons par déclarer la fonction
recupererUtilisateurParLogin
dans la classeAbstractRepository
en généralisant la méthode correspondante déjà existante dansUtilisateurRepository
:- utilisez PHPStorm sur la fonction
UtilisateurRepository::recupererUtilisateurParLogin
, clic droit > Refactor > Pull Members Up : ceci aura pour effet de déplacer la fonction dansAbstractRepository
. - utilisez PHPStorm sur la fonction
AbstractRepository::recupererUtilisateurParLogin
, clic droit > Refactor > Rename > indiquezrecupererParClePrimaire
: ceci renommera la méthode ainsi que tous ses appels. - enlevez le
static
de la méthodeAbstractRepository::recupererParClePrimaire
.
Corrigez tous les appels à la méthode avec PHPStorm : FaitesCtlr+Maj+R
pour remplacer dans tous les fichiersUtilisateurRepository::recupererParClePrimaire
par(new UtilisateurRepository())->recupererParClePrimaire
. - Testez que la page de détail d’un utilisateur marche toujours.
- utilisez PHPStorm sur la fonction
-
Pour que la fonction
recupererParClePrimaire(string)
puisse être générique, il faut récupérer nom de la clé primaire du type effectif de$this
. De la même manière qu’avecgetNomTable()
, demandez aux implémentations deAbstractRepository
de fournir une méthodegetNomClePrimaire() : string
. - Transformons
recupererParClePrimaire
en une méthode générique :- Utilisez
getNomTable
etgetNomClePrimaire
pour rendre la requête générique, construireDepuisTableauSQL
doit être appelé sur l’objet courant$this
,- Le type de retour de la méthode est
?AbstractDataObject
, - Changez les noms de variables pour avoir l’air d’une méthode
générique, par exemple
utilisateur
→objet
etlogin
→clePrimaire
. - Testez que la page de détail d’un utilisateur marche toujours.
- Utilisez
Faites de même pour les trajets.
-
Implémentez la méthode
getNomClePrimaire()
deTrajetRepository
. -
Créez l’action
afficherDetail
duControleurTrajet
en vous basant sur celle deControleurUtilisateur
.Rappel : Utilisez le remplacement
Ctrl+R
en préservant la casse pour vous faciliter le travail. -
Créer la vue associée
detail.php
en repartant de l’ancien code deTrajet::toString()
. Ajouter les liens vers la vue de détail dansliste.php
en spécifiant biencontroleur=trajet
dans le query string.
L’actionafficherDetail
doit maintenant fonctionner. -
Question innocente : Avez-vous pensé à échapper vos variables dans vos vues pour le HTML et les URL ?
Ayez toujours un utilisateur et un trajet avec des caractères spéciaux pour le HTML (par ex.<h1>Hack
) et les URL (par ex.a&b=c
) dans votre base de données. Comme ça, vous pourrez tester plus facilement que vous avez sécurisé cet aspect.
Remarque : Observez que lors de l’appel du constructeur de Trajet
dans la fonction construireDepuisTableauSQL
de TrajetRepository
, vous passez en paramètres la référence obtenue à partir de recupererParClePrimaire()
.
La fonction recupererParClePrimaire()
est “générique” et retourne un objet de type AbstractDataObject
ou null
.
Or, la signature du constructeur de Trajet
demande une référence de type Utilisateur
et pas n’importe quel AbstractDataObject
!
À l’exécution ce code fonctionne, car la liaison dynamique fait que le type effectif retourné par recupererParClePrimaire()
est bel et bien
Utilisateur
. Mais la vérification de type ne peut pas être garantie par votre IDE en amont et vous pouvez obtenir un warning.
On touche là aux limites d’un langage non fortement typé : la vérification que les types sont correctement définis et respectés est une
tâche du développeur, contrairement aux langages fortement typés où cette vérification est faite davantage lors de la phase de compilation.
Action supprimer
Pas de nouveautés.
-
Nous vous laissons migrer la fonction
supprimerParLogin($login)
deUtilisateurRepository
versAbstractRepository
en la renommantsupprimer($valeurClePrimaire)
. La méthode devient dynamique. Adapter sa requête SQL.
La suppression d’un utilisateur doit continuer à marcher après la modification. -
Créez l’action
supprimer
du contrôleur trajet, ainsi que sa vue associéetrajetSupprime.php
et son lien dansliste.php
.
Testez que la suppression d’un trajet marche dorénavant.
Actions afficherFormulaireCreation
et creerDepuisFormulaire
Commençons par rendre générique la méthode de création d’entités. Pour reconstituer la requête
INSERT INTO utilisateur (login, nom, prenom) VALUES (:loginTag, :nomTag, :prenomTag)
il est nécessaire de pouvoir lister les champs de la table utilisateur
. De même, il sera nécessaire de lister
les champs de la table trajet
. Nous allons factoriser le code nécessaire dans AbstractRepository
.
- Déplacez la fonction
ajouter($utilisateur)
deUtilisateurRepository
versAbstractRepository
. Changez la signature de la fonction parpublic function ajouter(AbstractDataObject $objet): bool
- Ajoutez une méthode abstraite
getNomsColonnes()
dansAbstractRepository
/** @return string[] */ protected abstract function getNomsColonnes(): array;
et une implémentation de
getNomsColonnes()
dansUtilisateurRepository
/** @return string[] */ protected function getNomsColonnes(): array { return ["login", "nom", "prenom"]; }
- Utilisez
getNomTable()
etgetNomsColonnes()
pour construire la requête SQL deajouter()
:INSERT INTO utilisateur (login, nom, prenom) VALUES (:loginTag, :nomTag, :prenomTag)
Aide :
- La fonction
join(string $separator, array $array)
pourrait vous faire gagner du temps. Elle concatène les éléments de$array
en insérant$separator
entre chaque case. - N’hésitez pas à afficher la requête générée pour vérifier votre code.
- La fonction
- Pour les besoins de
execute()
, nous devons transformer l’objetUtilisateur $utilisateur
en un tableauarray( "loginTag" => $utilisateur->getLogin(), "nomTag" => $utilisateur->getNom(), "prenomTag" => $utilisateur->getPrenom(), );
Ajoutez une méthode abstraite
formatTableauSQL()
dansAbstractRepository
protected abstract function formatTableauSQL(AbstractDataObject $objet): array;
Implémentez cette fonction dans
UtilisateurRepository
avecprotected function formatTableauSQL(AbstractDataObject $utilisateur): array { /** @var Utilisateur $utilisateur */ return array( "loginTag" => $utilisateur->getLogin(), "nomTag" => $utilisateur->getNom(), "prenomTag" => $utilisateur->getPrenom(), ); }
-
Utilisez
formatTableauSQL()
dansajouter()
pour obtenir le tableau donné àexecute()
. - Corrigez l’action
creerDepuisFormulaire
duControleurUtilisateur
pour faire appel aux méthodes deUtilisateurRepository
. L’action doit remarcher.
Passons à la création de trajet. Grâce à l’exercice précédent, la méthode
ajouter
de TrajetRepository
est quasiment fonctionnelle. Finissons cette
méthode.
-
Implémentez la méthode
getNomsColonnes
dansTrajetRepository
. Indiquez bien tous les champs, mêmeid
.PHPStorm peut vous générer le squelette de la méthode avec Clic droit dans la classe
TrajetRepository
> Generate > Implement Methods >getNomsColonnes
. -
Implémentez la méthode
formatTableauSQL
dansTrajetRepository
en vous basant sur le tableau de valeurs de la requête de création de trajets du TD3 qui se trouvait dansTrajet::ajouter()
.
Ajoutez aussi une case"idTag" => $trajet->getId()
dans le tableau renvoyé.
Mais il reste à gérer les actions de contrôleur et les vues de création. Démarrons par le formulaire de création.
- Créez la vue
vue/trajet/formulaireCreation.php
en vous basant sur votre formulaire de création de trajets du TD3.
Modifiez l’action
de<form>
et rajoutez deux<input type="hidden">
pour indiquer le contrôleur et l’action souhaités (inspirez-vous du formulaire de création des utilisateurs). - Créez l’action
afficherFormulaireCreation
dansControleurTrajet
en vous inspirant du MVC utilisateur. - Rajoutez dans
vue/trajet/liste.php
un lien vers le formulaire de création. - Testez que le lien vous amène bien vers un formulaire de création de trajet.
Passons à l’action de création de trajet.
-
Créez l’action
creerDepuisFormulaire
dansControleurTrajet
en vous inspirant du scriptcreerTrajet.php
du TD3, et de l’action similaire des utilisateurs.Ne traitez pas spécialement les cas d’erreur pour l’instant. Donnez un identifiant
null
au trajet. - Créez la vue
vue/trajet/trajetCree.php
similaire à celle des utilisateurs. -
Testez la création d’un trajet à partir du formulaire.
Aide : Si vous avez une erreur
Fatal error: Uncaught TypeError: ...\Trajet::getId(): Return value must be of type int, null returned
il faut modifier le type de retour de
Trajet::getId()
avec?int
, ce qui acceptera la valeurnull
.
Maintenant que cela marche enfin et que vous vous êtes autocongratulé,
comprenons pourquoi la création d’un nouveau trajet en BDD nécessite un
identifiant null
. La raison est que MySQL génère la valeur auto-incrémentée
d’une colonne (déclarée NOT NULL
) si on lui donne la valeur null
. Pratique !
Actions afficherFormulaireMiseAJour
et mettreAJour
Commençons par rendre générique la méthode de mise à jour des données.
- Déplacez la fonction
mettreAJour($utilisateur)
deUtilisateurRepository
versAbstractRepository
. Changez la signature de la fonction parpublic function mettreAJour(AbstractDataObject $objet): void
- Utilisez
getNomTable()
,getNomClePrimaire()
etgetNomsColonnes()
pour construire la requête SQL demettreAJour()
:UPDATE utilisateur SET nom= :nomTag, prenom= :prenomTag, login= :loginTag WHERE login= :loginTag;
Aide : N’hésitez pas à afficher la requête générée pour vérifier votre code.
-
Utilisez
formatTableauSQL()
dansmettreAJour()
pour obtenir le tableau donné àexecute()
. - Corrigez l’action
mettreAJour
duControleurUtilisateur
pour faire appel aux méthodes deUtilisateurRepository
. L’action doit remarcher.
Passons à la mise à jour de trajet. Grâce à l’exercice précédent, la méthode
mettreAJour
de TrajetRepository
est directement fonctionnelle ! Mais il reste à
gérer les actions de contrôleur et les vues de mise à jour. Démarrons par le
formulaire de mise à jour.
-
Créez la vue
vue/trajet/formulaireMiseAJour.php
en vous basant sur votre formulaire de création des trajets. Modifiez le<input type="hidden">
correspondant à l’action pour transmettre l’action de mise à jour. -
On souhaite que le formulaire de mise à jour des trajets soit prérempli, comme c’est le cas pour celui des utilisateurs. Inspirez-vous de ce dernier.
Notes :
- la valeur d’un
<input type="date">
doit être une date au format “Y-m-d”, - on peut cocher un
<input type="checkbox">
en lui ajoutant l’attributchecked
.
- la valeur d’un
- Rajoutez un
<input type="hidden">
pour transmettre l’id
du trajet. - Créez l’action
afficherFormulaireMiseAJour
dansControleurTrajet
en vous inspirant du MVC utilisateur. - Rajoutez dans
vue/trajet/liste.php
un lien vers le formulaire de mise à jour. - Testez que le lien vous amène bien vers un formulaire de mise à jour de trajet prérempli.
-
Question innocente 😇 : Avez-vous pensé à échapper vos variables dans vos vues pour le HTML et les URL ? Avez-vous testé avec un trajet contenant des caractères spéciaux pour le HTML et les URL ?
Rappel : Les attributs HTML, comme la value d’un
<input>
, doivent être échappés par rapport aux caractères spéciaux du HTML.
Passons à l’action de mise à jour de trajet. Cette action va commencer par créer
un Trajet
à partir des données transmises par le formulaire. Ce code est
identique au début de l’action creerDepuisFormulaire
, donc nous allons
l’isoler dans une méthode pour ne pas le dupliquer.
- PHPStorm permet d’isoler le code dans une méthode automatiquement : surlignez
les lignes complètes de
ControleurTrajet::creerDepuisFormulaire
qui lisent$_GET
et construisent le trajet, puis Clic droit > Refactor > Extract Method. IndiquezconstruireDepuisFormulaire
comme nom de méthode. Modifiez sa signature parprivate static function construireDepuisFormulaire(array $tableauDonneesFormulaire): Trajet
où
$tableauDonneesFormulaire
jouera le rôle de$_GET
. -
Actuellement, le trajet créé par
construireDepuisFormulaire
a un identifiantnull
, ce qui va bien pour la création mais pas pour la mise à jour d’un trajet. Initialisez l’id
du trajet pour qu’il contienne$tableauDonneesFormulaire["id"]
, ounull
si cette case n’existe pas dans le tableau.Astuce : PHP fournit une syntaxe raccourcie pour donner une valeur par défaut si une variable n’existe pas. Pour nos besoins, nous pourrons utiliser
$id = $tableauDonneesFormulaire["id"] ?? null;
-
Créez de la même manière la méthode
construireDepuisFormulaire
dansControleurUtilisateur
. Cette méthode doit être utilisée deux fois : dansmettreAJour
et danscreerDepuisFormulaire
.Remarque : Cette méthode semble peu utile pour les utilisateurs actuellement. Elle prendra toute son importance au TD8 quand la création de l’utilisateur se complexifiera avec un mot de passe, une adresse email qui doit être vérifiée…
- Créez l’action
mettreAJour
dansControleurTrajet
en vous basant sur l’action similaire des utilisateurs. - Créez la vue
vue/trajet/trajetMisAJour.php
similaire àtrajetCree.php
. - Testez la mise à jour d’un trajet à partir du formulaire.
Diagramme de classe final de la partie Repository
Bonus
Contrôleur passager
On voudrait pouvoir inscrire et désinscrire des passagers aux trajets (cf. TD3). La page de détail d’un trajet listerait la liste des passagers avec un lien de désinscription par passager, ainsi qu’un formulaire pour inscrire un passager à partir de son login.
Ces actions seraient traitées par un MVC passager qui aurait deux actions : inscrire et désinscrire.
Le modèle générique fournit directement l’inscription via la méthode ajouter
.
Par contre, il est nécessaire d’adapter la méthode générique supprimer
pour
pouvoir gérer une clé primaire constituée d’un couple.
Les vues passagers affichent un bref message et fournissent un lien pour retourner au détail du trajet modifié.
Autres idées
- Faire en sorte que les formulaires de création et de mise à jour d’un trajet
ne propose que les logins des conducteurs existants, via un champ
<select>
. construireDepuisFormulaire
devrait gérer ses erreurs avec unthrow new Exception("message personnalisé")
. L’action du contrôleur aurait donc untry
/catch
qui appelleraitafficherErreur
en cas d’exception. Les erreurs possibles sont des données manquantes dans$tableauDonneesFormulaire
, ou leconducteurLogin
qui ne correspond pas à un conducteur existant.- Gérer que la méthode générique
ajouter
renvoie l’identifiant auto-généré s’il en existe un (cf. exercice bonus TD3 aveclastInsertId
).