TP n°7 - Implémentation d’un CRUD sur ToDo
Générateurs de formulaires CRUD avec Symfony

Table des matières

1. Introduction

Cette séance a pour but de comprendre la mise en œuvre des mécanismes qui permettent à une application Web de gérer la soumission de données de la part des utilisateurs, et la persistence de ces données, pour que son comportement dépende des interactions précédentes avec le même utilisateur, ou d’autres utilisateurs accédant aux mêmes données.

Jusqu’ici, nous avons travaillé sur une application dont le comportement en mode Web était basé sur la lecture seule des données, sans possibilité de mettre en œuvre une modification de ces données.

Les seules sources de modification des données étaient, soit une utilisation en ligne de commande, depuis le répertoire dans lequel l’application est installée, soit une modification du contenu de la base de données en tant qu’administrateur ayant accès au back-office Web fonctionnant grâce à EasyAdmin.

Pour mettre en œuvre la soumission des données via les pages Web de l’application, on va notamment s’appuyer sur les formulaires de saisie HTML, et la soumission des données de ces formulaires via les requêtes POST HTTP.

2. Étape 1 : Mise en place de la séance

2.1. TODO Préparation du répertoire de travail

Vous allez dupliquer l’arborescence du projet PHP obtenu à la fin de la séance précédente, pour travailler sur une nouvelle version.

Vous pourrez ainsi faire évoluer le code, et revenir en arrière si besoin, en comparant l’état à la fin de la séance prédente, avec l’état courant.

  1. Effectuez les opérations suivantes pour dupliquer le dossier :

    cd "$HOME/CSC4101"
    cp -r tp-05 tp-07
    cd tp-07
    cd todo-app
    
  2. Ensuite, dans ce nouveau projet, on va réinitialiser les rouages du cadriciel Symfony avec Composer :

    cd "$HOME/CSC4101/tp-07/todo-app"
    rm -fr composer.lock symfony.lock var/cache/ vendor/ .project
    symfony composer install
    

    Confirmez la génération de fichiers pour Docker (on ne s’en servira pas tout de suite, mais pourquoi ne pas les avoir… le rôle de Docker ne sera pas évoqué en cours, mais vous pouvez en parler à vos encadrants de TP).

Cette étape est nécessaire car Symfony s’appuie sur une mécanique sophistiquée de mise en cache du code de l’application (dans var/cache/), pour assurer des performances maximales. Malheureusement, si on duplique un projet, une partie de ce cache a tendance à devenir incohérente (présence de chemins d’accès « en dur », etc.).

Il est donc préférable de réinitialiser le projet radicalement : à un état le plus « propre » possible. On fait ensuite réinstaller par Composer les bibliothèques du cadriciel Symfony (installées dans vendor/), et reconfigurer certains fichiers de configuration associés.

2.2. TODO Chargement du nouveau projet dans l’IDE

Vous allez travailler sur un nouveau projet dans le workspace dans l’IDE Eclipse, importé depuis ce nouveau répertoire de travail.

  1. Pour les utilisateurs de l’IDE Eclipse, supprimez les anciennes infos du projet Eclipse :

    cd "$HOME/CSC4101/tp-07/todo-app"
    rm -fr .project .settings/
    
  2. Importez dans votre IDE cette nouvelle version du projet Symfony Todo sur laquelle vous allez maintenant travailler.

    Dans Eclipse vous pouvez importer ce nouveau projet dans le même workspace que le précédent, mais avec un nom distinctif (par exemple « todo-tp-7 »).

    Si besoin, vous pourrez comparer le code des deux projets « todo-tp-7 » et « todo-tp-5 ». Mais pour l’instant, et pour éviter de vous mélanger, nous vous conseillons de fermer le projet de la séance précédente (menu Project / Close …).

3. Étape 1 : Ajout de fonctions de soumission de données à ToDo

L’objectif de cette séquence est d’enrichir l’application « fil-rouge » de gestion de tâches ToDo pour que l’interface Web de l’application permette d’interagir totalement avec le modèle de données existant, y compris en modification.

Nous allons utiliser des assistants de génération de code de Symfony qui permettent de générer rapidement des contrôleurs complets implémentant des fonctionnalités CRUD.

3.1. Principe des générateurs de code CRUD Symfony

Nous avons déjà vu dans une séance précédente les générateurs :

  • d’entité (make:entity)
  • de contrôleur « basique », avec son template Twig par défaut (make:controller)

Cette fois, nous allons utiliser l’assistant make:crud qui génère, pour une entité du modèle de l’application, un contrôleur, un jeu de formulaires, des templates et méthodes associées à chacune des opérations CRUD suivantes :

  • création d’une nouvelle entité (Create/new)
  • consultation (Read), déclinée en deux sous-ensembles :
    • d’une collection d’entités (index)
    • d’une entité (show)
  • modification d’une entité (Update/edit)
  • suppression d’une entité (Delete)

Nous avons choisi de travailler sur une nouvelle entité pour permettre la génération de fichiers source PHP sans risquer d’entrer en conflit avec le code existant déjà pour la consultation de l’entité Todo (src/Controller/TodoController.php).

