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
-
Copiez/collez dans un nouveau dossier
TD6tous les fichiers du dossierTD5. -
Si aucun paramètre n’est donné dans l’URL, initialisons la variable
actionavec 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 variableactiona été donnée dans l’URL. -
Testez votre site en appelant
controleurFrontal.phpsans 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.phpcontenant 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.phpcréé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 à
afficherVuevers'utilisateur/erreur.php'pour qu’ils utilisentafficherErreuravec 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 :
ModeleUtilisateurest scindé en deux classesUtilisateurRepositoryetUtilisateur.UtilisateurRepositoryetUtilisateuront changé de dossier et denamespacepar rapport àModeleUtilisateur.ajouterest maintenant une méthode statique qui prend unUtilisateuren argument.- La classe
UtilisateurRepositorydé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
ModeleUtilisateurenUtilisateur.
Utilisez le refactoring de PhpStorm : Clic droit sur le nom de la classe > Refactor > Rename. -
Créez deux dossiers
DataObjectetRepositorydansModele. - Créez une classe
UtilisateurRepositorydans le dossierRepositoryavec lenamespacecorrespondant (App\Covoiturage\Modele\Repository). Déplacez les méthodes suivantes deUtilisateurdansUtilisateurRepository:recupererUtilisateursrecupererUtilisateurParLoginajouter: À 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 cetUtilisateurafin 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 objetUtilisateursoit correctement retourné.- éventuellement
recupererTrajetsCommePassager: À transformer en une méthodepublic staticprenant en paramètre un objet de typeUtilisateurdont 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
UtilisateurRepositoryafin qu’elles appellent correctement la méthodeconstruireDepuisTableauSQL. -
Déplacer
Utilisateurdans le dossierDataObjectetConnexionBaseDeDonneesdansRepository.Attention si vous utilisez le drag & drop de PhpStorm, vous allez avoir des mauvaises surprises car les
namespacerisquent 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 lenamespacecorrespondant.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:recupererUtilisateursappartient à la classeUtilisateurRepositorydésormais.
afficherDetail:recupererUtilisateurParLoginappartient à la classeUtilisateurRepository.
creerDepuisFormulaire:ajouteretrecupererUtilisateursappartiennent à la classeUtilisateurRepositorydésormais.ajoutersera maintenant statique et prendra en argument un objet de la classeUtilisateur; les getters deUtilisateurservent à 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
UtilisateurRepositoryune 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.phpqui affiche “L’utilisateur de login$logina 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
supprimerdu 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.phpen 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.phppour 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.phpqui 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 $utilisateurqui servira à remplir le formulaire avec ses attributs. -
L’attribut
valuede la balise<input>permet de préremplir un champ du formulaire. Utilisez l’attribut HTMLreadonlyde<input>pour que l’internaute ne puisse pas changer le login. - Rappel : Vous souhaitez envoyer l’information
action=mettreAJouren plus des informations saisies lors de l’envoi du formulaire. La bonne façon de faire pour un formulaire de méthodeGETest 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.phppeut être raccourcie en utilisant la syntaxe<?= $loginHTML ?>qui est équivalente à
<?php echo $loginHTML; ?>
-
-
Écrivez l’action
afficherFormulaireMiseAJourdu 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’actionafficherFormulaireMiseAJouraffiche bien le formulaire. -
Ajoutons les liens manquants. Enrichissez la vue
liste.phppour 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
mettreAJourqui effectue la mise à jour dans la BDD.Créez la vue
src/vue/utilisateur/utilisateurMisAJour.phppour qu’elle affiche “L’utilisateur de login$logina bien été mis à jour”. Affichez en dessous de ce message la liste des utilisateurs mise à jour (à la manière deutilisateurSupprime.phpetutilisateurCreee.php). -
Ajoutez à
UtilisateurRepositoryune méthode statiquemettreAJour(Utilisateur $utilisateur). Cette méthode est proche deajouter(Utilisateur $utilisateur), à ceci près qu’elle utilise une requête SQLUPDATEet que son type de retour estvoid. En effet, on va considérer qu’une mise à jour se passe toujours correctement. -
Créez l’action
mettreAJourdu 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
construireDepuisTableauSQLmême si cela marcherait actuellement. En effet, la fonctionconstruireDepuisTableauSQLest 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.
Afin d’exécuter l’action récupérée dans le contrôleur correspondant au nom donné dans le query string, on peut procéder ainsi :
<?php
$nomDeClasseControleur::$action();
?>
-
Définissez une variable
controleurdanscontroleurFrontal.phpen récupérant sa valeur à partir de l’URL.Aide : Ce bout de code est similaire à celui concernant
actiondanscontroleurFrontal.php. -
On souhaite créer le nom de la classe à partir de
controleur. Par exemple, quand$controleur="utilisateur", nous souhaitons créer une variable$nomDeClasseControleurqui 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. - Reprenez le code du contrôleur afin de :
- Tester si la classe de nom
$nomDeClasseControleurexiste à l’aide de la fonctionclass_exists, et appeler l’actionafficherErreurdeControleurUtilisateursi ce n’est pas le cas. - Si le contrôleur existe bien, vérifier que l’action visée existe bien dans ce contrôleur et afficher un message d’erreur si ce n’est pas le cas (comme nous le faisions avant).
- Si tout est bon (contrôleur et action existent), appeler l’action
actionde la classe$nomDeClasseControleur.
- Tester si la classe de nom
-
Testez votre code en appelant vos anciennes pages du contrôleur utilisateur avec la bonne URL.
-
Les liens URL de vos différentes vues (
<a href="...">et formulaires…) ne fonctionnent plus. C’est normal, il faut maintenant spécifier dans quel contrôleur se trouve l’action désirée. Mettez donc à jour adéquatement les éléments suivants :- Les liens dans la vue
liste.php(afficher les détails, mettre à jour, supprimer). - Les deux vues contenant un formulaire :
formulaireCreationetformulaireMiseAJour. Ici, il faudra ajouter un champ de typehiddencomme nous l’avons fait précédemment, afin d’indiquer le contrôleur adéquat.
- Les liens dans la vue
- Dans
controleurFrontal.php, faites en sorte de donner la valeurutilisateurpar défaut à la variablecontroleursi aucun contrôleur n’est précisé par l’utilisateur (de manière similaire à ce que nous avons déjà pour$action). Testez en essayant de charger http://localhost/TD6/web/controleurFrontal.php (donc, sans spécifier de nom de contrôleur et d’action). Vous devriez alors arriver sur la page listant les utilisateurs (contrôleur par défaututiliseur, action par défautafficherListe).
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.phpetRepository/TrajetRepository.php(indépendamment de la classeTrajetque vous avez fait dans les TDs 2 & 3) -
À partir de votre classe
Trajetdes 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().
Attention : il faudra probablement importer la classe
DateTime(use DateTime). Aussi, de manière générale, comme nous allons changer beaucoup de choses au fil du TD, certains imports risquent d’être cassés au fur et à mesure. Mais pas de panique, grâce au système d’autoloading mis en place dans le dernier TP et à PHPStorm, il est facile de corriger les imports cassés/manquants. PHPStorm vous signale les classes qui ne sont pas importées par un warning (nom de la classe souligné en jaune). En survolant le nom de la classe manquante, l’IDE vous propose certaines solutions comme notamment ajouter les lignesuse ...nécessaires (Import Class). Vous pouvez aussi utiliser le raccourciAlt+Entrée. - dans
- Corrigeons les appels aux méthodes dans
TrajetRepository.php:Utilisateur::recupererUtilisateurParLogin→UtilisateurRepository::recupererUtilisateurParLoginUtilisateur::construireDepuisTableauSQL→UtilisateurRepository::construireDepuisTableauSQLTrajet::construireDepuisTableauSQL→TrajetRepository::construireDepuisTableauSQL- changez la signature de la fonction
recupererPassagerspourstatic public function recupererPassagers(Trajet $trajet): arrayet corrigez le tableau de valeurs donné à la requête préparée.
$trajet->recupererPassagers()→TrajetRepository::recupererPassagers($trajet)
Si vous aviez codé l’attribut
trajetsCommePassagerdeUtilisateurau TD3 :- Dans
UtilisateurRepository.php:
Trajet::construireDepuisTableauSQL→TrajetRepository::construireDepuisTableauSQL - Dans
Utilisateur.php: importez la classeTrajetdansUtilisateur.php(utilisé au niveau du PHPDoc du getter et du setter de l’attributtrajetsCommePassager).
-
Créez une vue
src/vue/trajet/liste.phpsimilaire à celle des utilisateurs (en commentant les liens pour l’instant).
Idem pourtrajet/erreur.php. -
Créez un contrôleur
controleur/ControleurTrajet.phpsimilaire à 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+Rsous PHPStorm) pour remplacer tous lesutilisateurpartrajet. 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
afficherListedu 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 AbstractRepositoryet faites hériter la classeUtilisateurRepositorydeAbstractRepository. - Pour qu’on puisse migrer la fonction
recupererUtilisateurs()deUtilisateurRepositoryversAbstractRepository, 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()dansAbstractRepositoryprotected 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()deUtilisateurRepositoryversAbstractRepositoryen la renommantrecuperer().Astuce : sur PhpStorm le moyen le plus simple pour déplacer la fonction vers sa classe parente serait Clic droit sur la déclaration de la méthode > Refactor > Pull Members Up. 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 lestaticderecuperer()./** * @return AbstractDataObject[] */ public function recuperer(): array - De même,
AbstractRepositoryva demander à toutes ses classes filles de posséder une méthodeconstruireDepuisTableauSQL($objetFormatTableau).- Ajoutez donc une méthode abstraite dans
AbstractRepositoryprotected abstract function construireDepuisTableauSQL(array $objetFormatTableau) : AbstractDataObject; - Enlevez le
staticde la signature de la fonctionconstruireDepuisTableauSQL()deUtilisateurRepository. - Passez tous les appels à
construireDepuisTableauSQL()deUtilisateurRepositoryen appel de méthode d’instance (dynamique) avecnew 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()deUtilisateurRepositorydéclare bien le type de retourUtilisateur(sous-classe deAbstractDataObject).
- Ajoutez donc une méthode abstraite dans
-
Corrigez l’action
afficherListeduControleurUtilisateurpour faire appel à la méthode d’instancerecuperer()deUtilisateurRepositoryavec :new UtilisateurRepository()->recuperer();L’action
afficherListedu 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) dans toutes les classes qui utilisaient cette méthode.
- Faites de même pour
TrajetRepository:- commentez la méthode
recupererTrajets(), TrajetRepositorydoit hériter deAbstractRepository,construireDepuisTableauSQL()passe depublic staticàprotected. Mettez aussi à jour ses appels.- implémentez
getNomTable(), - l’appel à
UtilisateurRepository::construireDepuisTableauSQL(...)n’est plus statique.
- commentez la méthode
- Corrigez l’action
afficherListeduControleurTrajetpour 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
recupererUtilisateurParLogindans la classeAbstractRepositoryen généralisant la méthode correspondante déjà existante dansUtilisateurRepository:- utilisez PHPStorm sur la fonction
UtilisateurRepository::recupererUtilisateurParLogin, clic droit > Refactor > Pull Members Up. - utilisez PHPStorm sur la fonction
AbstractRepository::recupererUtilisateurParLogin, clic droit > Refactor > Rename > indiquezrecupererParClePrimaire: ceci renommera la méthode ainsi que tous ses appels. - enlevez le
staticde la méthodeAbstractRepository::recupererParClePrimaire.
Corrigez tous les appels à la méthode avec PHPStorm : FaitesCtlr+Maj+Rpour remplacer dans tous les fichiersUtilisateurRepository::recupererParClePrimaireparnew 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 deAbstractRepositoryde fournir une méthodegetNomClePrimaire() : string. - Transformons
recupererParClePrimaireen une méthode générique :- Utilisez
getNomTableetgetNomClePrimairepour rendre la requête générique, construireDepuisTableauSQLdoit ê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→objetetlogin→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
afficherDetailduControleurTrajeten vous basant sur celle deControleurUtilisateur.Rappel : Utilisez le remplacement
Ctrl+Ren préservant la casse pour vous faciliter le travail. -
Créer la vue associée
detail.phpen repartant de l’ancien code deTrajet::toString(). Ajouter les liens vers la vue de détail dansliste.phpen spécifiant biencontroleur=trajetdans le query string.
L’actionafficherDetaildoit 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. Ce qu’il faudrait ici, c’est pouvoir paramétrer la classe
avec un type générique class AbstractRepository <T extends DataObject> et faire en sorte que UtilisateurRepository soit définit ainsi class UtilisateurRepository extends AbstractRepository<Utilisateur> et utiliser T au lieu de DataObject comme type. C’est ce que vous faisiez l’année dernière en Java. Cependant, cela n’est pas possible en PHP… Du moins pour le moment ! L’inclusion du typage générique dans PHP est une demande qui revient régulièrement, donc, il est possible que dans l’avenir PHP évolue pour l’inclure dans une future version…
Action supprimer
Pas de nouveautés.
-
Nous vous laissons migrer la fonction
supprimerParLogin($login)deUtilisateurRepositoryversAbstractRepositoryen 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
supprimerdu contrôleur trajet, ainsi que sa vue associéetrajetSupprime.phpet 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)deUtilisateurRepositoryversAbstractRepository. 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$arrayen insérant$separatorentre 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 $utilisateuren un tableauarray( "loginTag" => $utilisateur->getLogin(), "nomTag" => $utilisateur->getNom(), "prenomTag" => $utilisateur->getPrenom(), );Ajoutez une méthode abstraite
formatTableauSQL()dansAbstractRepositoryprotected abstract function formatTableauSQL(AbstractDataObject $objet): array;Implémentez cette fonction dans
UtilisateurRepositoryavecprotected 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
creerDepuisFormulaireduControleurUtilisateurpour 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
getNomsColonnesdansTrajetRepository. 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
formatTableauSQLdansTrajetRepositoryen 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.phpen vous basant sur votre formulaire de création de trajets du TD3.
Modifiez l’actionde<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
afficherFormulaireCreationdansControleurTrajeten vous inspirant du MVC utilisateur. - Rajoutez dans
vue/trajet/liste.phpun 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
creerDepuisFormulairedansControleurTrajeten vous inspirant du scriptcreerTrajet.phpdu TD3, et de l’action similaire des utilisateurs.Ne traitez pas spécialement les cas d’erreur pour l’instant. Donnez un identifiant
nullau trajet. - Créez la vue
vue/trajet/trajetCree.phpsimilaire à 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 returnedil 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)deUtilisateurRepositoryversAbstractRepository. 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
mettreAJourduControleurUtilisateurpour 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.phpen 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’iddu trajet. - Créez l’action
afficherFormulaireMiseAJourdansControleurTrajeten vous inspirant du MVC utilisateur. - Rajoutez dans
vue/trajet/liste.phpun 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::creerDepuisFormulairequi lisent$_GETet construisent le trajet, puis Clic droit > Refactor > Extract Method. IndiquezconstruireDepuisFormulairecomme nom de méthode. Modifiez sa signature parprivate static function construireDepuisFormulaire(array $tableauDonneesFormulaire): Trajetoù
$tableauDonneesFormulairejouera le rôle de$_GET. -
Actuellement, le trajet créé par
construireDepuisFormulairea un identifiantnull, ce qui va bien pour la création mais pas pour la mise à jour d’un trajet. Initialisez l’iddu trajet pour qu’il contienne$tableauDonneesFormulaire["id"], ounullsi 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
construireDepuisFormulairedansControleurUtilisateur. Cette méthode doit être utilisée deux fois : dansmettreAJouret 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
mettreAJourdansControleurTrajeten vous basant sur l’action similaire des utilisateurs. - Créez la vue
vue/trajet/trajetMisAJour.phpsimilaire à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. Ou alors de définir
une méthode supprimerPassager spécifique au nouveau repository
traitant les passagers…
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>. construireDepuisFormulairedevrait gérer ses erreurs avec unthrow new Exception("message personnalisé"). L’action du contrôleur aurait donc untry/catchqui appelleraitafficherErreuren cas d’exception. Les erreurs possibles sont des données manquantes dans$tableauDonneesFormulaire, ou leconducteurLoginqui ne correspond pas à un conducteur existant.- Gérer que la méthode générique
ajouterrenvoie l’identifiant auto-généré s’il en existe un (cf. exercice bonus TD3 aveclastInsertId).