Il existe un autre générateur de code relativement similaire, pour le module EasyAdmin, qui génère aussi des fonctionnalités CRUD pour une entité du modèle des données (make:admin:crud). Normalement, vous ne l’utilisez pas dans le cours, dans cette édition. Mais attention à ne pas confondre les deux, au cas où…

3.2. TODO Étape 1-a : Ajout d’une nouvelle entité Paste

L’objectif de cette étape est de générer le code d’une nouvelle entité du Modèle des données Doctrine, qui gère des sauvegardes de « bouts de texte ».

Nous allons travailler sur une nouvelle entité du modèle qu’on va ajouter à l’application ToDo : l’entité Paste (collage).

Elle est censée permettre de garder dans notre application Web une trace de copiés-collés (mémos, astuces, bouts de code, …).

Une paste sera identifiée par :

  • un contenu textuel (content)
  • une date de sauvegarde (created)
  • un type de contenu (content_type), optionnel, permettant éventuellement d’en typer le format avec un type MIME.

Attention : on utilise le type text et non string car les bouts de texte qu’on souhaite sauvegarder sont potentiellement longs.

Procédez aux étapes suivantes :

  1. Utilisez l’assistant générateur de code pour ajouter au modèle de données une nouvelle entité Doctrine :

    symfony console make:entity
    
    Class name of the entity to create or update (e.g. FierceElephant):
    > Paste
    
    created: src/Entity/Paste.php
    created: src/Repository/PasteRepository.php
    
    Entity generated! Now let's add some fields!
    You can always add more fields later manually or by re-running this command.
    
    New property name (press <return> to stop adding fields):
    > content
    
    Field type (enter ? to see all types) [string]:
    > text
    
    Can this field be null in the database (nullable) (yes/no) [no]:
    > 
    
    updated: src/Entity/Paste.php
    
    Add another property? Enter the property name (or press <return> to stop adding fields):
    > created
    
    Field type (enter ? to see all types) [string]:
    > datetime
    
    Can this field be null in the database (nullable) (yes/no) [no]:
    > 
    
    updated: src/Entity/Paste.php
    
    Add another property? Enter the property name (or press <return> to stop adding fields):
    > content_type
    
    Field type (enter ? to see all types) [string]:
    > 
    
    Field length [255]:
    > 
    
    Can this field be null in the database (nullable) (yes/no) [no]:
    > yes
    
    updated: src/Entity/Paste.php
    
    Add another property? Enter the property name (or press <return> to stop adding fields):
    > 
    
    
    
     Success! 
    
    
    Next: When you're ready, create a migration with symfony console make:migration
    
    
  2. Rafraîchissez le projet dans votre IDE, et vérifiez ensuite le code généré dans src/Entity/.

3.3. TODO Étape 1-b : Mise à jour du schéma de la base de données

L’objectif de cette étape est de découvrir l’outil de migrations de bases de données de Symfony

Les migrations Doctrine sont des scripts qui permettent de gérer les changement du modèle de données à appliquer au SGBD. Nous ne les avons pas étudiés jusqu’ici, et ils concernent principalement la gestion des applications en production.

Jusqu’à présent, dans les séances de TP, à chaque évolution du modèle de données, on utilisait la séquence suivante pour aligner le contenu de la base de données avec le nouveau modèle de données décrit dans les classes Doctrine :

  1. suppression de la base de données : symfony console doctrine:database:drop --force
  2. recréation de la base de données : symfony console doctrine:database:create
  3. recréation du schéma de la base de données : symfony console doctrine:schema:create
  4. rechargement des données de tests : symfony console doctrine:fixtures:load -n

Cette séquence est appropriée en développement : on recrée la base de donnée en repartant de zéro, et on teste l’ajout de données via les Fixtures. Mais il n’est pas question d’en faire autant quand on a une base de données en production, si l’on souhaite redéployer une nouvelle version de l’application.

Dans ce cas, on devra disposer d’une procédure de migration de la base de données, qui fera la mise à jour dans la base de données de production, sans rien supprimer (espérons).

C’est justement ce que permet de faire le module Migrations de Symfony, dont il est question dans le message de fin affiché par make:entity : « Next: When you’re ready, create a migration with symfony console make:migration ».

Essayons maintenant :

  1. Générez un script de migration correspondant à la dernière évolution du schéma de la base de données, avec la commande suivante :

    symfony console make:migration
    
    created: migrations/Version20240828105223.php
    
    
     Success! 
    
    
    Review the new migration then run it with symfony console doctrine:migrations:migrate
    See https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html
    
    
  2. Observez dans votre IDE le script PHP qui vient d’être généré (ici migrations/Version20240828105223.php).

    Vous remarquerez qu’il revient ici à appliquer un CREATE TABLE pour la nouvelle entité ajoutée.

  3. On peut donc appliquer cette migration à notre base de données. Il suffit de suivre les indications données dans ce message : « Run the migration with symfony console doctrine:migrations:migrate ».

    Donc lancez la commande suivante, et validez l’application de la migration :

    symfony console doctrine:migrations:migrate
    
    WARNING! You are about to execute a migration in database "main" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]:
    
  4. Vous pouvez maintenant lancer l’application et re-tester. Elle devrait toujours fonctionner comme précédemment pour la consultation des tâches de la base de données.

Vous savez désormais comment appliquer des migrations à une base de données en production.

Mais tant que vous êtes en phase de développement de votre projet, le plus simple est peut-être de continuer à faire des database:create suivis de schema:create.

3.4. TODO Étape 1-c : Ajout d’un controleur CRUD pour les Pastes

L’objectif de cette étape est d’ajouter un contrôleur permettant de disposer de pages Web CRUD dans l’application, pour ajouter (Create), consulter (Retrieve) modifier (Update), ou supprimer (Delete), des instances de notre nouvelle entité Paste.

Procédez aux étapes suivantes :

  1. Ajoutez un nouveau contrôleur CRUD PasteController, pour la classe Paste :

    symfony console make:crud
    
    
    The class name of the entity to create CRUD (e.g. BravePuppy):
    > Paste
    
    Choose a name for your controller class (e.g. PasteController) [PasteController]:
    > 
    
    Do you want to generate PHPUnit tests? [Experimental] (yes/no) [no]:
    > 
    
    created: src/Controller/PasteController.php
    created: src/Form/PasteType.php
    created: templates/paste/_delete_form.html.twig
    created: templates/paste/_form.html.twig
    created: templates/paste/edit.html.twig
    created: templates/paste/index.html.twig
    created: templates/paste/new.html.twig
    created: templates/paste/show.html.twig
    
    
     Success! 
    
    
    Next: Check your new CRUD by going to /paste/
    
  2. Rafraîchissez le contenu du projet dans votre IDE, et consultez les fichiers générés.

    Vous pouvez voir que l’assistant a non-seulement généré le code PHP de la classe du contrôleur, dans src/Controller, mais aussi tous les éléments associés, comme des gabarits Twig nécessaires.

    Le code des méthodes index() et show() générées dans PasteController devrait vous sembler familier, très similaire à celui que vous avez ajouté dans une séquence précédente dans TaskController.

    On va voir le reste des méthodes ci-dessous.

Notez que ce générateur de contrôleur Symfony est différent de celui qu’on avait étudié dans une séquence précédente.

Vous disposez maintenant de deux générateurs de contrôleurs :

  • make:controller qui génère un contrôleur de base, presque vide (déjà utilisé précédamment pour l’ajout de TaskController)
  • make:crud celui-ci, qui génère le code d’un contrôleur sophistiqué intégrant les fonctionnalités CRUD

À vous de choisir selon les besoins : gérer un CRUD sur une éntité du modèle de données, ou tout écrire à la main dans d’autres cas…

3.5. TODO Étape 1-d : Tests de l’application modifiée

Testez maintenant l’application qui permet maintenant une soumission de données :

  1. Consultez la liste des nouvelles routes, avec symfony console debug:router
  2. Lancez l’application en mode Web comme d’habitude
  3. Testez l’application dans le navigateur derrière les chemins des pastes : http://localhost:8000/paste/.

    Vérifiez que la création fonctionne, et que vous pouvez maintenant ajouter, modifier ou supprimer des données via les fonctions accessibles par les nouvelles routes CRUD.

    Les boutons « Save » de validation des formulaires sont assez peu visibles pour l’instant, mais bien actifs.

    On corrigera cela plus tard.

    Il se peut que vous constatiez des problèmes de fonctionnement du contrôleur CRUD généré ci-dessus, avec une route inconnue pour le chemin /paste/. Il est alors nécessaire de corriger cela : il faut probablement à ce stade nettoyer à nouveau le cache de Symfony.

    symfony console cache:clear
    

    Si jamais ça ne suffisait pas (un bug Symfony semble empêcher parfois de faire fonctionner les nouvelles routes, malheureusement…), vous pouvez réinitialiser le projet Symfony un peu plus en profondeur :

    rm -fr var composer.lock symfony.lock vendor
    symfony composer install
    
  4. Vérifiez bien que le formulaire d’ajout de nouvelle paste fonctionne enfin, en vérifiant l’impact sur la base de données : les pastes ajoutées sont bien retrouvées dans la liste, et ainsi de suite.

Excellent. L’assistant make:crud a généré des formulaires opérationnels, bien intégrés avec Doctrine. Cool.

3.6. TODO Étape 1-e : Compréhension des requêtes mises en jeu dans un formulaire de création

Dans le fonctionnement des formulaires de création d’entités pastes, la séquence d’interactions HTTP, est la suivante :

  1. chargement du formulaire HTML de création (1ère requête : GET sur /paste/new)
  2. soumission de données à la validation du formulaire (2ème requête : POST, encore sur /paste/new)
  3. une fois la soumission traitée par l’application, redirection (automatique opérée par le navigateur) vers la liste des pastes (nouvelle requête GET, cette fois vers /paste/)

Vérifiez que c’est bien le cas :

  1. dans le navigateur à l’aide du Moniteur réseau dans les outils du développeur Web, observez les requêtes pendant la création. La colonne « Méthode » indique s’il s’agit d’un GET ou d’un POST :

    capture-3-requests.png

    Figure 1 : Trace des requêtes lors de la création dans le navigateur

    Observez en particulier les en-têtes transmis dans la réponse à la requête POST : en complément du code de réponse « 303 », le serveur envoie la cible de la redirection, dans l’en-tête Location.

  2. Vérifiez aussi ce qui apparaît dans les logs du serveur HTTP (dans le terminal), où on voit passer ces 3 requêtes et les codes de réponse renvoyés (303 pour la redirection) :

    [PHP        ] ... [200]: GET /paste/new
    ...
    [PHP        ] ... [303]: POST /paste/new
    ...
    [PHP        ] ... [200]: GET /paste/
    
  3. Observez également les caractéristiques du traitement de la requête POST via la barre d’outils Symfony (le Symfony profiler).

    Dans le coin inférieur droit de la page un popup affiche la dernière requête/réponse correspondant à l’état courant de la page :

    popup-profiler-redirect.png

    Figure 2 : Popup du profiler pour la page courante

    Il nous indique les éléments suivants :

    • on est bien en train de consulter la page de liste des pastes (affichée par PasteController::index), avec le code de réponse 200.
    • mais il nous indique aussi que c’est suite à un Redirect from consécutif au POST sur app_paste_new. À côté, il fournit un lien « (769960) » qui permet de cliquer pour accéder à l’outil des requêtes/réponses du profiler, directement sur cette requête POST précédente.

    Cela signifie qu’on a affiché cette page de liste des pastes (envoyée avec succès : 200), car le navigateur nous y a redirigé.

    La redirection a été provoquée car il avait reçu, juste avant, un code de réponse 303 en réponse à sa requête POST (qui avait été faite sur la route app_paste_new). Elle avait été traîtée par l’application Symfony qui lui a associé en interne, le jeton de requête (Token) « 769960 », dans cet exemple.

  4. Dans l’outil de requêtes / réponses du Profiler Symfony, on retrouve les mêmes infos (plus détaillées) concernant cette dernière requête post-redirection :

    profiler-requests.png

    Figure 3 : Affichage de la requête dans le Profiler

    Remarquez que l’historique des requêtes précédamment traitées par Symfony est accessible derrière le lien « Last 10 ». On y retrouve nos 3 dernières requêtes de gestion du formulaire de création :

    last-ten.png

    Figure 4 : Dix dernières requêtes traitées par l’application

  5. Cliquez sur ce lien de jeton de requête POST (ici « 769960 »).

    Vous voyez apparaître toutes les infos traîtées par Symfony concernant la requête POST de soumission des données, de façon similaire à ce que vous connaissez déjà pour les requêtes GET :

    • nom de la méthode du contrôleur invoquée : PasteController::new
    • données du POST :
      • content
      • created
    • etc.

    post-details.png

    Figure 5 : Détails de la requête de soumission de données

  6. Cliquez alors sur l’outil « Doctrine »

    Vous voyez alors, à la fin, la requête SQL qui a été générée :

    1. START TRANSACTION
    2. INSERT INTO paste (content, created, content_type) VALUES (?, ?, ?) ...
    3. COMMIT

    En cliquant sur « View runnable query«  on voit une version plus compréhensible de la requête SQL : INSERT INTO paste (content, created, content_type) VALUES ('Essai', '2024-08-09 10:10:00', NULL);

  7. Cliquez sur l’outil « Forms » : vous voyez des détails sur les données qui ont été soumises depuis le formulaire HTML via la méthode POST, et la façon dont les champs du formulaire sont interfacés avec le modèle objet de l’entité Doctrine Paste.

    form-details.png

    Figure 6 : Contenu du formulaire de données soumises, associées au modèle de données

Grâce à cet outil Profiler de Symfony dans l’environnement de développement, vous pouvez ainsi, à tout moment, aller approfondir les détails sur tout l’historique des requêtes précédentes de l’application, pour déboguer, ou mieux comprendre le fonctionnement de l’application.

3.7. TODO Étape 1-f : Compréhension sommaire de l’algorithme de gestion du formulaire

Le générateur make:crud a créé pour nous des méthodes qui gèrent les requêtes GET et POST et le fonctionnement des formulaires.

Examinez dans votre IDE le code du contrôleur src/Controller/PasteController.php qui a été généré par l’assistant, qui correspond à ce qui a été présenté en cours magistral (une version un peu plus sophistiquée, certes).

  1. Regardez le code des méthodes index() et show(), et en parallèle les gabarits Twig templates/paste/index.html.twig et templates/paste/show.html.twig.

    Vous ne devriez pas être trop dépaysés, par rapport à ce qu’on a utilisé jusqu’à présent dans TodoController.php.

  2. Consultez maintenant le code de la méthode PasteController::new(), qui gère le formulaire de création de nouvelles Pastes. Il correspond à celui qu’on a présenté dans le cours.
  3. Ajoutez des éléments de traces avec la fonction dump() pour tester et comprendre les différents éléments de ce code, aux différentes étapes de l’algorithme de traitement des soumissions : les données soumises, les entités du modèle. Vous devez retrouver parmi ces éléments ce que vous avez vu précédemment dans les outils du profiler Symfony

À ce stade, l’application fonctionne de façon interactive, et sauvegarde les opérations CRUD sur les Pastes, en base de données.

Le code généré est modifiable, si besoin, par exemple pour ajouter des opérations pour la gestion de règles métier.

Il n’est pas indispensable de comprendre, à ce stade, l’ensemble complet des opérations effectuées dans ce code, notamment pour la modification et la suppression, car les générateurs réalisent une version assez optimisée, et qu’on pourrait écrire de façon un peu différente, et peut-être moins compacte, donc plus facilement compréhensible.

Nous détaillerons le fonctionnement complet des formulaires dans une séance ultérieure du cours.

3.8. Étape 1-g : Modification du look dans les formulaires générés

Les gabarits générés n’utilisent probablement pas votre structure de gabarits et de blocs. Il va falloir faire quelques ajustements pour récupérer la mise en page standard de l’application.

3.8.1. TODO Utilisation de votre hiérarchie de gabarits

Vérifiez dans l’outil Twig du profiler Symfony, comment sont compilés les gabarits des pages relatives aux Pastes.

Si vous examinez attentivement la construction du gabarit templates/paste/index.html.twig, vous constatez qu’il surcharge en fait le bloc Twig body. C’est le comportement par défaut du générateur de code make:crud de Symfony.

A priori, ça fonctionne avec notre structure définie dans le gabarit de base base.html.twig… mais on a vu à la séquence précédente qu’on avait un léger souci.

On a en effet fait évoluer le gabarit de base : on a fait le choix d’inclure une section main au sein du bloc body (pour l’ajout de BootStrap). À charge pour chaque page de surcharger ce block main (vérifiez votre todo.html.twig).

Il faut donc qu’on s’aligne, et qu’on surcharge également le bloc interne main.

Modifiez en conséquence les gabarits des Pastes générés par make:crud, afin de surcharger plutôt ce bloc main, et utiliser ainsi la bonne mise en forme.

3.8.2. TODO Utilisation du style de formulaire Bootstrap

Le détail de l’intérieur des formulaires n’est pas spécialement élégant, bien qu’on utilise BootStrap. Corrigeons cela :

  1. Configurez le thème des formulaires Twig comme étant Bootstrap 5 : modifiez le fichier config/packages/twig.yaml pour ajouter l’entrée form_themes (attention à l’indentation du format YAML) :

    twig:
        ...
        form_themes: ['bootstrap_5_layout.html.twig']
    
  2. Modifiez, si besoin, le gabarit templates/paste/new.html.twig si ce n’était pas déjà fait, pour y surcharger le bloc main plutôt que le bloc body (cf. plus haut).
  3. Rechargez la page de création de nouvelle paste, et vérifiez que les champs de formulaire ont maintenant un look différent.

3.8.3. TODO Modification du look des boutons

L’assistant générateurs de code make:crud a généré des boutons de validation de formulaires qui ne s’affichent pas de façon optimale par défaut (comme « Save »), quand on utilise Bootstrap.

Vous pouvez donc modifier le code des gabarits générés pour que les boutons ressemblent à ceux qu’attend Bootstrap, en vous inspirant des exemples fournis dans la documentation.

  1. Chargez le code du gabarit new.html.twig, dans l’IDE. Remarquez qu’il inclut un autre gabarit, dans lequel est situé le code du formulaire proprement dit :

    {{ include('paste/_form.html.twig') }}
    

    Attention à la syntaxe : il s’agit là d’inclure le code d’un autre gabarit, et non de surcharger les valeurs d’un autre gabarit, comme pour {% extends 'base.html.twig' %}

  2. Modifiez le code du formulaire _form.html.twig pour changer l’apparence du bouton de validation du formulaire, par exemple avec :

    <button class="btn btn-primary">...</button>
    
  3. Vérifiez que le formulaire dans la page de création est bien impacté, mais aussi celui dans la page de modification, qui inclut elle aussi le même sous-formulaire _form.html.twig.

    En effet, créer ou modifier des données nécessite à peu près les mêmes champs, dans la plupart des applications simples. C’est donc le comportement choisi par les concepteurs du générateur de CRUD : les deux gabarits de la création et la modification incluent ce même sous-gabarit de fomulaire.

  4. Observez l’intitulé du bouton, qui est tantôt « Save », tantôt « Update ».

    Cela correspond en principe à la valeur qui est présente dans la balise <button>. Ici, on trouve, dans le gabarit :

    <button class="btn btn-primary">{{ button_label|default('Save') }}</button>
    

    Cela signifie que si une variable button_label est fournie au gabarit, alors on s’en sert, et sinon, on prend comme valeur par défaut 'Save'.

  5. Vérifiez le code dans edit.html.twig. Remarquez la définition de button_label, à la volée, lors de l’inclusion du sous-formulaire _form.html.twig.

    Vous voyez encore une fois qu’à travers Twig on dispose d’un mécanisme puissant pour factoriser les instructions et éviter de dupliquer le code, tout en permettant des particularisations.

Nous avons fait le tour du fonctionnement des mécanismes CRUD et de la structure des gabarits associés, mais on n’a pas trop examiné le code PHP du traitement des requêtes dans l’application.

Examinons maintenant plus en détails ce que font les méthodes CRUD de la classe Controller.

4. Étape 2 : Ajout d’une gestion CRUD pour les tâches

L’objectif de cette séquence est de comprendre le fonctionnement des fonctions de gestion des formulaires dans les contrôleurs CRUD.

On souhaite mettre en œuvre le reste des fonctions CRUD (Create Retrieve Update Delete) pour les tâches dans l’application.

La consultation (Read / Retrieve) pour l’entité Todo du modèle applicatif fonctionne déjà, et on s’attaque maintenant à l’ajout des opérations en modification.

Pour mettre en œuvre la soumissions de données via les formulaires, pour la création ou la modification, il y a un certain nombre d’éléments à ajouter dans le code, à la fois au niveau des gabarits Twig que du code PHP. Il faut correctement calibrer les interactions entre les différentes couches de l’application : requêtes HTTP GET et POST, détails de HTML, subtilités du code PHP ou Twig…

On ne peut malheureusement pas se servir cette fois du générateur make:crud, car notre contrôleur TodoController existe déjà. Ce générateur ne sait pas modifier du code de classe controleur existante.

On va devoir coder « à la main » en s’inspirerant du code de gestion des formulaire obtenu ci-dessus à l’aide du maker pour les Paste.

Les opérations nécessaires pour mener à bien cette étape ne sont pas décrites in extenso dans le présent support.

Il y a besoin d’être assez méticuleux sur le nommage, donc soyez attentifs aux opérations que vous effectuez.

4.1. TODO Étape 2-a : Ajout de la création des tâches

On commence par la première fonction du CRUD : la création. On va tout simplement recopier les éléments correspondants générés par le maker pour les Pastes, dans le code gérant les Todo.

Procédez aux étapes suivantes, tout en testant au fur et à mesure vos modifications dans l’application :

  1. Ajout d’une classe gestionnaire de formulaire / « type de données », TodoType définie dans le fichier src/Form/TodoType.php :
    • recopiez, dans votre IDE, le fichier PasteType.php et collez-le en tant que TodoType.php

      Notez qu’Eclipse propose de renommer le nom de la classe PasteType en TodoType pour l’aligner sur le nom du fichier. Pratique !

    • dans TodoType.php, modifiez le code pour renommer toutes les occurrences de la chaîne « Paste » en « Todo » (resp. « paste » en « todo ») : utilisez pour cela l’outil syntaxique « Edit / Find/replace »
    • modifiez la liste des champs ajoutés dans le constructeur de formulaire dans la méthode TodoType::buildForm, pour refléter les attributs de Todo :

      • title ;
      • created ;
      • updated ;

      Le reste des attributs pourra être vu plus tard.

  2. Ajout d’une méthode new() dans TodoController :
    • copiez-collez le code de la méthode PasteController::new() pour l’ajouter dans TodoController.

      Dans certains contrôleurs, comme l’a fait le maker dans PasteController, il faut placer cette méthode new() avant la méthode show() dans le fichier source.

      Cela permet d’éviter une confusion dans le routage, pour ne pas essayer de charger la tâche d’identifiant « new » si on emprunte la route /todo/new, si la route « _show » a simplement comme motif de chemin '/{id}'.

      Dans notre cas, on avait déjà une définition du routage de TodoController::show(), qui spécifie bien le type de l’argument id attendu, donc pas de problème de confusion :

      #[Route('/{id}', name: 'todo_show', requirements: ['id' => '\d+'], methods: ['GET'])]
      public function show(Todo $todo): Response
      {
          // ...
      

      Explication : requirements: ['id' => '\d+'] permet de ne faire correspondre la route que si id contient un entier (« \d+ » est l’expression rationnelle pour signifier « succession de décimaux »).

    • modifiez toutes les occurrences de « Paste » en « Todo » (resp. « paste » en « todo »).

      Notez qu’Eclipse possède une fonction utile pour renommer une variable dans ses différentes occurrences dans le corps d’une méthode : positionnez le curseur dans la première chaîne $paste et sélectionnez le menu du bouton droit « Refactor / Rename ». Au fur et à mesure de la saisie du nouveau nom, vous voyez les autres occurrences de la variable être renommées simultanément. Cet outil est capable de reconnaître la sémantique du langage PHP pour agir au bons endroits, là où un outil syntaxique risquerait de tout casser.

      Certains autres IDE proposent une fonction similaire.

      Pensez à renommer les noms de gabarits ou de routes, qui ne sont que des chaînes de caractère, et pas des variables. Ils ne sont donc pas concernées par l’outil de renommage sémantique, qui ne s’applique qu’aux noms de classes, d’attributs ou de méthodes.

    • ajoutez les use manquants éventuels pour les classes inconnues jusqu’alors dans ce fichier (Request de Symfony\Component\HttpFoundation, EntityManagerInterface, …)
  3. Ajout des gabarits du formulaire de création de nouvelle tâche :
    • recopiez les deux gabarits new.html.twig et _form.html.twig depuis templates/paste/ vers templates/todo/
    • renommez, dans templates/todo/new.html.twig toutes les occurrences de « Paste » en « Todo » (resp. « paste » en « todo »)
  4. Testez que le formulaire de création de nouvelle tâche fonctionne.

    Résolvez les éventuelles erreurs liées aux attributs manquants signalées par Doctrine.

  5. Ajoutez dans la page de liste des tâches (dans todo/index.html.twig) le lien de création, comme ça a été fait pour les Pastes

Vous constatez que pour obtenir un ensemble de pages parfait, il y a beaucoup de travail à faire sur ce genre de petits détails.

Ici, il ne s’agit pas d’obtenir une application Symfony « parfaite », car vous n’êtes probablement pas destinés à devenir des développeurs professionnels… la perfection peut attendre un peu !

Vous venez de faire un grand nombre d’opérations de mise au point un peu fastidieuses, pour reprendre une des opérations de gestion des formulaires, pour ajouter la « création » d’entités à un contrôleur existant.

Mais il reste encore la modification, et la suppression (voir plus loin).

Nous sommes partis du code tel que généré par make:crud pour les pastes pour faciliter le copier/coller. Le travail aurait été à peu près aussi difficile, s’il s’était agit de recopier les exemples de la documentation Symfony (par exemple depuis Forms).

Nous vous avons invité à faire toutes ces opérations, certes fastidieuses, pour qu’elles vous donnent l’occasion de tester en détail le fonctionnement des formulaires et de la mécanique sous-jacente, à l’aide des outils de mise-au-point présentés plus haut.

Maintenant que vous savez le faire, et que vous voyez l’effort que ça représente, vous mesurez probablement encore plus l’utilité d’un maker comme make:crud.

Il resterait maintenant à ajouter le code des méthodes restantes du CRUD : modification (edit()) et suppression (delete()) du contrôleur (voir la section optionnelle ci-dessous).

À l’avenir (dans votre projet), dans un tel cas, il vaudra probablement mieux supprimer l’ancienne classe contrôleur et les anciens gabarits, et générer à la place une nouvelle classe avec le maker make:crud.

4.2. Ajout des méthodes CRUD restantes dans TodoController (optionnel)

Vous pouvez éventuellement sauter cette étape, pour ne pas perdre trop de temps. Vous pourrez y revenir ultérieurement si besoin.

Il rester à ajouter les fonctionnalités CRUD restantes des Todo (modification et suppression) :

Procédez comme ci-dessus, en recopiant le reste des méthodes et gabarits, depuis Paste :

  1. la méthode de modification d’une tâche, son gabarit et les liens d’édition des tâches dans la liste des tâches (à côté du lien de consultation);
  2. la méthode de suppression d’une tâche ainsi que son gabarit, qui sont nécessaires au fonctionnement de l’édition.
  3. les liens de modification et de suppression dans le formulaire de consultation d’une tâche

4.3. Customisation des widgets de dates (optionnel)

Il se peut que vous ayez ajouté les champs de sasie de dates dans TodoType avec du code de ce type :

use Symfony\Component\Form\Extension\Core\Type\DateType;

//...

  public function buildForm(FormBuilderInterface $builder, array $options): void
  {
    $builder
        // ...
          ->add('created')
        // ...
        ;
    // ...

L’affichage par défaut n’est pas aussi utilisable que celui des dates de création des pastes :

field-date-before.png

Figure 7 : Affichage d’un champ DateTime en mise en forme par défaut

En effet, on n’a pas spécifié la façon dont le widget doit afficher le champ.

Pour afficher les champs de saisie des dates, plutôt que d’afficher le détail de chacun des sous-formulaires des sous-éléments constituant une date de la classe DateTime PHP, on peut afficher un élément de formulaire HTML5 et laisser le navigateur faire le job.

Vous pouvez ainsi modifier le « widget » de la façon suivante (inspirée des indications de la documentation Symfony), qui se trouve être la même que dans ce que le maker a généré pour les Pastes :

use Symfony\Component\Form\Extension\Core\Type\DateType;

//...

  public function buildForm(FormBuilderInterface $builder, array $options): void
  {
    $builder
        // ...
        ->add('created', DateType::class, [
              'widget' => 'single_text'])
        ;
    // ...

Le résultat est tout de suite mieux :

field-date-after.png

Figure 8 : Affichage d’un champ DateTime en mise en forme avec un Date picker

Il y a plein de détails, comme ça, sur la façon de construire les différents composants des pages. Malheureusement, il n’est pas possible d’aborder tout cela dans le cours, faute de temps.

Si vous avez vu ça dans un site, il y a probablement une façon de le faire… plus ou moins bien documentée…

5. Ajout de règles de gestion (optionnel)

L’objectif de cette dernière séquence optionnelle est d’approfondir la compréhension du fonctionnement des algorithmes de gestion des formulaires, en ajoutant des règles de gestion par rapport à la cohérence des données.

Cette section suppose d’avoir terminé la section optionnelle « Ajout des méthodes CRUD restantes dans TodoController » ci-dessus.

Le fonctionnement des méthodes CRUD, tel qu’il est généré par les assistants générateurs de code est assez basique. Il permet de faire des créations, modifications ou suppressions « brutes », sans gestion particulière.

Jusqu’ici, il s’agit principalement de fournir une interface Web pour interagir avec la base de données sous-jacente.

Or dans les vraies applications, en général, tous les attributs stockés dans la base de données ne sont pas modifiables par tous les utilisateurs. On doit ainsi respecter des règles de gestion applicatives, qui répondent à des besoins « métiers ».

Il s’agit de travailler sur le code de pages dont le contenu est customisable à loisir, et où le comportement des formulaires peut être relativement complexe.

Il faudra donc coder ces algorithmes précisément, notamment dans les contrôleurs Symfony, et mettre ainsi en place le fameux HATEOS vu en cours.

Nous allons voir comment coder ce genre de règles dans le code PHP de notre application.

5.1. Règles de gestion de la gestion des tâches

Dans notre application fil-rouge, on souhaite ainsi que la gestion des tâches permise par les fonctions de modification des données obéisse à certaines règles :

  • les dates de dernière modification des tâches (todo:updated) sont non modifiables par l’utilisateur dans les formulaires, et mises à jour automatiquement lorsqu’une modification est effectuée sur la tâche (création ou modification)
  • une tâche nouvellement créée :
    • ne peut être créée comme déjà terminée. Il faudra la modifier ultérieurement pour changer son statut (todo:completed)
    • sa date de création (todo:created) est définie automatiquement lors de son ajout dans la base de données, et ne sera plus modifiable ultérieurement

5.2. Mise en œuvre

Modifiez le code pour mettre en œuvre ces règles. Il s’agit de modifier deux élements :

  1. le gestionnaire de formulaire TodoType (dans src/Form/TodoType.php).

    Il est utilisé aussi bien pour le formulaire de création d’une nouvelle tâche que pour une tâche modifiée.

    Vous allez ajouter une option à son comportement de façon à ce qu’il affiche ou non certains champs selon que la tâche est nouvelle, ou modifiée.

    1. Ajoutez-lui une option, via la méthode configureOptions(), nommée task_is_new de type booléen, dont la valeur par défaut est fausse :

      public function configureOptions(OptionsResolver $resolver)
      {
        $resolver->setDefaults([
                'data_class' => Todo::class,
                'task_is_new' => false
        ]);
        $resolver->setAllowedTypes('task_is_new', 'bool');
      }
      
    2. Modifiez le comportement de la création du formulaire, dans buildForm() pour ajouter ou non certains champs, selon qu’ils doivent être inclus dans le formulaire, en fonction de la valeur de task_is_new :

      public function buildForm(FormBuilderInterface $builder, array $options)
      {
        // ...
        $builder->add(...);
      
        if(! $options['task_is_new'] ) 
        {
                $builder->add(...);
        }
        // ...
      
  2. le contrôleur TodoController :
    1. Modifiez le code dans la méthode new(), qui appelle la génération du formulaire, pour passer en argument un tableau d’options, qui contient la valeur de l’option task_is_new, qui doit alors être vraie :

      public function new(Request $request): Response
      {
        $todo = new Todo();
        ...
      
        $form = $this->createForm(TodoType::class, $todo,
                ['task_is_new' => true]
                );
      

      Comme le code de edit() n’est pas changé, les champs présents dans le formulaire de création et de modification sont donc gérés différemment, en fonction de la valeur par défaut de l’option task_is_new.

      Vérifiez que la création fonctionne toujours, et si besoin, définissez une valeur par défaut d’un des attributs.

      Testez l’application ainsi modifiée, et vérifiez que les formulaires de création d’une nouvelle tâche, et de modification d’une tâche existante contiennent des champs différents.

    2. Modifiez le code de gestion des formulaires soumis, pour gérer les attributs dont la valeur doit être calculée. Inspirez-vous de cet example :

      public function edit(Request $request, Todo $todo, EntityManagerInterface $entityManager): Responsepublic function edit(Request $request, Todo $todo): Response
      {
        $form = $this->createForm(TodoType::class, $todo);
        $form->handleRequest($request);
      
        if ($form->isSubmitted() && $form->isValid()) {
      
                // Ici la liste des valeurs calculées, juste avant la sauvegarde
                // dans la base de données
                $todo->setUpdated(new \DateTime());
      
                $entityManager->flush();
      
                        return $this->redirectToRoute('app_todo_index', [], Response::HTTP_SEE_OTHER);
        }
        ...
      

Testez l’application ainsi modifiée, et vérifiez que les dates sont mises à jour correctement dans la base de données.

Vous voyez, à travers la mise en œuvre d’une simple règle de gestion, comment la classe gestionnaire de formulaire TodoType prend en charge une partie du comportement. Encore un élément de l’ensemble des outils de Symfony qui permet d’organiser le code, en objet, en « améliorant » (relativement) la maintenabilité du code, pour des applications réelles.

6. Évaluation

Vous avez expérimenté avec les éléments suivants :

  • utiliser le générateur de Contrôleurs CRUD Symfony make:crud pour mettre en place du code gérant les formulaires
  • observer le fonctionnement de la gestion des soumissions de données dans HTTP, avec les 3 requêtes et la redirection
  • utiliser différents outils du Profiler Symfony pour la mise au point
  • comprendre le rôle des méthodes standard qui mettent en œuvre les fonctions CRUD dans les classes contrôleur : new() (pour « C »), index() et show() (pour « R »), edit() (« U ») et enfin delete() (« D »)
  • comprendre le principe de factorisation d’éléments communs à des formulaires, dans des sous-gabarits Twig
  • identifier le rôle des classes gestionnaire de formulaires comme TodoType qui font le lien avec les attributs des entités Doctrine du modèle de données

7. Ensuite

D’ici la fin de la séance, et d’ici le prochain cours magistral, vous allez travailler en autonomie pour préparer les phases suivantes du projet, en vous appuyant sur le support de travail en autonomie 7-8.

Author: Olivier Berger (TSP)

Date: 2024-09-06 Fri 13:21

Emacs (Org mode)