Jérôme Cutrona

Version statique de l'intranet de Jérôme Cutrona - Rejoindre la version dynamique 🔒

Les exemples et corrections sont potentiellement non fonctionnels.

B.U.T. Informatique - IUT de Reims - Université de Reims

Symfony 6.3

Navigation

Objectifs de la séance

  • Créer un projet Symfony avec l'outil « symfony » en ligne de commande (« Symfony CLI »)
  • Prendre en main Symfony dans sa version 5
  • Utiliser le routage
  • Développer des « templates » Twig
  • Écrire des tests fonctionnels
  • Utiliser l'ORM Doctrine
  • Gérer des relations entre entités
  • Générer des données factices (« fixtures ») avec « Foundry »
  • Utiliser le composant Form
  • Utiliser le composant Security
  • Mettre en place un « back-office » avec EasyAdmin

Préambule

L'objectif de ce TP est de vous familiariser avec le développement d'applications Symfony. Le sujet vous accompagne dans la découverte du framework tout en essayant de se conformer aux bonnes pratiques de Symfony.

Remarque importante

De nombreux points du sujet consistent en l'observation de fichiers ou de fonctionnements qui s'appuient sur des conventions liées au framework. Ne les négligez pas ! Le but n'est pas de vous faire suivre bêtement un tutoriel mais de vous amener à comprendre ce que vous faites.

Utilisation du moteur PHP local

Vous allez utiliser le moteur PHP installé localement sur votre poste de travail. Vous déclencherez le moteur PHP local, directement ou indirectement de diverses manières :

  • à l'aide de la commande php pour exécuter du code
    php -r "code PHP"
  • à l'aide de la commande php pour exécuter un programme
    php -f un_script_PHP
    ou
    php un_script_PHP
  • comme une commande grâce à l'utilisation du shebang en tête d'un programme PHP
    bin/console cache:clear

    Le programme « console » est fourni dans Symfony, s'utilise comme un script shell, et est écrit en PHP. Sa première ligne est #!/usr/bin/env php

  • comme une commande grâce à l'utilisation du shebang en tête d'un paquet PHP (fichier .phar)
    composer.phar update
  • en mode server local à travers l'utilisation du serveur Web intégré à PHP
    php -S localhost:8000 -t public/

    PHP lance ici un serveur Web local que vous allez interroger avec votre navigateur Web.

Documentation

Vous aurez besoin de la documentation de Symfony ainsi que de celle des diverses API :

Mise en place d'une application Symfony

Vous allez créer des applications Symfony de façon conventionnelle, en utilisant l'outil « symfony » en ligne de commande (« Symfony CLI »). Une nouvelle application Symfony nécessite un peu plus de 80 Mo de sources PHP pour fonctionner (environ 10.000 fichiers dans un peu moins de 2.000 répertoires !). Ces données seront rapidement agrémentées de plusieurs dizaines de mégaoctets de cache générés par Symfony.

Vous allez installer Symfony à l'aide de l'outil « symfony » en ligne de commande (« Symfony CLI ») et pour cela, vous allez devoir l'installer. Cet outil utilise Composer dont vous devrez vous assurer du bon fonctionnement.

Quelques considérations liées à la configuration système de l'IUT

Remarque importante

Si vous travaillez sur votre ordinateur personnel, sur Linux ou sur Windows, vous pouvez placer le répertoire « symfony-contacts » du projet où bon vous semble. La partie qui suit concerne uniquement le travail sur les ordinateurs de l'IUT. Cela ne vous empêche pas d'en prendre connaissance.

Votre répertoire d'accueil « $HOME » se trouve sur un emplacement réseau NFS, ce qui vous permet de retrouver vos fichiers quel que soit le PC du département sur lequel vous vous connectez. Symfony utilise le sous-répertoire « var » du projet pour la gestion de son cache, des traces d'exécution et des données de session. Ces tâches génèrent de nombreuses entrées-sorties sur le système de fichiers, particulièrement inadaptées aux systèmes de fichiers en réseau. Il en va de même pour PhpStorm qui surveille les modifications de tous les fichiers de votre projet et ne peut pas fonctionner normalement lorsque ces fichiers sont sur un partage NFS. Afin d'améliorer les performances de votre application et d'éviter de saturer le serveur de fichiers du département informatique, vous allez travailler dans le répertoire « /working/votre_login » de la machine sur laquelle vous êtes connecté.

En fin de séance

En fin de séance, vous devez impérativement synchroniser votre dépôt distant (en ligne de commande ou avec PhpStorm)

git commit
puis
git push
faute de quoi vous perdrez votre travail entre chaque séance !

Pensez également à supprimer votre répertoire de travail /working/votre_login/symfony-contacts pour éviter la saturation du disque local :

rm -Rf /working/votre_login/symfony-contacts

En début de séance

Lorsque vous reprenez le travail la séance suivante, vous avez trois choix possibles, qui dépendent de la façon dont vous avez travaillé :

  • Mettre à jour votre dépôt local
    cd /working/votre_login/symfony-contacts
    git pull
  • Effacer votre dépôt local et repartir de zéro
    cd /working/votre_login
    rm -Rf /working/votre_login/symfony-contacts
    git clone https://iut-info.univ-reims.fr/gitlab/votre_login/symfony-contacts.git
    cd /working/votre_login/symfony-contacts
  • Réinitialiser votre dépôt local selon le dépôt distant
    cd /working/votre_login/symfony-contacts
    git fetch
    git reset --hard origin/main

Ensuite, dans le répertoire de votre projet, vous devez et (ré)installer les composants nécessaires à son fonctionnement :

composer install

Vous devrez également reconfigurer votre accès base de données en redéfinissant le fichier « .env.local »

Installation de l'exécutable « symfony »

Le développement d'applications Symfony peut être facilité par l'utilisation de l'outil « symfony » en ligne de commande (« Symfony CLI »). Ce dernier peut contrôler que le système comporte tous les prérequis pour le développement d'applications Symfony ou tester la présence de failles de sécurité connues.

Travail à réaliser
  1. Installez l'exécutable « symfony » qui contient le serveur Web local en lançant la commande suivante :
    wget https://get.symfony.com/cli/installer -O - | bash
    --2022-09-16 15:27:24--  https://get.symfony.com/cli/installer
    Resolving get.symfony.com (get.symfony.com)... 13.32.145.22, 13.32.145.97, 13.32.145.64, ...
    Connecting to get.symfony.com (get.symfony.com)|13.32.145.22|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 6100 (6,0K) [binary/octet-stream]
    Saving to: ‘STDOUT’
    
    -                                   100%[==================================================================>]   5,96K  --.-KB/s    in 0s      
    
    2022-09-16 15:27:24 (378 MB/s) - written to stdout [6100/6100]
    
    Symfony CLI installer
    
    Environment check
      [*] cURL is installed
      [*] Tar is installed
      [*] Git is installed
      [*] Your architecture (amd64) is supported
    
    Download
      Downloading https://github.com/symfony-cli/symfony-cli/releases/latest/download/symfony-cli_linux_amd64.tar.gz...
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
    100 5105k  100 5105k    0     0  15.4M      0 --:--:-- --:--:-- --:--:-- 81.7M
      Uncompress binary...
      Installing the binary into your home directory...
      The binary was saved to: /home/Users/cutron01/.symfony5/bin/symfony
    
    The Symfony CLI was installed successfully!
    
    Use it as a local file:
      /home/Users/cutron01/.symfony5/bin/symfony
    
    Or add the following line to your shell configuration file:
      export PATH="$HOME/.symfony5/bin:$PATH"
    
    Or install it globally on your system:
      mv /home/Users/cutron01/.symfony5/bin/symfony /usr/local/bin/symfony
    
    Then start a new shell and run 'symfony'
    
  2. Modifiez votre « .bashrc » (à la racine de votre compte) afin qu'il contienne
    export PATH="$HOME/.symfony5/bin:$PATH"
  3. Chargez les modifications de votre « .bashrc »
    source ~/.bashrc
  4. Vérifiez le bon fonctionnement de l'exécutable « symfony »
    symfony self:version
  5. Contrôlez la compatibilité du système avec la commande :
    symfony check:requirements  --verbose
    Symfony Requirements Checker
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    > PHP is using the following php.ini file:
    /etc/php/8.0/cli/php.ini
    
    > Checking Symfony requirements:
    
    [OK] iconv() must be available
    [OK] json_encode() must be available
    [OK] session_start() must be available
    [OK] ctype_alpha() must be available
    [OK] token_get_all() must be available
    [OK] simplexml_import_dom() must be available
    [OK] detect_unicode must be disabled in php.ini
    [OK] xdebug.show_exception_trace must be disabled in php.ini
    [OK] xdebug.scream must be disabled in php.ini
    [OK] PCRE extension must be available
    [OK] string functions should not be overloaded
    [OK] xdebug.max_nesting_level should be above 100 in php.ini
    [OK] PCRE extension should be at least version 8.0 (10.4 installed)
    [OK] PHP-DOM and PHP-XML modules should be installed
    [OK] mb_strlen() should be available
    [OK] utf8_decode() should be available
    [OK] filter_var() should be available
    [OK] posix_isatty() should be available
    [OK] intl extension should be available
    [OK] intl extension should be correctly configured
    [OK] intl ICU version should be at least 4+
    [OK] intl.error_level should be 0 in php.ini
    [OK] a PHP accelerator should be installed
    [OK] short_open_tag should be disabled in php.ini
    [OK] magic_quotes_gpc should be disabled in php.ini
    [OK] register_globals should be disabled in php.ini
    [OK] session.auto_start should be disabled in php.ini
    [OK] xdebug.max_nesting_level should be above 100 in php.ini
    [OK] "memory_limit" should be greater than "post_max_size".
    [OK] "post_max_size" should be greater than "upload_max_filesize".
    [OK] PDO should be installed
    [OK] PDO should have some drivers installed (currently available: mysql, sqlite)
    
    
                                                  
     [OK]                                         
     Your system is ready to run Symfony projects 
                                                  
    
    Note  The command console can use a different php.ini file
    ~~~~  than the one used by your web server.
          Please check that both the console and the web server
          are using the same PHP version and configuration.
    
    Information

    Dans un environnement Windows, vous pouvez télécharger une archive zip qui contient « symfony.exe ». Ce programme est l'exécutable de l'outil que vous devez donc placer dans un répertoire de votre système et le rendre accessible en ligne de commande (le répertoire doit figurer dans la variable d'environnement « Path »).

Création d'un projet Symfony

Vous allez pouvoir créer un projet Symfony à l'aide de l'outil « symfony » en ligne de commande (« Symfony CLI »).

Travail à réaliser
  1. Si ce n'est pas déjà fait, installez Composer
    Information

    L'outil « symfony » en ligne de commande (« Symfony CLI ») utilise Composer pour créer le nouveau projet. S'il n'est pas disponible sur le système, une version temporaire sera téléchargée. Il est plus rapide et logique d'avoir votre propre version fonctionnelle qui sera par ailleurs utile par la suite.

  2. Vérifiez que Composer fonctionne correctement :
    composer about
  3. Mettez à jour Composer :
    composer self-update
  4. Vérifiez que vous êtes bien dans le répertoire « /working/votre_login » (ou n'importe quel répertoire de votre choix si vous êtes sur votre ordinateur personnel)
  5. Lancez la création d'un nouveau projet Symfony version « 6.3.* » :
    symfony --version 6.3 --webapp new symfony-contacts
    Information

    L'équivalent avec Composer est :

    composer create-project symfony/website-skeleton symfony-contacts "6.3.*"

Vérification du bon fonctionnement de l'application

Si vous avez réalisé toutes les étapes précédentes sans encombre, votre application doit fonctionner. Vous allez donc le vérifier. Vous allez utiliser l'environnement de développement de Symfony qui est celui qui est activé par défaut en mode développement.

Travail à réaliser
  1. Placez-vous le répertoire de votre application
  2. Dans le terminal, lancez le serveur Web local avec la commande suivante :
    symfony serve
    Le serveur Web fonctionnera tant que vous n'aurez pas terminé l'exécution l'outil ligne de commande « symfony » avec « CTRL+C ».
  3. Lancez votre navigateur web et saisissez l'URL suivante :
    http://127.0.0.1:8000
    Vous devriez obtenir : Test de l'environnement de développement
  4. Constatez l'apparition de la Web Debug Toolbar
  5. Observez les sorties texte du serveur Web local
  6. Naviguez rapidement à travers les outils de la Web Debug Toolbar
  7. Observez les sorties texte du serveur Web local
  8. Terminez l'exécution de l'outil ligne de commande « symfony » avec « CTRL+C »

Gestion des versions avec Git

Afin de fiabiliser votre méthode de développement, vous allez utiliser Git pour gérer le suivi des versions de votre application Symfony.

Dépôt local

Vous allez configurer votre dépôt Git local.

Travail à réaliser
  1. Assurez-vous d'être dans le répertoire du projet symfony-contacts
  2. Constatez que le dépôt Git local a été initialisé par l'outil « symfony » en ligne de commande (« Symfony CLI »)
    git log
Information

Si vous créez le projet avec Composer, vous devez effectuer les actions suivantes :

  1. Initialisez votre dépôt Git
    git init
  2. Ajoutez l'ensemble des fichiers à l'index
    git add .
  3. Effectuez la première validation
    git commit -m "Initial commit"
  4. Renommez la branche principale en « main » (une branche vide ne peut pas être renommée, c'est pourquoi ceci est fait après le premier « commit »)
    git branch -m main

Dépôt distant

Passons maintenant à la configuration du dépôt distant.

Travail à réaliser
  1. Créez un nouveau projet « symfony-contacts » sur GitLab (pensez à décocher la case « Initialize repository with a README »)
  2. Associez le dépôt local et le dépôt distant
  3. Poussez votre branche locale
Remarque importante

Dans toute la suite du TP, pensez à effectuer des « commit » réguliers (un par question au minimum). Vous pouvez utiliser PhpStorm pour les commandes Git « commit » et « push ». PhpStorm propose une option « Commit and Push… » particulièrement adaptée à votre situation : Configuration de Git

N'oubliez pas d'ajouter les nouveaux fichiers au gestionnaire de versions (Ctrl+Alt+A) avant vos « commit ». Vous pouvez constater que le nom des fichiers s'affiche en vert pour ceux qui ont été ajoutés au suivi par opposition au rouge pour ce qui n'ont pas été ajoutés et gris clair pour ceux qui sont ignorés. Configuration de Git

Création du projet dans PhpStorm

La gestion d'une application Symfony nécessitant de modifier de nombreux fichiers dans une arborescence complexe, vous utiliserez PhpStorm pour l'édition de texte. Des greffons apportent le support de Symfony, Composer, des fichiers YAML, Twig, … Certains sont préinstallés dans la configuration générale.

Vous allez créer un nouveau projet PHP à partir de PhpStorm pour gérer votre application Web.

Remarque importante

PhpStorm est un outil puissant parmi d'autres développés par JetBrains. Leur utilisation dans le cadre professionnel impose de s'acquitter d'une licence annuelle. Cependant, dans la cadre universitaire, vous pouvez disposer d'une licence gratuite pour les étudiants. Ceci vous permet d'utiliser les outils sur votre ordinateur personnel, tant que vous êtes étudiant. L'utilisation de PhpStorm au sein du département ne nécessite pas de démarche particulière puisque nous avons un serveur de licences JetBrains.

Je vous invite néanmoins à demander une licence et à travailler sur votre ordinateur personnel en dehors des séances de TP.

Travail à réaliser
  1. Dans votre terminal et dans le répertoire du projet, lancez la commande
    phpstorm . &
    Information

    Cette commande est la façon la plus simple et la plus rapide de créer un nouveau projet PhpStorm dans un répertoire. Il n'est pas utile de la lancer une fois le projet créé puisque vos anciens projets sont proposés à l'ouverture de PhpStorm ou dans son menu « File » puis « Recent Projects ».

  2. Patientez pendant l'indexation des fichiers du projet visible dans la partie droite de la barre d'état Indexatioon du projet
  3. Installez le greffon « Symfony Support » Symfony plugin installed
  4. Activez le greffon « Symfony Support » si ce n'était pas déjà fait et que cela vous est proposé Proposition d'auto-configuration du greffon Symfony
  5. Installez PHP CS Fixer en vous référant au tutoriel Installation et configuration de PhpStorm pour configurer PHP CS Fixer
  6. Rendez-vous dans les préférences de PhpStorm dans « PHP → Quality Tools » et vérifiez que PHP CS Fixer est bien configuré Configuration de PHP CS Fixer dans PhpStorm
    Remarque importante

    La création du projet PhpStorm induit la création du répertoire « .idea » dans le répertoire de votre projet Symfony. En temps normal, en particulier lors d'un développement collaboratif comme la SAÉ de S3, il est impensable d'inclure le répertoire « .idea » au dépôt Git. En effet, chaque modification de la configuration de PhpStorm faite par l'un des développeurs va engendrer la modification de la configuration de tous les autres développeurs une fois que la modification se retrouvera dans la branche principale.

    Néanmoins, à cause de la configuration particulière au sein du département informatique qui vous oblige à travailler dans le répertoire local « /working/votre_login » sur des ordinateurs différents, et parce que vous êtes l'unique développeur de votre projet, il est légitime d'inclure le répertoire de configuration de PhpStorm, « .idea », afin que vous n'ayez pas à reconfigurer votre environnement lorsque vous clonerez votre dépôt en début de séance.

Documentation du projet

Tout projet de développement doit fournir les clés pour l'installation, la configuration et le démarrage par un nouveau développeur. Cet effort de documentation peut également s'avérer bénéfique pour le créateur du projet qui peut oublier certains points essentiels de configuration au fil du temps. Ainsi, vous allez rédiger le mode d'emploi de votre projet dans un fichier « REAMDE.md » et le tenir à jour au fil du développement.

Travail à réaliser
  1. Créez le fichier « README.md » à l'aide de PhpStorm (qui vous propose de l'ajouter à l'index Git)
  2. Complétez les informations suivantes :
    1. un titre de niveau 1 contenant le titre explicite du projet
    2. un titre de niveau 2 « Auteur(s) » vous permettant de préciser votre nom et votre prénom (ainsi que ceux de votre binôme, le cas échéant)
    3. un titre de niveau 2 « Installation / Configuration » précisant les points essentiels permettant d'installer, de configurer et de lancer le projet
  3. Validez votre travail dans Git

Mise en place de scripts Composer

Les dépendances de votre projet Symfony sont gérées par Composer. Profitons-en pour mettre en place quelques scripts Composer qui faciliteront la gestion quotidienne du projet.

Travail à réaliser
  1. Ajoutez un script « start » qui lance le serveur web de test (« symfony serve ») sans restriction de durée d'exécution
  2. Ajoutez un script « test:cs » qui lance la commande de vérification du code par PHP CS Fixer
  3. Ajoutez un script « fix:cs » qui lance la commande de correction du code par PHP CS Fixer
  4. Documentez vos scripts dans le fichier « composer.json »
  5. Documentez vos scripts dans le fichier « README.md »
  6. Validez votre travail dans Git
  7. Relancez votre serveur Web local à l'aide du script Composer que vous venez d'écrire

Le routeur

Dans le framework Symfony, un unique programme « public/index.php » est lancé pour chaque requête HTTP vers votre application et il est dépendant de l'environnement, par défaut « dev » pour développement et « prod » pour production. Le système de routage entre alors en jeu pour permettre de réaliser des tâches différentes en fonction de la requête HTTP soumise à l'application.

Travail à réaliser

Lisez le point « Routing: Under the Hood » d'une ancienne version de la documentation qui avait l'avantage de proposer une représentation graphique du flux applicatif qui est reprise ici : Symfony request flow

Les routes actuelles

Les routes disponibles sont potentiellement différentes en fonction de l'environnement de l'application. Il existe, par défaut, des routes accessibles dans les environnements « prod » et « dev » et des routes spécifiques à l'environnement de développement.

Travail à réaliser
  1. Observez les routes disponibles dans l'environnement de production :
    bin/console debug:router --env=prod
  2. Observez la configuration générale du routage en ouvrant le fichier « config/routes.yaml » qui décrit que le routage est défini dans les attributs de vos contrôleurs
  3. Observez les routes disponibles dans l'environnement de développement, qui est celui par défaut dans votre configuration :
    bin/console debug:router

    Les routes disponibles dans l'environnement de développement sont actuellement, dans cet ordre :

    • Une route pour les erreurs Twig
    • une route « _wdt » qui correspondent à la « Web Debug Toolbar » Web Debug Toolbar
    • celles commençant par « _profiler » qui sont destinée au « Profiler » (profilage de l'application lors de son exécution)
  4. Observez la configuration du routage « Profiler » en ouvrant le fichier « config/routes/web_profiler.yaml »
  5. Vous constatez que les routes sont définies dans la configuration du « WebProfilerBundle » et uniquement dans l'environnement de développement avec « when@dev » en début de configuration

Liens entre route et contrôleur

Chaque route définie permet de déclencher une action d'un contrôleur (« Controller »). Une action est une méthode publique d'un contrôleur qui est associée à une URI, généralement à l'aide d'un attribut « Route » qui précède la méthode.

Les fichiers des contrôleurs sont localisés dans le répertoire « Controller » et sont nommés « xxxController.php », « xxx » étant le nom du contrôleur. Les contrôleurs font partie de l'espace de nom « App\Controller » ou d'un sous espace de nom de ce dernier. Ils héritent possiblement de « Symfony\Bundle\FrameworkBundle\Controller\AbstractController ».

Travail à réaliser
  1. Complétez vos connaissances en lisant au moins la partie « Creating Routes as Attributes » de la documentation.
  2. Créez un nouveau contrôleur « HelloController » à l'aide du « MakerBundle » :
    bin/console make:controller Hello
    Information

    Le « MakerBundle » peut fonctionner en mode interactif ou à l'aide de paramètres de la ligne de commande. Ici, vous avez fourni le paramètre « Hello » qui correspond au nom du contrôleur que vous souhaitez. Ce dernier sera donc nommé « HelloController » car le « MakerBundle » ajoute automatiquement le suffixe « Controller » que vous n'aviez pas précisé.

  3. Ouvrez le fichier du contrôleur dans PhpStorm
  4. Observez l'ensemble des routes disponibles pour votre application avec la commande
    bin/console debug:router
  5. Observez l'ensemble des routes disponibles pour votre application ainsi que les contrôleurs associés avec la commande
    bin/console debug:router --show-controllers
  6. Faites le lien entre la première route disponible et le code généré par le « MakerBundle »
  7. Observez les détails de la route nommée « app_hello » grâce à la commande
    bin/console debug:router app_hello
  8. Ajoutez l'action et la route associée qui suivent dans votre contrôleur :
        #[Route('/hello/world')]
        public function world(): Response
        {
            return new Response('Hello world!');
        }
    
  9. Notez que cette route ne comporte pas de nom dans l'attribut #[Route( et faites le lien avec le nom généré automatiquement que vous observez à l'aide de la commande
    bin/console debug:router
  10. Essayez l'URL « http://localhost:8000/hello/world »
  11. Regardez le code source HTML du contenu reçu par votre navigateur
  12. Essayez l'URL « http://localhost:8000/hello/guys »
  13. Constatez la réponse reçue par votre navigateur et son code HTTP

Route contenant un paramètre

Les routes ne sont pas nécessairement définies à partir d'un URI fixe qui permet de déclencher une action d'un contrôleur. Il est en effet possible de rendre paramétriques des parties de l'URI.

Travail à réaliser
  1. Complétez vos connaissances en lisant au moins la partie « Route Parameters » de la documentation.
  2. Remplacez le texte « world » du chemin « /hello/world » de la route par un paramètre « name »
  3. Adaptez le prototype de l'action « world() » afin qu'elle puisse recevoir la valeur du paramètre « name » de la route
  4. Observez la nouvelle formulation de la route de votre application avec la commande
    bin/console debug:router
  5. Remplacez le mot « world » de la chaîne « Hello world! » par la valeur du paramètre de l'action
    Information

    Ce que vous venez de faire est extrêmement dangereux puisque toute chaîne fournie dans l'URL va se retrouver directement dans la réponse envoyée au navigateur, constituant ainsi une possibilité d'attaque XSS reflétée. Il faudrait échapper la valeur de la variable « $name », comme cela vous a déjà été démontré à plusieurs reprises dans les précédents sujets de TP.

  6. Essayez l'URL « http://localhost:8000/hello/world »
  7. Essayez l'URL « http://localhost:8000/hello/bob »
  8. Constatez la réponse reçue par votre navigateur et son code HTTP
  9. Remarquez le lien entre le paramètre « name » du chemin de la route et le paramètre « $name » de l'action

Les vues dans Symfony : Twig

Maintenant que vous avez rendu paramétrique l'action world() du contrôleur HelloController, vous allez lui associer le rendu d'une vue Twig.

Web Debug Toolbar

La « Web Debug Toolbar » n'est activée que si la vue contient la balise de fin de corps d'une page Web </body>.

Travail à réaliser
  1. Saisissez l'URL « http://localhost:8000/hello/world » dans votre navigateur
  2. Dans l'action « world() » du « HelloController », demandez le rendu d'un modèle Twig en remplaçant l'instruction
    return new Response('…
    par
    return $this->render('hello/world.html.twig');
  3. Essayez l'URL « http://localhost:8000/hello/world »
  4. Constatez l'erreur signalée par Symfony ainsi que le code réponse HTTP reçu par votre navigateur
  5. Créez le fichier « world.html.twig » manuellement en effectuant un clic droit sur le répertoire « templates/hello » dans PhpStorm et en utilisant le menu « New » puis « File »
  6. Essayez l'URL « http://localhost:8000/hello/world »
  7. Constatez la réponse vide reçue par votre navigateur
  8. Ajoutez le texte « Hello world! » dans le fichier « templates/hello/world.html.twig »
  9. Essayez l'URL http://localhost:8000/hello/world
  10. Constatez que la « Web Debug Toolbar » n'est pas présente
  11. Regardez le code source HTML du contenu reçu par votre navigateur
  12. Faites en sorte que le fichier « templates/hello/world.html.twig » propose une structure HTML correcte dont le titre sera « Hello World! » et qui contiendra le texte « Hello World! »
  13. Essayez l'URL « http://localhost:8000/hello/world »
  14. Notez l'apparition de la « Web Debug Toolbar ».
  15. Cliquez sur un des éléments de la « Web Debug Toolbar » pour accéder au profileur
  16. Naviguez dans les pages du profileur afin d'en découvrir les possibilités

Transmettre des données à la vue

La vue a pour objectif de présenter les données à l'utilisateur. Vous allez donc découvrir comment transmettre des données au modèle Twig et comment les exploiter.

Travail à réaliser
  1. Saisissez l'URL « http://localhost:8000/hello/bob » dans votre navigateur
  2. Lisez et observez les exemples de « Rendering Templates » et « Creating Templates »
  3. Passez la valeur du paramètre de l'action à la vue
    Information

    Vous avez remarqué que le paramètre du chemin de la route « {name} » se retrouve sous le même nom « $name » comme paramètre de l'action (et c'est d'ailleurs par rapport au nom que Symfony fait « automagiquement » la correspondance entre les deux). Lorsque vous passez la même valeur à la vue, vous devez choisir le nom sous lequel la donnée sera manipulée dans le modèle Twig. Afin de maintenir une logique globale et faciliter la compréhension de votre code, il est vivement conseillé de préserver le même nom pour l'utilisation dans le modèle Twig.

  4. Utilisez la valeur dans la vue pour produire « Hello la_valeur_du_paramètre! » dans un titre de niveau 1 et dans un paragraphe
  5. Vérifiez le bon fonctionnement de cette nouvelle version de la vue dans votre navigateur Web
  6. Saisissez l'URL « http://localhost:8000/hello/bob<script> » dans votre navigateur (oui, il y a bien « <script> » à la fin de l'URL !)
  7. Observez le résultat dans le navigateur
  8. Observez le code source HTML du résultat dans le navigateur

Héritage et inclusion de « templates » Twig

Vous venez de réaliser une vue à l'aide de « templates » Twig. Cependant, la constitution de plusieurs vues ne devrait pas demander d'écrire une page Web complète pour chacune d'entre elles. Il est possible d'éviter ce genre de répétition en utilisant l'héritage ou l'inclusion de « templates ». Pour y parvenir, il convient de respecter les conventions de nommage et d'emplacement des modèle Twig.

Travail à réaliser
  1. Modifiez votre vue « world.html.twig » afin qu'elle hérite de « base.html.twig »
  2. Remplacez la structure HTML de votre modèle par l'utilisation des blocs définis dans « base.html.twig »
    Information

    Profitez des suggestions de PhpStorm pour retrouver simplement les noms des blocs disponibles hérités du modèle « base.html.twig ».

  3. Accédez à « http://localhost:8000/hello/bob »
  4. Vérifiez le code source du résultat dans votre navigateur

Prise en main rapide de Twig

Vous allez explorer quelques-unes des possibilités offertes par Twig.

Travail à réaliser
  1. Utilisez une structure algorithmique Twig « for » pour afficher 10 fois le paragraphe de salutation dans le nouveau « template »
  2. Modifiez le texte produit pour y inclure le numéro du tour de boucle de 1 à 10 en fin de ligne (le résultat est donc de la forme « Hello bob! (6) »)
  3. Utilisez la fonction « cycle » de Twig pour afficher alternativement en rouge puis vert (un attribut HTML « style="" » suffira pour cet essai.
  4. Ajoutez un filtre sur le nom afin qu'il apparaisse avec la première lettre en majuscule
  5. Installez le paquet Composer « twig/intl-extra » pour bénéficier des fonctions d'internationalisation de Twig
  6. Formatez le nombre de 1 à 10 pour qu'il soit systématiquement affiché sur deux caractères

De la route à la vue

Vous allez créer une nouvelle route, une nouvelle action dans votre contrôleur ainsi qu'un nouveau « template » Twig associé. L'action permettra d'afficher le message de salutation un nombre de fois donné.

Création d'une nouvelle route

Vous allez ajouter une nouvelle route pour découvrir de nouvelles possibilités du routeur de Symfony.

Travail à réaliser
  1. Consultez les règles de bonnes pratiques concernant les contrôleurs
  2. Créer une action vide « manyTimes() » dans le contrôleur « HelloController »
    Information

    En saisissant « pubf » suivi de « TAB » dans une classe, PhpStorm génère automatiquement

        public function ()
        {
            
        }

    Vous êtes alors prêt à saisir le nom de la méthode et un nouvel appui sur TAB fait passer le focus sur la saisie des paramètres de la méthode.

  3. Associez une route de chemin « /hello/name/times » paramétré par « name » et « times » à cette action en ajoutant l'attribut « #[Route(… »
  4. Si vous décidez de nommer votre route, utilisez le nommage standard « app_nomducontroleur_nomdelaction » (c'est un exemple !)
  5. Modifiez votre action pour que ses paramètres soient en accord avec ceux du chemin de la route
  6. Consultez les règles de bonnes pratiques concernant le nommage des modèles Twig
  7. Retournez le résultat la méthode « render() » du modèle « hello/many_times.html.twig »
  8. Utilisez les suggestions de PhpStorm (« Alt+Entrée ») pour générer automatiquement ce nouveau modèle : Twig: Create Template
    Remarque importante

    Si la génération automatique du « template » n'est pas proposée par PhpStorm, c'est que vous n'avez pas installé le plugin Symfony : Symfony plugin installed que vous ne l'avez pas activé lorsque cela vous a été proposé : Symfony plugin enabled ou qu'il n'est pas actif pour ce projet : Symfony plugin enabled for this project

  9. Modifiez le code Twig en prenant « world.html.twig » comme base :
    • Remplacez le titre de la page Web par « Hello many times! »
    • Remplacez le titre de niveau de la page Web par « Hello many times valeur_du_paramètre_name! »
    • Affichez « times » fois au lieu de « 10 » fois le même paragraphe de salutation que dans « world.html.twig »
  10. Essayez l'URL « http://localhost:8000/hello/bob/5 » et vérifiez le bon fonctionnement

Ajout de conditions sur les paramètres

Le routage de Symfony permet de vérifier que les paramètres répondent à certaines conditions pour pouvoir déclencher une route.

Travail à réaliser
  1. Complétez la route pour que son second paramètre times soit un chiffre en utilisant la validation de paramètres
  2. Essayez l'URL « http://localhost:8000/hello/bob/five » et vérifiez que la validation du paramètre « times » fonctionne (et donc que la route n'est pas acceptée !)
  3. Vérifiez dans l'action du contrôleur que « times » est bien différent de zéro et inférieur ou égal à 10. Fixez sa valeur à 3 dans le cas contraire
  4. Essayez les URL « http://localhost:8000/hello/bob/5 », « http://localhost:8000/hello/bob/0 » ainsi que « http://localhost:8000/hello/bob/42 » et vérifiez la cohérence du résultat produit
  5. Rendez le paramètre « times » optionnel qui vaudra par défaut « 3 ».
  6. Utilisez la « Web Debug Toolbar » pour visualiser quelle est la route déclenchée par l'URL « http://localhost:8000/hello/bob »
  7. Faîtes en sorte que ce soit la route « app_hello_manytimes » qui soit déclenchée par l'URL « http://localhost:8000/hello/bob » (ceci doit mettre en évidence la façon dont les routes sont étudiées par Symfony)

Redirection dans le contrôleur

Les contrôles que vous venez d'effectuer conduisent à une forme d'incohérence de l'application. En effet, si vous demandez l'URL « /hello/bob/42 », les conditions ajoutées dans le contrôleur vont limiter l'affichage à 3 messages alors que l'URL contient toujours « 42 ». Il serait souhaitable dans ce cas que le « 42 » de l'URL soit remplacé par « 3 ». En raisonnant du côté serveur, il faut donc effectuer une première réponse à la requête de « /hello/bob/42 » qui demande une redirection vers « /hello/bob/3 » afin que le navigateur effectue une nouvelle demande de ressource, correcte cette fois.

Travail à réaliser
  1. Observez l'incohérence entre l'« URL » « /hello/bob/42 » demandée et les 3 messages affichés dans le navigateur
  2. Lisez le chapitre concernant les redirections
  3. Effectuez une redirection dans l'action « manyTimes() » en demandant seulement « 3 » affichages lorsque le paramètre « times » est hors des bornes fixées précédemment
    Information

    Si vous tapez les guillemets du premier paramètre de la méthode « redirectToRoute() », PhpStorm doit vous proposer la liste de routes disponibles. Suggestion de routes dans redirectToRoute(

  4. Vérifiez le fonctionnement en observant la redirection de « /hello/bob/42 » dans la barre de développement de votre navigateur (pensez à cocher « Preserve log » pour pouvoir visualiser la réponse de redirection)

De la vue à la route

Symfony et son modèle de routeur prévoient de ne jamais écrire manuellement des URL vers les ressources de l'application. Les URL doivent être produites automatiquement en Twig en se basant sur le nom de la route correspondant à l'URL souhaitée.

Travail à réaliser
  1. Faites le ménage dans l'action « index() » qui avait été générée par le « MakerBundle » dans le contrôleur « HelloController » pour ne préserver que le rendu de la vue
  2. Faites également le ménage dans le modèle associé « hello/index.html.twig » en supprimant le contenu de tous les blocs hormis le titre
  3. Consultez les règles de production de liens vers des ressources de l'application
  4. Dans une liste à puces, créez un lien relatif permet de produire le résultat de « 5 » « Hello » à « Joe »
    Information

    PhpStorm peut vous proposer les routes existantes dans les fonctions Twig « path() » ou « url() », profitez-en.

  5. Dans une liste à puces, créez un lien absolu permet de produire le résultat de « 8 » « Hello » à « Bob »
  6. Vérifiez le bon fonctionnement de vos liens
  7. Vérifiez que vos deux liens sont bien respectivement relatif et absolu en observant le code HTML reçu par votre navigateur

Introduction aux tests fonctionnels

Vous avez été sensibilisés aux tests pendant le « TP Développement d'une application Web de consultation et modification de morceaux de musique ». Vous avez certainement remarqué le répertoire « tests » dans l'arborescence de Symfony. Il contiendra les tests unitaires, d'intégration ou fonctionnels associés à vos classes.

Symfony est nativement prévu pour fonctionner avec PHPUnit grâce au « PHPUnit Bridge ». Pour des raisons de simplicité d'écriture et de facilité de compréhension, nous utiliserons plutôt Codeception qui possède un module pour Symfony.

Installation et configuration de Codeception

Travail à réaliser
  1. Installez Codeception et ses modules « asserts » et « symfony » à l'aide de Composer :
    composer require --dev --no-interaction codeception/codeception codeception/module-asserts codeception/module-symfony
    Information

    L'option « --no-interaction » permet d'éviter toute interaction lors de l'installation. Dans ce cas particulier, il ne vous sera donc pas demandé si vous souhaitez appliquer les recettes Flex de Codeception. Si toutefois la question vous est tout de même posée, nous ne souhaitons pas les appliquer car cela crée de nombreux répertoires et fichiers dans « tests » et nous n'en avons pas l'utilité.

  2. Initialisez Codeception pour votre projet :
    php vendor/bin/codecept bootstrap --namespace=App\\Tests --empty
  3. Ajoutez les paramètres de configuration de Symfony à l'environnement de Codeception en complétant le fichier « codeception.yml » :
    params:
        - .env
        - .env.test
  4. Modifiez la configuration de PHP CS Fixer dans le fichier « .php-cs-fixer.dist.php » pour exclure le répertoire « tests/Support/_generated »

Tests de l'action « manyTimes() » du contrôleur « HelloController »

Travail à réaliser
  1. Créez une nouvelle suite de tests que nous dédirons aux contrôleurs :
    php vendor/bin/codecept generate:suite Controller
  2. Mettez au propre le source PHP généré :
    composer fix:cs
  3. Créez un « Cest » (« Codecept » + « Test ») dédié à l'action « manyTimes() » du « HelloController » :
    php vendor/bin/codecept generate:cest Controller Hello\\ManyTimes
  4. Activez les modules « Symfony » et « Asserts » de la suite « Controller » dans le fichier « tests/Controller.suite.yml » :
    modules:
        # enable helpers as array
        enabled:
            - Symfony:
                  app_path: 'src'
                  environment: 'test'
            - Asserts:
    
  5. Lancez la construction des classes « Actor » de Codeception pour prendre en compte les nouveaux modules :
    php vendor/bin/codecept build
  6. Remplacez le fichier des tests de votre contrôleur par l'ensemble de tests « ManyTimesCest.php » (télécharger)
    Information

    Ces tests fournis sont assez détaillés pour que vous les utilisiez comme base d'exemple.

  7. Lancez les tests à l'aide de la commande
    php vendor/bin/codecept run
  8. Vérifiez que votre code passe les tests

Ajout de scripts Composer pour lancer les tests

Travail à réaliser
  • Ouvrez le fichier « composer.json »
  • Ajoutez un script « test:codeception » qui
  • Ajoutez un script « test » qui lance :
    1. le script Composer qui teste la mise en forme du code
    2. le script Composer des tests avec Codeception
  • Décrivez ces scripts Composer dans « composer.json » et dans la documentation du projet

Configuration de l'accès base de données de l'application

Vous avez utilisé l'outil « symfony » en ligne de commande pour installer votre application Symfony. Des valeurs par défaut ont été affectés à divers paramètres de configuration dans le fichier « .env » situé à la racine de votre projet.

Remarque importante

Le fichier « .env » sera inclus dans votre dépôt Git. Il est donc vital qu'il ne contienne pas vos mots de passe ! Il servira uniquement de base à la configuration en mentionnant les paramètres à définir. La configuration effective de votre application locale se fera dans le fichier « .env.local ». Ce dernier est par défaut exclu du gestionnaire de versions et doit le rester.

Travail à réaliser
  1. Copiez le fichier « .env » en « .env.local » à partir de PhpStorm et ouvrez-le
  2. Donnez les valeurs adéquates à la variable DATABASE_URL afin de vous connecter sur le serveur dont le nom DNS est « mysql » accessible avec l'utilisateur « web » dont le mot de passe est « web » (ou selon vos paramètres si vous êtes sur votre ordinateur personnel) pour accéder à la base de données MySQL (MariaDB 10.2.25) nommée « cutron01_contact_demo »
    Information

    Notez que, dans le fichier « .env », plusieurs exemples de « DATABASE_URL » sont donnés et en particulier un adapté à MySQL/MariaDB.

    Retrouvez les parties constitutives d'une URL dans le sujet de TP sur le proptocole HTTP que vous avez réalisé en première année.

    Si vous souhaitez travailler sur votre ordinateur personnel, vous pouvez télécharger le script de création de la base de données (Clic droit puis « Enregistrer sous… »).

  3. Vérifiez votre accès à la base de données :
    bin/console doctrine:query:sql "SELECT * FROM contact WHERE id=4"

    Vous devriez obtenir une ligne de résultat :

     ---- ----------- ---------- ------------------------------ 
      id   firstname   lastname   email                         
     ---- ----------- ---------- ------------------------------ 
      4    Christine   Michaud    christine.michaud@raynaud.fr  
     ---- ----------- ---------- ------------------------------ 
    

Consultation des contacts

Le modèle de données étant à présent configuré, il est temps de l'utiliser. Pour cela, vous allez construire une nouvelle fonctionnalité de listage de l'ensemble des contacts de la base de données.

Modèle de données

Symfony intègre Doctrine pour effectuer la persistance et l'accès aux données dans une base de données. L'interaction entre votre application et la base de données se fera à travers des objets PHP appelés des entités qui constituent la logique métier de l'application ainsi que les relations entre les entités. Par convention, les définitions de ces classes seront localisées dans le répertoire src/Entity.

Afin que ces classes puissent interagir avec le SGBD, il convient de définir les liens entre les objets PHP et la base de données. Cette opération est qualifiée de « mapping » qui est décrit sous forme d'attributs PHP dans la définition de la classe. Le « mapping » peut être réalisé manuellement ou de façon interactive à l'aide du « MakerBundle ».

Vous avez configuré votre application pour accéder à la base de données « cutron01_contact_demo » : La table contact de la base de données

Information

La démarche habituelle pour un projet Symfony est de créer les entités et leurs relations en fonction des besoins révélés par la phase d'analyse, puis de créer la base de données à l'aide de Doctrine, pour ensuite insérer des données factices qui permettent d'obtenir une application de démonstration pour le développement. Nous dérogeons temporairement à cette règle pour des raisons de progression pédagogique en vous fournissant une base de données qui contient déjà des données générées par nos soins.

Travail à réaliser
  1. Lisez le paragraphe « Creating an Entity Class »
  2. Utilisez le « MakerBundle » pour générer l'entité « Contact » conformément à la table MySQL :
    bin/console make:entity Contact
    en donnant les propriétés suivantes :
    • « firstname » de type « string » de taille « 30 » non « nullable »
    • « lastname » de type « string » de taille « 40 » non « nullable »
    • « email » de type « string » de taille « 100 » non « nullable »
  3. Actualisez la liste des fichiers de votre répertoire « Entity » par un clic droit sur la racine du projet puis « 🗘 Reload from disk »
  4. Observez le fichier généré dans le répertoire « src/Entity »
  5. Consultez la classe générée et expliquez les divers attributs PHP.
  6. Observez le fichier généré dans le répertoire « src/Repository »

Liste des contacts

Travail à réaliser
  1. Créez un nouveau contrôleur « ContactController » à l'aide du « MakerBundle »
  2. Lisez le chapitre « Récupérer des objets dans la base de données » de la documentation de Symfony
  3. Supprimez tous les éléments inutiles de l'action et de la vue ainsi que les paramètres passés à la vue
  4. En paramètre de l'action « index() » générée par défaut, demandez le service « ContactRepository »
  5. Dans l'action, utilisez la méthode « findBy() » de la classe « EntityRepository » afin de récupérer l'ensemble des contacts de la base de données (critère de sélection vide) par ordre alphabétique (tri ascendant sur le nom puis le prénom)
  6. Transmettez l'ensemble des contacts à la vue Twig
  7. Donnez un titre au contenu Web produit et proposez un titre de niveau 1 représentatif
  8. Dans la vue, effectuez le parcours de l'ensemble des contacts pour produire une liste à puces présentant chaque contact sous la forme « nom, prénom »
  9. Vérifiez que la liste s'affiche correctement

Tests fonctionnels

Afin de poursuivre les tests fonctionnels, vous allez mettre en place des contrôles sur le résultat de la liste des contacts.

Information

La démarche des tests liés à la consultation de la base de données est ici incomplète (voire incorrecte) puisque nous nous appuyons sur la base de données de développement. Ceci vous permet néanmoins de prévoir les tests et la démarche sera rectifiée par la suite.

Travail à réaliser
  1. Copiez votre fichier d'environnement de test « .env.test » en « .env.test.local »
  2. Définissez la variable contenant le chemin d'accès à la base de données comme dans l'environnement de développement
  3. Ajoutez ce nouveau fichier d'environnement dans le fichier de configuration de Codeception « codeception.yml »
  4. Mettez en commentaire la propriété « dbname_suffix » de « when@test.doctrine.dbal » du fichier « config/packages/doctrine.yaml » pour éviter d'ajouter « _test » à la fin du nom de la base de données de test
  5. Générez une classe de « Cest » pour l'action « index() » du « ContactController » :
    php vendor/bin/codecept generate:cest Controller Contact\\Index
  6. Sur le modèle des tests présentés en début de TP, vérifiez dans un même test que la ressource « /contact » donne une réponse HTTP 200 et que le corps de la réponse vérifie que :
    • le titre de la page Web contient « Liste des contacts »
    • un titre de niveau 1 contenant « Liste des contacts » est présent
    • une liste à puce contient 195 éléments
    Information

    Profitez des suggestions de PhpStorm au fil de la saisie de votre code pour découvrir les méthodes à utiliser.

Remarque importante

Codeception enregistre dans le répertoire « tests/_output » le corps de la réponse HTTP des ressources impliquées dans des tests échoués. Consultez ces contenus, ceci facilite grandement la recherche des erreurs !

Détails d'un contact, version naïve

Vous allez à présent rechercher un contact pour afficher le détail de ses données. Cette recherche va s'effectuer de façon naïve à partir de l'identifiant du contact. Ceci permet de comprendre la mécanique et d'arriver en douceur au « ParamConverter » dans la partie suivante.

Travail à réaliser
  1. Dans votre contrôleur « ContactController », créez une action « show(int $contactId) » qui reçoit en paramètre un identifiant de contact
  2. Associez une route de chemin « /contact/{contactId} » à la nouvelle action
  3. Appliquez les conditions requises à « contactId »
  4. Dans la nouvelle action, demandez le rendu de la vue Twig « contact/show.html.twig »
  5. Générez le fichier Twig à l'aide de PhpStorm
  6. Vérifiez la bonne interaction de ces trois éléments
  7. En paramètre de l'action « show() », demandez le service « ContactRepository »
  8. Dans votre action, récupérez le contact concerné et transmettez-le à la vue
  9. Si le contact n'existe pas, levez une « NotFoundHttpException » dont le texte doit être explicite
  10. Présentez le nom et le prénom du contact dans le titre du document HTML et dans un titre de niveau 1
  11. Présentez toutes les informations du contact (hormis l'identifiant) dans une liste de définitions
  12. Vérifiez que la route produit un résultat conforme
  13. Vérifiez que la route produit une réponse « 404 » pour un identifiant de contact qui n'existe pas
    Information

    Vous ajouterez des tests pour cette action dès que vous aurez des générateurs de données fictives.

Détails d'un contact, utilisation d'un « EntityValueResolver »

Vous allez à présent rechercher un contact en utilisant un « EntityValueResolver » comme préconisé dans les bonnes pratiques. Cette démarche permet de simplifier le code.

Travail à réaliser
  1. Dans le contrôleur « ContactController », modifiez le chemin de la route « /contact/{contactId} » pour que le nom du paramètre corresponde au nom de l'identifiant de l'entité « Contact »
  2. Modifiez les paramètres de l'action « show() » pour :
    • Remplacer l'identifiant de contact par un « Contact »
    • Supprimer le service « ContactRepository » qui sera inutile
  3. Maintenez les conditions requises sur le paramètre du chemin de la route
  4. Supprimez le code maintenant inutile de l'action, en particulier le test
  5. Vérifiez que la route produit toujours un résultat conforme
  6. Vérifiez que la route produit une réponse « 404 » pour un identifiant de contact qui n'existe pas

Des routes pour produire des URL

Vous venez de construire une liste de contacts et les détails d'un contact. Il convient à présent de rendre accessible les détails d'un contact par un clic sur son nom dans la liste des contacts. Vous n'écrirez pas d'URL mais utiliserez les fonctionnalités Twig permettant de les produire à votre place à partir du nom des routes.

Travail à réaliser
  1. Si nécessaire, relisez le chapitre « Linking to Pages » de la documentation
  2. Modifiez votre vue « contact/index.html.twig » afin qu'elle propose à présent des liens vers les détails du contact sur chaque nom de contact
  3. Vérifiez que les liens sont correctement produits et fonctionnels
  4. Améliorez le précédent test fonctionnel pour qu'il vérifie que la liste à puces contient 195 liens
  5. Écrivez un nouveau test permettant de cliquer sur le premier lien de la liste des contacts et de vérifier que la route obtenue est la bonne (sans tenir compte du paramètre de route)

Mise en place de votre base de données, retour aux bonnes pratiques

Dans la partie précédente, vous avez consulté une base de données existante. La démarche habituelle lors de la création d'un projet Symfony est de construire de nouvelles entités liées entre elles par des relations, de gérer des versions des entités et de la base de données et de remplir la base de données avec des données, factices ou non, permettant de faire fonctionner le projet complet directement après clonage. Vous allez donc à présent suivre cette démarche et apprendre à construire des données factices générées aléatoirement à partir de « Faker ».

Création de la base de données

Avant de pouvoir créer la base de données, vous allez devoir modifier la configuration pour accéder à votre propre base de données MySQL.

Travail à réaliser
  1. Ouvrez le fichier de la configuration du projet
  2. Modifiez la configuration base de données pour :
    • Utiliser votre compte MySQL en lieu et place de l'utilisateur « web »
    • Modifiez la base de données utilisée en « votre_login_contact »
    • Mettez la valeur du paramètre « serverVersion » en accord avec le serveur MySQL du département, à savoir « mariadb-10.2.25 »
      Information

      La valeur de « serverVersion » doit impérativement être correcte pour que les migrations fonctionnent. En effet, MySQL et MariaDB n'ont pas la même façon d'accéder aux méta-informations de la base de données.

  3. Lancez la création de la base de données :
    bin/console doctrine:database:create
  4. Sur phpMyAdmin, vérifiez que la base a bien été créée

Création d'une migration de données

Rappelons que vous avez créé une entité « Contact ». Cette entité n'est plus persistante, maintenant que la base de données de démonstration a été débranchée. Vous allez donc reprendre le cours normal de la création du projet en effectuant avec Doctrine la synchronisation entre les entités et la base de données pour que la structure de la base de données soit équivalente à celle de l'ensemble des entités.

Puisque vos entités peuvent évoluer au fil des fonctionnalités développées, il est normal que la base de données évolue de la même façon. Cependant, contrairement à la base de données de développement qui peut être détruite sans danger, la base de données de production doit pouvoir être modifiée sans perte ni corruption de données. C'est l'objectif des migrations qui vont contenir les requêtes permettant de faire évoluer la base de données.

Travail à réaliser
  1. Lancez la construction de la première migration
    bin/console make:migration
  2. Observez le contenu de la migration générée dans le répertoire « migration »

Création du schéma de la base de données

Les migration contiennent l'ensemble des requêtes SQL permettant d'appliquer et de supprimer la mise à jour de la base de données à un instant donné de l'évolution du projet. Doctrine maintient l'état des migrations appliquées sur la base de données dans une table « doctrine_migration_versions ». Ainsi, la comparaison de l'ensemble des fichiers contenus dans le répertoire « migrations » et du contenu de la table « doctrine_migration_versions » permet de connaître les migrations à appliquer pour que la structure de la base de données soit à jour.

Travail à réaliser
  1. Lancez l'exécution des migrations
    bin/console doctrine:migrations:migrate
  2. Sur phpMyAdmin, vérifiez la présence, la structure et le contenu de la table « doctrine_migration_versions »
  3. Vérifiez la présence, la structure et le contenu de la table « contact »

Générateur de données factices

Pour faciliter la démarche de remplissage de la base de données de développement, vous utiliserez les bundles « DoctrineFixturesBundle » et « ZenstruckFoundryBundle ». Le premier permet de gérer la création de données factices (« fixtures ») avec Doctrine. Le second propose de faciliter le processus de création d'une entité en définissant les valeurs par défaut des propriétés d'une nouvelle instance, souvent générées aléatoirement avec « Faker ». Par ailleurs, les méthodes « create*() » des « Factory » génèrent de nouvelles instances mais les font aussi persister automatiquement en base de données.

Travail à réaliser
  1. Utilisez Composer pour demander le bundle « orm-fixtures » pour le développement (ne pas oublier l'option « --dev » !)
  2. Observez les nouveaux fichiers à l'aide de l'onglet « Commit » de PhpStorm
  3. Effectuez un « commit » avec ces fichiers
  4. Utilisez Composer pour demander le bundle « zenstruck/foundry » pour le développement
  5. Observez les nouveaux fichiers à l'aide de l'onglet « Commit » de PhpStorm
  6. Effectuez un « commit » avec ces fichiers
  7. Parcourez la documentation du « DoctrineFixturesBundle »
  8. Lancez la création d'un générateur de données pour les contacts :
    bin/console make:fixtures ContactFixtures
  9. Observez le contenu du fichier généré et supprimez le corps de la méthode « load() » qui sera complété dans quelques instants
    Information

    Le code d'exemple qui est proposé lors de la génération du fichier s'applique lorsque l'on utilise directement Doctrine. Puisque nous utilisons « Foundry » pour nous faciliter la tâche, il est hors de propos.

  10. Effectuez un « commit » avec ce fichier
  11. Lisez le chapitre « Generate » et de la documentation de « Foundry »
  12. Lancez la création d'une nouvelle forge de données :
    bin/console make:factory
  13. Observez le fichier généré
  14. Effectuez un « commit » avec ce fichier
  15. Modifiez la méthode « getDefaults() » pour que :
  16. Puisque certains noms de famille et certains prénoms composés contiennent des espaces incompatibles avec la structure d'une adresse email, il est plus sûr de remplacer tout caractère non alphabétique par un tiret
  17. Vous devez effectuer des traitements identiques sur le nom et sur le prénom de l'utilisateur en vue de construire son mail, proposez une méthode protégée « normalizeName » qui effectue ces traitements et qui sera utilisée par « getDefaults() »
  18. Votre méthode construit un « Transliterator » à chaque appel, ce qui est inutile et inefficace : construisez le « Transliterator » dans le constructeur de la classe, stockez-le dans une propriété d'instance et utilisez-le dans « normalizeName() »
  19. Effectuez un « commit » avec ce fichier
  20. Lisez les exemples du paragraphe « Using your Factory » de la documentation de « Foundry »
  21. Modifiez le générateur de données de « Contact » pour qu'il crée 150 « Contact » en utilisant la forge précédemment construite
    Information

    Vous avez certainement envie de générer les données factices mais il va falloir attendre un peu !

Génération de la base de données et des données factices

A ce stade, vous pourriez générer les données factices en base de données. Malheureusement, si vous avez commis des erreurs dans votre code et tentez de générer un nouveau jeu de données corrigé, elles vont se cumuler avec les anciennes. Pour palier ce problème, vous allez créer un script Composer qui reprend toutes les étapes précédentes pour générer de façon fiable une nouvelle base de données contenant les données factices.

Travail à réaliser
  1. Créez un script Composer « db » qui exécute les étapes suivantes :
    • Destruction forcée de la base de données
      php bin/console doctrine:database:drop --force --if-exists
    • Création de la base de données
      php bin/console doctrine:database:create
    • Application des migrations successives sans questions interactives
      php bin/console doctrine:migrations:migrate --no-interaction
    • Génération des données factices sans questions interactives
      php bin/console doctrine:fixtures:load --no-interaction
  2. Documentez ce script dans le fichier « composer.json »
  3. Documentez ce script dans le fichier « README.md »
  4. Puisque cela n'a pas été fait auparavant, documentez la configuration de la base de données dans « README.md »
  5. Lancez le script Composer « db »
  6. Observez le contenu de la table « contact » dans phpMyAdmin
  7. Réalisez le même type d'observation en utilisant la console, par exemple avec :
    bin/console doctrine:query:sql "SELECT * FROM contact LIMIT 5"
    Information

    Il est essentiel de vérifier la pertinence de toutes les données générées. Il faut vérifier toutes les lignes et toutes les colonnes pour avoir une vision globale de la cohérence de ce qui est produit, aléatoirement ou pas.

  8. En vous inspirant de la documentation de « Faker » dans « Foundry », modifiez la configuration de « Faker » qui qu'il génère des textes en français
  9. Réinitialisez la base de données et contrôlez les données produites qui doivent ressembler à :
    bin/console doctrine:query:sql "SELECT * FROM contact LIMIT 5"
     ---- ----------- ----------- ------------------------------ 
      id   firstname   lastname    email                         
     ---- ----------- ----------- ------------------------------ 
      1    Édith       Gregoire    edith.gregoire@perez.com      
      2    Adélaïde    Gauthier    adelaide.gauthier@dupre.fr    
      3    Maurice     Boulanger   maurice.boulanger@allard.net  
      4    Louis       Caron       louis.caron@marty.org         
      5    Paulette    Besnard     paulette.besnard@rey.org      
     ---- ----------- ----------- ------------------------------ 
    
  10. Vérifiez l'affichage de ces nouvelles données dans votre application

Tests fonctionnels basés sur des données factices

Les tests fonctionnels dépendant de données ont précédemment été réalisés sur une version de démonstration de la base de données de développement. Ce principe est problématique puisqu'il nécessite un serveur de base de données MySQL contenant des données et une connaissance du jeu de données. Habituellement, des données factices sont générées à la volée pour chaque test sur un moteur SQLite qui ne nécessite pas d'infrastructure particulière.

Vous allez donc utiliser « Foundry » dans les tests et pour cela, il faut installer le module « Doctrine2 » de Codeception.

Travail à réaliser
  1. Ajoutez à votre projet la dépendance de développement au module « codeception/module-doctrine2 »
    composer require --dev codeception/module-doctrine2
  2. Activez le module « Doctrine2 » dans la configuration de la suite des tests « Controller » (fichier « tests/Controller.suite.yml »)
  3. Activez l'option « cleanup » pour que chaque test s'exécute dans une transaction pour empêcher les effets de bords
  4. Ajoutez le fichier source de cet utilitaire (« Helper ») à votre projet en le plaçant dans le répertoire « tests/Support/Helper »
  5. Activez le « Helper » qui dépend du module « Symfony » en ajoutant les lignes suivantes à la liste des modules activés dans le fichier « tests/Controller.suite.yml » :
        - \App\Tests\Support\Helper\EntityManagerReset:
            depends: Symfony
    Information

    Ce « Helper » permet de réinitialiser l'EntityManager entre chaque test pour éviter les effets de bords.

  6. Générez les classes « Actor » de Codeception avec le nouveau module activé :
    php vendor/bin/codecept build
  7. Configurez l'accès à une base de données SQLite de test dans « .env.test » en prenant tel quel le modèle proposé dans « .env »
    Information

    Il est classique de configurer une base de données SQLite pour les tests directement dans le fichier « .env.test ».

  8. Supprimez le fichier « .env.test.local »
  9. Supprimez la référence à « .env.test.local » dans « codeception.yml »
  10. Modifiez le script Composer « test:codeception » pour qu'il initialise la base de données de test avant de lancer les tests :
    • Nettoyage du répertoire « _output » et du code généré par Codeception
      php vendor/bin/codecept clean
    • Destruction silencieuse forcée de la base de données
      php bin/console doctrine:database:drop --force --quiet --env=test
    • Création silencieuse de la base de données
      php bin/console doctrine:database:create --quiet --env=test
    • Création silencieuse du schéma de la base de données
      php bin/console doctrine:schema:create --quiet --env=test
    • Exécution des tests Codeception
      php vendor/bin/codecept run
  11. Documentez ce script dans le fichier « composer.json »
  12. Documentez ce script dans le fichier « README.md »
  13. Lancez le script Composer « test:codeception » et constatez qu'il produit des erreurs
  14. Mettez à jour le test de la liste des contacts en le débutant par la création de 5 « Contact » avec « ContactFactory »
  15. Ajoutez un test de clic sur le premier contact de la liste :
    • Créez un « Contact » « Joe Aaaaaaaaaaaaaaa »
    • À l'aide de « ContactFactory », créez 5 « Contact »
    • Cliquez sur le lien « Aaaaaaaaaaaaaaa, Joe »
    • Vérifiez que la réponse est un succès
    • Vérifiez que vous êtes sur la route « app_contact_show » avec les bonnes valeurs de paramètres
  16. Ajoutez un test qui contrôle que les contacts sont correctement triés :
  17. Vérifiez que votre code passe tous les tests

Structuration de l'application

Votre « application » n'en a pour l'instant aucune des caractéristiques classiques. Vous allez utiliser Bootstrap pour vous permettre d'ajouter facilement une barre de navigation et d'autres éléments par la suite. Nous nous attacherons peu au design de l'application pour privilégier les fonctionnalités apportées par votre code et les composants Bootstrap.

Intégration de Bootstrap

La première étape va consister à intégrer Bootstrap à votre application en utilisant les contenus fournis par un « CDN ».

Travail à réaliser
  1. Lisez la documentation sur le démarrage rapide Bootstrap 5
  2. Modifiez votre « template » de base pour y intégrer le CSS et le JavaScript de Bootstrap 5 dans les blocs dédiés
  3. Profitez-en pour définir le « viewport » adapté
  4. Ajoutez la langue du document HTML

Ajout d'une page d'accueil

Votre application sert actuellement les ressources « /contact » et « /contact/{id} » mais pas « / » qui donne une réponse « 404 ». Vous allez ajouter un nouveau contrôleur dont l'unique rôle sera de rediriger de « / » vers « /contact ».

Travail à réaliser
  1. Créez un nouveau contrôleur « HomeController »
  2. Modifiez la route créée par défaut pour qu'elle soit associée au chemin « / »
  3. Remplacez le code de l'action pour effectuer une redirection « 303 » vers la route associée à « /contact »
  4. Supprimez le fichier de la vue et le répertoire « templates/home » qui deviennent inutiles

Ajout d'une barre de navigation

Votre application sera plus facile à utiliser avec une barre de navigation contenant les actions classiques que vous proposerez.

Travail à réaliser
  1. Découvrez comment fonctionne la « Navbar » Bootstrap en lisant la documentation
  2. Copiez l'exemple de la documentation dans votre modèle Twig de base
  3. Conservez les éléments suivants :
    1. le lien « navbar-brand » qui contient le texte « Contacts » et mène à la page d'accueil
    2. une liste « nav-item » qui contiendra les futures actions possibles
    3. le formulaire de recherche que vous franciserez
  4. Vérifiez que la barre de navigation s'affiche correctement, notamment sous forme d'un menu burger en mode mobile

Structuration des contacts et feuille de style

Les contacts sont actuellement listés de façon simple, sans structure du nom et du prénom. Ceci rend impossible toute mise en forme particulière. Vous allez donc ajouter une feuille de style et ajouter de la structure dans la liste des contacts.

Travail à réaliser
  1. Créez une feuille de style accessible par « /css/style.css »
  2. Lisez le chapitre « Linking to CSS, JavaScript and Image Assets »
  3. Modifiez le « template » « base.html.twig » pour y intégrer la feuille de style
  4. Modifiez la vue de listage des contacts pour ajouter un « span » autour du nom et un autour du prénom, auxquels vous associerez respectivement les classes CSS « firstname » et « lastname »
  5. Modifiez la feuille de style pour que la classe « lastname » soit en gras et en petites majuscules

Mise à jour des tests

La barre de navigation contient une liste à puces. Sa présence peut fausser le résultat du sélecteur « ul > li » présent dans les tests car celui-ci est trop peu restrictif. La solution va consister à rendre le système plus testable en augmentant la spécificité de la liste des contacts et en modifiant le sélecteur dans les tests.

Travail à réaliser
  1. Vérifiez que les tests ne passent plus
  2. Ajoutez la classe CSS « contacts » à la liste des contacts
  3. Mettez le sélecteur des tests en accord avec cette modification
  4. Vérifiez que les tests passent de nouveau

Recherche des contacts

La barre de navigation que vous venez d'ajouter comporte un champ de recherche qui doit être rendu fonctionnel. Pour cela, il faut intervenir sur trois aspects : le modèle avec une nouvelle méthode dans le « Repository », le contrôleur pour déclencher la recherche avec le texte reçu et la vue pour proposer l'interface de saisie et afficher le résultat. Vous chercherez donc le texte fourni par l'utilisateur dans le nom et le prénom des contacts.

Recherche dans le modèle

La première étape va consister à rechercher les données au niveau du modèle, donc dans le « ContactRepository ». Un contact est retenu si son nom ou son prénom contient le texte cherché.

Travail à réaliser
  1. Créez une nouvelle méthode « search() » dans le « ContactRepository »
  2. Ajoutez-lui un paramètre représentant le texte cherché dont la valeur par défaut est une chaîne vide ainsi qu'un type de retour « array »
  3. Ajoutez une annotation pour préciser le type de retour « Contact[] »
  4. Lisez la partie « Querying with the Query Builder » la documentation de Doctrine et découvrez les possibilités offertes par le « QueryBuilder »
  5. Utilisez le « QueryBuilder » pour effectuer l'interrogation des données donnant les contacts, triés par nom puis prénom, dont le nom ou le prénom contient le texte cherché

Logique dans le contrôleur

La méthode « index() » contrôleur « ContactController » doit être adapté à la recherche.

Travail à réaliser
  1. Ouvrez le fichier du « ContactController » et placez-vous au niveau de la méthode « index() »
  2. Remplacez l'utilisation de la méthode « findBy() » du « ContactRepository » par la nouvelle méthode « search() »
  3. Vérifiez que l'affichage fonctionne toujours
  4. Demandez la requête en la déclarant dans les paramètres de la méthode
  5. Lisez comment utiliser l'objet « Request »
  6. Récupérez la valeur du paramètre HTTP GET « search » ou une chaîne vide par défaut
  7. Utilisez la valeur précédente comme texte de recherche des contacts
  8. Ajoutez le paramètre HTTP GET « search » dans l'URL de listage des contacts (avec comme valeur « le », par exemple)
  9. Vérifiez que l'affichage est bien restreint à des contacts dont le nom ou le prénom contient « le »

Présentation dans la vue

L'interface de saisie du texte cherché est présente mais elle n'est pas reliée à la recherche effective dans le contrôleur. Il faut mettre la saisie en accord avec le code du contrôleur et peaufiner l'interface utilisateur.

Travail à réaliser
  1. Fixez le nom du champ de saisie pour qu'il corresponde au code du contrôleur
  2. Testez le champ et le bouton de recherche
  3. Consultez l'URL « /contact/12 » puis utilisez le champ de recherche depuis cette URL
  4. Ajoutez une action au formulaire pour que la route « app_contact » soit utilisée
  5. Constatez que la valeur cherchée n'apparaît pas dans le champ de recherche lors de l'affichage des résultats
  6. Dans le contrôleur, passez la valeur de « search » à la vue
  7. Donnez la valeur de « search », ou par défaut une chaîne vide, comme valeur du champ de recherche
  8. Vérifiez le comportement de l'ensemble

Test de la fonctionnalité

La pérennité de cette fonctionnalité passe nécessairement par un test.

Travail à réaliser
  1. Écrivez une nouvelle méthode de tests « search() » dans « tests/Controller/Contact/IndexCest.php »
  2. Utilisez le « ContactFactory » pour ajouter 4 contacts dont un contiendra une séquence de caractère dans le nom et un autre la même séquence dans le prénom
  3. Lancez la recherche de la séquence de caractères
  4. Contrôlez les contacts retenus
  5. Vérifiez que le test passe

Catégories de contacts

La structure des données persistantes est actuellement simpliste. Vous allez ajouter une entité pour les catégories afin qu'un contact appartienne possiblement à une catégorie.

Entité « Category »

Vous allez reproduire les opérations de construction d'une entité en vous aidant du « MakerBundle ».

Travail à réaliser
  1. Utilisez le « MakerBundle » pour créer une nouvelle entité « Category » qui comporte une propriété « name » de type « string » avec une taille maximale de « 30 » et qui ne peut pas être « null » en base de données
  2. Vérifiez le code source de la classe générée

Relations « Contact » / « Category »

En même temps que la création de l'association entre « Contact » et « Category », vous ajouterez une propriété « phone » à l'entité « Contact ».

Travail à réaliser
  1. Utilisez le « MakerBundle » pour modifier l'entité « Contact » existante :
    bin/console make:entity Contact
  2. Ajoutez la propriété « phone » de type « string » avec une taille maximale de « 20 » et qui ne peut pas être « null » en base de données
  3. Ajoutez une propriété « category » de type relation « ManyToOne » qui peut être nulle et qui impliquera une propriété « contacts » dans l'entité « Category »
  4. Observez le code de « Category » ainsi que les modifications du code de « Contact »

Migration

Vous savez que toute modification de la structure de la base de données doit être référencée dans une migration.

Travail à réaliser
  1. Créez une nouvelle migration
  2. Vérifiez le code de la migration
  3. Constatez que l'affichage de la liste des contacts ne fonctionne plus
  4. Appliquez la migration
  5. Constatez que l'affichage de la liste des contacts fonctionne à nouveau

Données factices

Les générateurs de données (« *Fixtures ») et les forges de données (« *Factory ») vont devoir être adaptés aux nouvelles entités.

Travail à réaliser
  1. Tentez de re-générer la base de données en utilisant votre script « Composer »
  2. Mettez à jour la forge de « Contact » pour corriger le problème
  3. Vérifiez que vos contacts ont des numéros de téléphone français valides
  4. Générez la forge pour la classe « Category »
  5. Modifiez la forge pour que le nom de la catégorie soit généré aléatoirement comme un mot dont la première lettre est en majuscules
  6. Créez un générateur de données factices pour la classe « Category »
  7. Téléchargez dans le répertoire « src/DataFixtures/data » le fichier « Category.json » (télécharger) qui contient une série de noms de catégories
  8. Dans le générateur, utilisez les fonctions « file_get_contents() » et « json_decode() » pour lire ce fichier et créer les instances de « Category » correspondantes à l'aide de « CategoryFactory »
    Information

    Vous devez faire référence au fichier « Category.json » depuis le fichier « CategoryFixtures.php ». Vous ne pouvez pas utiliser le répertoire courant « . » qui fait référence au programme en cours d'exécution. Vous devez utiliser la constante magique qui donne le répertoire du fichier dans lequel elle est utilisée.

  9. Générez une nouvelle version de la base de données et constatez l'apparition des catégories
  10. Indiquez les dépendances entre « ContactFixtures » et « CategoryFixtures »
  11. Modifiez le générateur de « Contact » pour qu'il affecte une catégorie aléatoire à 90% des contacts générés
    Information

    La méthode « boolean() » de Faker admet un paramètre de type « integer » qui fixe la probabilité d'obtenir « true » sur 100.

    Puisque le tirage aléatoire de catégorie doit avoir lieu pour chaque entité « Contact » générée, vous allez devoir utiliser une fonction anonyme. Vous pouvez vous appuyer sur l'exemple fourni dans « Using with DoctrineFixturesBundle ».

  12. Exécutez la commande suivante pour connaître le nombre de contacts dans chaque catégorie (dont la catégorie « null ») :
    bin/console doctrine:query:sql "SELECT category.name AS category, COUNT(contact.id) AS contacts \
    FROM contact \
    LEFT JOIN category ON contact.category_id = category.id \
    GROUP BY category.name \
    ORDER BY category.name"
    
  13. Utilisez le générateur « AppFixtures » pour générer une catégorie aléatoire qui ne contiendra aucun contact
  14. Indiquez les dépendances entre « AppFixtures » d'une part et « ContactFixtures » et « CategoryFixtures » d'autre part
    Information

    Vous devez imposer les dépendances pour que la catégorie construite dans « AppFixtures » reste vide. Elle doit donc être construite en dernier.

  15. Exécutez la commande suivante pour connaître le nombre de contacts dans chaque catégorie (dont la catégorie vide) :
    bin/console doctrine:query:sql "SELECT category.name AS category, COUNT(contact.id) AS contacts \
    FROM contact \
    RIGHT JOIN category ON contact.category_id = category.id \
    GROUP BY category.name \
    ORDER BY category.name"
    

Mise à niveau de l'affichage

Le modèle ayant changé, les vues doivent être adaptées.

Travail à réaliser
  1. Modifiez la vue des détails d'un contact pour afficher le numéro de téléphone et le nom de la catégorie
    Information

    Puisque la catégorie peut ne pas être définie, vous devez tester dans la vue si « votre_contact.category » est non nul pour ensuite accéder à « votre_contact.category.name ». L'opérateur « ?? » vous facilite la tâche en vous autorisant l'accès à une valeur non définie sans erreur avec une valeur par défaut dans le cas où la valeur n'est pas définie.

    Trouvez facilement un contact dont la catégorie est vide en utilisant :

    bin/console doctrine:query:sql "SELECT * FROM contact WHERE category_id IS NULL LIMIT 1"
  2. Créez un contrôleur « CategoryController » pour les catégories
  3. Utilisez l'action générée par défaut pour effectuer un listage alphabétique des catégories
  4. Dans la « Navbar », ajoutez un lien « Catégories » qui mène à la liste des catégories

Inclusion de « template » pour uniformiser et factoriser

Vous devez vous attendre à construire une liste de contacts par catégorie. Vous avez déjà réalisé la liste de l'ensemble des contacts qui peut être restreinte par une recherche. Le code Twig va donc être le même pour la liste de contacts d'une catégorie. Il serait particulièrement malvenu de dupliquer le code ! Vous allez donc extraire le code d'affichage de la liste des contacts et l'utiliser à deux endroits différents en l'incluant.

Travail à réaliser
  1. Créez un « template » « templates/contact/_contacts.html.twig » qui contient uniquement le code HTML d'affichage d'une liste de contacts extrait de la vue « templates/contact/index.html.twig »
  2. Modifiez la vue « templates/contact/index.html.twig » pour inclure ce « template »
  3. Vérifiez l'affichage de la liste et lancez les tests
  4. Dans le contrôleur « CategoryController », créez une nouvelle action « show() » qui liste les contacts d'une catégorie :
    • Utilisez un « EntityValueResolver »
    • Passez uniquement l'entité « Category » à la vue « templates/category/show.html.twig »
    • Utilisez le « template » « templates/contact/_contacts.html.twig »
    Information

    Pensez à passer une variable « contacts » en paramètre de votre inclusion de « template ».

Derniers détails

Il reste quelques détails d'affichage à régler.

Travail à réaliser
  1. Consultez la ressource « /category/1 » pour vérifier votre travail
  2. Constatez que les contacts d'une catégorie ne sont pas triés
  3. Ajoutez un critère de tri sur la collection des contacts de la catégorie (nom puis prénom)
  4. Dans la liste de catégories, ajoutez un lien vers les contacts de la catégorie

Requêtes personnalisées avec le « QueryBuilder »

Jusqu'à présent, vous avez utilisé des méthodes de haut niveau pour effectuer des sélections dans la base de données. Votre code est resté éloigné des requêtes SQL même si vous en avez retrouvé la philosophie dans la recherche des contacts par nom et/ou prénom. Dans cette partie, vous allez écrire des requêtes plus complexes et plus efficaces en utilisant le « QueryBuilder ».

Listage des catégories avec le nombre de contacts qu'elles contiennent

Pour obtenir les catégories ainsi que le nombre de contacts qu'elles contiennent, votre requête doit sélectionner les objets « Category » et l'entier donnant le nombre de contacts dans la même requête, pour des raisons évidentes d'efficacité. Vous devez donc créer une nouvelle méthode dans le « CategoryRepository ».

Travail à réaliser
  1. Dans l'action « index() » du « CategoryController », modifiez l'utilisation du « CategoryRepository » pour, dans un premier temps, sélectionnez l'ensemble des catégories triées par ordre alphabétique, en construisant une requête avec le « QueryBuilder »
  2. Vérifiez que vous pouvez toujours afficher la liste des catégories
  3. Effectuez une jointure externe sur les contacts avec « leftJoin() »
  4. Groupez par catégorie en utilisant « groupBy() »
  5. Vérifiez que votre vue fonctionne toujours
  6. Sélectionnez le total des contacts par catégorie avec « addSelect('COUNT(alias_utilisé_dans_leftjoin) as count') »
  7. Constatez que votre vue ne fonctionne plus
  8. Dans le contrôleur, utilisez la fonction « dump() » pour pouvoir explorer le contenu de la collection dans la « WDT » : Effet de la fonction dump() sur la WDT
    Information

    Vous pouvez obtenir le même résultat en utilisant la fonction « dump() » de Twig dans une structure « {% %} ».

  9. Observez les clés du tableau contenu dans la première case de la collection permettant d'accéder aux valeurs sélectionnées : un entier pour l'entité « Category » et la chaîne « count » pour le nombre de contacts
  10. Renommez la sélection de l'entité « Category » avec « select('c as category') »
  11. Observez de nouveau la collection dans la « WDT » : pour chaque ligne, l'entité Category est accessible par la clé « category » et le décompte des contacts par la clé « count »
  12. Ajustez le code de la vue pour afficher correctement les catégories et leur nombre de contacts dans un badge Bootstrap
    Information

    Prenez soin de renommer les variables Twig qui portent les entités « Category » et le décompte du nombre de contacts.

    Pensez à supprimer l'appel à la fonction « dump() » .

  13. Créez une méthode « findAllAlphabeticallyWithContactCount() » dans le « CategoryRepository » pour y déporter la requête que vous avez élaborée

Conséquence du chargement paresseux (« lazy loading ») de l'ORM

Lors de l'affichage des détails d'un contact, vous affichez ses informations propres ainsi que le nom de la catégorie du contact qui est issue de la relation avec l'entité « Category ». Le chargement paresseux (« lazy loading ») implique donc que la demande de la catégorie va engendrer une nouvelle requête. Afin d'optimiser les accès base de données, vous allez charger l'entité « Category » au même moment que le chargement de l'entité « Contact ».

Travail à réaliser
  1. Consultez la fiche d'un contact
  2. Constatez à l'aide de la « WDT » que deux requêtes base de données sont nécessaires à l'affichage de la page : une pour récupérer l'entité « Contact », l'autre pour récupérer l'entité « Category » associée
  3. Créez une méthode « findWithCategory(int $id): ?Contact » dans le « ContactRepository »
  4. Utilisez le « QueryBuilder » pour effectuer la requête de sélection du contact dont l'identifiant est demandé ainsi que de la catégorie associée :
    • Effectuez la jointure externe sur l'entité « Category » avec « leftJoin() »
    • Sélectionnez l'entité « Category » à l'aide de « addSelect() »
    • Ajoutez la restriction sur l'identifiant de l'entité « Contact » concernée avec « where() »
    • Fixez la valeur de l'identifiant de l'entité « Contact » avec « setParameter() »
  5. Lisez la documentation « Automatically Fetching Objects (EntityValueResolver) », en particulier le point sur l'utilisation d'une expression avec l'attribut PHP « MapEntity »
  6. Associez l'attribut PHP « MapEntity » au paramètre « Contact » dans l'action du contrôleur pour utiliser la méthode « findWithCategory() » du « ContactRepository » lors de la conversion du paramètre
  7. Vérifiez qu'une seule requête base de données est effectuée pour l'affichage des détails du contact

Autre conséquence du chargement paresseux (« lazy loading ») de l'ORM

Le même problème de nombre de requêtes trop important va survenir lorsque vous allez afficher la catégorie de chaque contact dans la liste des contacts.

Travail à réaliser
  1. Consultez la liste des contacts
  2. Ajoutez la catégorie des contacts sous forme d'un badge Bootstrap dans « templates/contact/_contacts.html.twig »
  3. Faîtes en sorte qu'un clic sur le badge conduise à la liste des contacts de la catégorie
    Information

    Utilisez la classe CSS Bootstrap « nav-link » pour le lien contenu dans le badge.

  4. Testez en Twig la présence de la catégorie du contact en utilisant « is not null »
  5. Constatez à l'aide de la « WDT » que sept requêtes base de données sont nécessaires à l'affichage de la page :
    • une pour récupérer l'ensemble des entités « Contact »
    • six qui sont la même requête avec un paramètre différent pour récupérer chacune des entités « Category » associées
  6. Observez le temps d'exécution global et celui de l'ensemble des requêtes (rafraichissez la page plusieurs fois pour estimer des valeurs moyennes)
  7. Modifiez la méthode « search() » dans le « ContactRepository » pour effectuer la jointure externe sur l'entité « Category » et l'ajouter à la sélection
  8. Constatez à l'aide de la « WDT » qu'une seule requête base de données est désormais nécessaires à l'affichage de la page
  9. Observez le temps d'exécution global et celui de l'ensemble des requêtes (rafraichissez la page plusieurs fois pour estimer des valeurs moyennes) et constatez l'amélioration
  10. Consultez la liste des contacts d'une catégorie et constatez que l'affichage de la catégorie pour chaque contact est inutile
  11. Dans « templates/contact/_contacts.html.twig », combinez le test « show_category??true and » pour vérifier s'il faut afficher la catégorie
  12. Passez la valeur « show_category: false » lors de l'inclusion dans « templates/category/show.html.twig »
  13. Vérifiez le bon fonctionnement de l'ensemble
  14. Vérifiez que les tests passent toujours

Utilisation de formulaires

Vous allez à présent proposer une interface de modification du contenu de la base de données. Pour cela, vous utiliserez des formulaires. Ces derniers seront construits sur un objet donné dont ils permettront la présentation sous forme de champs de saisie mais contiendront également les fonctionnalités de contrôle de cohérence.

Vous allez donc rendre possible l'édition d'objets « Contact » en proposant de les modifier, de les créer puis de les supprimer.

Préparation du contrôleur

Les chemins associés aux actions du CRUD seront :

  • « /contact/create » pour la création (Create)
  • « /contact/{id} » pour la lecture (Read)
  • « /contact/{id}/update » pour l'édition (Update)
  • « /contact/{id}/delete » pour la suppression (Delete)

Vous avez déjà réalisé l'action de lecture. Il vous reste à créer les actions associées à la création, l'édition et la suppression. Vous allez donc ajouter les actions nécessaires au contrôleur « ContactController ».

Travail à réaliser
  1. Ajoutez trois actions à votre contrôleur :
    • « update » qui demande un paramètre de type entité « Contact »
    • « create » sans paramètre
    • « delete » qui demande un paramètre de type entité « Contact »
  2. Ajoutez une condition requise sur « id » dans les routes conduisant à une conversion de paramètre pour qu'il prenne la forme d'un nombre
  3. Créez les « templates » associés aux précédentes actions dans lesquels vous afficherez dans le titre de la page et dans un titre de niveau 1 :
    • « Édition de Nom, prénom »
    • « Suppression de Nom, prénom »
    • « Création d'un nouveau contact »
    Information

    Puisque vous devez mettre la même valeur dans le titre de la page et dans le titre de niveau 1, il est judicieux de ne pas dupliquer les opérations. Vous êtes donc invité à utiliser une variable dans laquelle vous pouvez stocker une valeur construite en utilisant l'interpolation de variable Twig.

  4. Vérifiez que les nouvelles routes sont bien prises en compte :
    bin/console debug:router --show-controllers --env=prod
  5. Pour valider votre travail, ajoutez les « Cests » suivants à la suite de tests « Controller\\Contact » :

Interface de modification d'un « Contact » existant

Avant de généraliser l'utilisation d'un formulaire pour la création et l'édition d'un « Contact », vous allez vous concentrer sur l'édition dans l'action « update() ».

Travail à réaliser
  1. Générez la classe de formulaire « ContactType » associée à l'entité « Contact » :
    bin/console make:form ContactType Contact
  2. Consultez le fichier généré
  3. Construisez un formulaire « ContactType » dont les données proviennent de l'entité « Contact » que vous récupérez en paramètre de votre action
  4. Transmettez le formulaire à votre « template » Twig et affichez-le
    Information
  5. Essayez votre formulaire et constatez le message d'erreur
    Object of class App\Entity\Category could not be converted to string
  6. Complétez la définition du champ « category » en précisant la propriété de l'entité « Category » qui doit être utilisée pour présenter les données à l'utilisateur :
    …
    ->add('category', EntityType::class, [
        'class' => Category::class,
        'choice_label' => 'name',
    ])
    …
    
  7. Essayez de nouveau votre formulaire et constatez qu'il fonctionne mais que le contenu de la liste déroulante n'est pas trié par ordre alphabétique
  8. Lisez le chapitre « Using a Custom Query for the Entities » de la documentation de « EntityType Field »
  9. Modifiez votre champ de formulaire « category » pour qu'il récupère les données triées :
    …
    ->add('category', EntityType::class, [
        'class' => Category::class,
        'choice_label' => 'name',
        'query_builder' => function (EntityRepository $entityRepository) {
            return $entityRepository->createQueryBuilder('c')
                ->orderBy('c.name', 'ASC');
        },
    ])
    …
    
  10. Essayez de nouveau votre formulaire et constatez qu'il fonctionne et que le contenu de la liste déroulante est trié par ordre alphabétique, mais qu'il n'est pas possible de sélectionner la catégorie « null »
  11. Lisez le chapitre sur les bonnes pratiques concernant les boutons d'envoi de formulaires (une description plus détailée est présente dans une version plus ancienne de la documentation)
  12. Remplacez le rendu Twig de votre formulaire avec « form() » par une décomposition du formulaire utilisant « form_start() », un seul appel de « form_widget() » pour les champs et « form_end() », comme préconisé
  13. Ajoutez un bouton d'envoi ayant pour texte « Modifier », en respectant les bonnes pratiques
  14. Profitez de l'occasion pour ajouter la classe « container-lg » à « <body> » et étirer le bouton sur toute la largeur

Présentation des formulaires

Le formulaire précédemment produit est fonctionnel mais visuellement minimaliste. Twig peut rapidement vous apporter la présentation de Bootstrap.

Travail à réaliser
  1. Lisez la documentation sur le thème Bootstrap 5 pour les formulaires
  2. Ajoutez le thème Bootstrap 5 pour les formulaires au fichier de configuration de Twig
  3. Essayez votre modification
  4. Associez au bouton les classes CSS permettant de respecter le style Boostrap

Validation des données

Le formulaire personnalisé que vous venez de produire comporte un certain nombre de contrôles comme l'obligation de présence des diverses valeurs. Il est possible de renforcer les contrôles pour avoir la certitude que les données fournies par l'utilisateur seront toujours valides.

Travail à réaliser
  1. Dans « ContactType », modifiez les types des champs :
  2. Dans « ContactType », modifiez les options de « category » :
  3. Dans « ContactType », ajoutez l'option « 'empty_data' => '' » pour toutes les propriétés de type « string » : « firstname », « lastname », « email » et « phone »
    Information

    Cette dernière option permet de contourner une erreur qui survient lorsqu'un champ de saisie peut admettre un simple espace qui sera nettoyé (voir « trim ») à la réception des données. Le champ est alors « null » et cela provoque une erreur dans « handleRequest() ».

  4. Lisez le chapitre « Validating Forms » puis parcourez la liste des possibilités dans « Validation Constraints Reference »
  5. Importez les contraintes dans l'entité « Contact » :
    use Symfony\Component\Validator\Constraints as Assert;
  6. Ajoutez sous forme d'attributs PHP les contraintes sur les propriétés :
    • « NotBlank », « Length » max appropriée pour « firstname »
    • « NotBlank », « Length » max appropriée pour « lastname »
    • « NotBlank », « Length » max appropriée, « Email » pour « email »
    • « NotBlank », « Length » max appropriée, « Regex(pattern: '/^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4})$/', message: 'Format de téléphone invalide') » pour « phone »

Persistance des modifications d'un « Contact » existant

Maintenant que vous disposez de l'interface d'édition d'un « Contact », vous allez modifier votre contrôleur afin qu'il permette de faire persister les modifications soumises par l'utilisateur. Une manière classique d'organiser le code dans le contrôleur consiste à utiliser une seule action pour construire le formulaire et faire persister les données. Le formulaire est affiché lorsque l'action est déclenchée par une requête dont la méthode est GET et les données sont enregistrées lorsque la méthode est POST.

Travail à réaliser
  1. Dans l'action du contrôleur, récupérez la requête en la déclarant comme paramètre de votre action
  2. Liez les données soumises par l'utilisateur au formulaire
  3. Vérifiez si le formulaire est posté et s'il est valide
  4. Dans le cas où le test précédent est valide, enregistrez les données
    Information

    Comme indiqué dans la documentation, il est inutile de demander à faire persister l'objet puisqu'il est issu de la base de données et donc surveillé par Doctrine. Il suffit alors d'appeler « flush() ».

  5. Pour finir, Redirigez l'utilisateur vers l'affichage du contact qui vient d'être modifié
  6. Si le formulaire n'est pas posté ou que les données ne sont pas valides, produisez le rendu du formulaire (le code de cette partie est déjà écrit)
  7. Essayez ces nouvelles fonctionnalités
  8. Observez les échanges HTTP dans la console de développement de votre navigateur lors de la mise à jour d'un contact : méthode HTTP, données transmises au serveur, structure des données transmises, code de réponse HTTP et redirection

Interface de création d'un « Contact »

Passez à la création d'un « Contact ». La démarche est proche de celle adoptée pour la modification, à la différence près qu'elle débute avec une nouvelle entité « Contact » plutôt qu'avec une entité issue de la base de données.

Travail à réaliser
  1. Dans l'action « create() », créez une entité « Contact »
  2. Construisez un formulaire ContactType sur le Contact que vous venez de créer
  3. Transmettez la le formulaire à votre « template » Twig et affichez-le de la même façon que pour la modification
  4. Ajoutez un bouton d'envoi ayant pour texte « Ajouter », en respectant les bonnes pratiques
  5. Donnez au bouton les classes CSS permettant de respecter le style Boostrap
  6. Essayez votre formulaire et constatez qu'il s'affiche
  7. Essayez d'envoyer votre formulaire sans saisir de nom de contact et constatez le blocage de l'envoi et l'affichage du message d'erreur au niveau du champ « firstname »
  8. Ajoutez les contrôles et le code nécessaire à l'enregistrement du nouveau contact
  9. Vérifiez le bon affichage du nouveau contact

Personnalisation des formulaires en Twig

Les intitulés de champs de saisie sont le nom des propriétés des instances de « Contact ». Une personnalisation et une francisation sont nécessaires. Elles doivent intervenir dans la vue puisqu'elles relèvent de la présentation des données.

Travail à réaliser
  1. Ouvrez votre fichier de vue « contact/create.html.twig »
  2. Remplacez le rendu Twig de l'ensemble des champs de formulaire avec « form_widget() » par une décomposition champs par champ utilisant « form_row() », ce qui facilitera la personnalisation et la séparation entre la logique du formulaire et sa présentation à l'utilisateur
  3. Ajoutez en Twig un « label » pour chaque champ du formulaire au niveau des « form_row() »
  4. Déplacez la définition du « placeholder » de la catégorie du formulaire « ContactType » à la vue Twig
  5. Essayez votre nouvelle formulation de la création d'un contact

Inclusion de template Twig

La personnalisation que vous venez d'apporter au formulaire de création d'un contact devrait être reproduite dans celui d'édition d'un contact. La duplication de code engendrée par la duplication de ces formulaires dans les vues Twig serait une solution rapide et facile à laquelle vous ne cèderez pas ! Vous allez factoriser le code Twig du formulaire en le transformant en un sous-modèle que vous inclurez.

Travail à réaliser
  1. Créez un nouveau « template » « contact/_form.html.twig » vide
  2. Déplacez le code Twig concernant votre formulaire de « contact/create.html.twig » vers « contact/_form.html.twig »
  3. Incluez le « template » du formulaire dans « contact/create.html.twig »
  4. Vérifiez le fonctionnement de cette vue remaniée
  5. Remplacez le texte du bouton d'envoi dans le modèle du formulaire par une variable « submit_label » dont la valeur sera fournie lors de l'inclusion du modèle de formulaire dans « contact/create.html.twig »
  6. Vérifiez que le fonctionnement est toujours correct puisque le modèle inclus a accès aux variables du contexte actif (ceci concerne la variable « form » définie dans « contact/create.html.twig » et utilisée dans « contact/_form.html.twig »)
  7. Ajoutez le paramètre « with_context = false » à votre instruction d'inclusion afin d'interdire le partage de variables du contexte local et ainsi minimiser les effets de bord possibles
  8. Constatez que la vue ne fonctionne plus car la variable Twig « form » n'est pas définie
  9. Passez explicitement votre formulaire en paramètre de l'inclusion
  10. Essayez votre nouvelle formulation de création d'un contact
  11. Appliquez la démarche d'inclusion du modèle de formulaire à la vue d'édition d'un contact, en pensant à modifier le texte du bouton d'envoi
  12. Essayez votre nouvelle formulation de l'édition d'un contact

Interface de suppression d'un « Contact »

Passez à l'interface de suppression d'un Contact. L'action de suppression pourrait se réaliser sans interface puisqu'il suffit que la ressource « /contact/{id} » supprime le contact. Vous allez néanmoins construire une vue demandant une confirmation. Cela vous permettra de découvrir le fonctionnement d'un formulaire avec plusieurs boutons d'envoi.

Travail à réaliser
  1. Lisez le chapitre « How to Submit a Form with Multiple Buttons »
  2. Dans l'action « delete() », construisez à partir du « FormBuilder » un formulaire comportant deux boutons d'envoi, « delete » et « cancel »
  3. Transmettez ce formulaire à la vue et affichez-le en prenant soin de séparer les deux boutons en utilisant une nouvelle fois « form_start() », « form_row() » et « form_end() »
  4. Modifiez en Twig le « label » des boutons pour afficher « Supprimer » et « Annuler »
  5. Pour distinguer visuellement les deux boutons, modifiez en Twig le « attr » de chacun d'eux pour affecter respectivement la classe CSS « btn btn-primary » et « btn btn-secondary »
  6. Sur le même principe, le formulaire recevra la classe CSS « d-flex » pour en faire un conteneur flex dans lequel les boutons sont régulièrement espacés
  7. Dans le contrôleur :
    1. Si le formulaire est soumis et que c'est par le bouton « delete », supprimez le contact et redirigez vers la liste des contacts
    2. Si le formulaire est soumis et que ce n'est pas par le bouton « delete », redirigez vers la fiche du contact
    3. Si le formulaire n'est pas soumis effectuez le rendu du formulaire
    Information

    Le fonctionnement attendu présenté ci-dessus donne l'idée générale du traitement à réaliser. À vous de l'interpréter pour en faire un algorithme clair et efficace.

Liens depuis la liste des contacts et derniers réglages

Les nouvelles fonctionnalités d'ajout, d'édition et de suppression devraient être accessibles depuis la liste des contacts ou depuis le menu. Afin d'obtenir une interface avec peu de texte, vous ajouterez des liens matérialisés par des symboles Material.

Travail à réaliser
  1. Ajoutez la feuille de style des « Google Fonts » à votre modèle Twig de base
  2. Lisez la partie de la documentation décrivant comment utiliser les icônes en HTML
  3. Dans la vue des contacts « templates/contact/_contacts.html.twig », ajoutez devant le nom de chaque contact :
    1. l'icône « edit » comme lien vers l'édition du contact
    2. l'icône « delete » comme lien vers la suppression du contact
  4. Complétez votre design en affectant respectivement les classes CSS Bootstrap « text-warning » et « text-danger » aux deux précédentes icônes
  5. Ajoutez une entrée de menu « Ajouter un contact » dans la « Navbar »
  6. Constatez que ces nouveaux liens font échouer les tests.
  7. Affectez la classe CSS « contact » au lien autour du « nom, prénom » du contact dans « templates/contact/_contacts.html.twig »
  8. Mettez à jour les sélecteurs dans les tests

Utilisateurs, authentification et autorisations

La partie précédente a permis de mettre en place la modification des contacts. Il convient à présent de sécuriser ces modifications en introduisant des utilisateurs ainsi que les droits d'accès associés.

Création de l'entité « User »

Symfony, via le « MakerBundle », assiste le développeur dans la création d'une entité dédiée à la gestion des utilisateurs.

Travail à réaliser
  1. Lisez le chapitre « Create your User Class »
  2. Effectuez impérativement un « commit » si ce n'était pas fait
  3. Créez une entité « User » en utilisant les valeurs par défaut :
    bin/console make:user
    
     The name of the security user class (e.g. User) [User]:
     > 
    
     Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
     > 
    
     Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
     > 
    
     Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).
    
     Does this app need to hash/check user passwords? (yes/no) [yes]:
     > 
    
     created: src/Entity/User.php
     created: src/Repository/UserRepository.php
     updated: src/Entity/User.php
     updated: config/packages/security.yaml
    
               
      Success! 
               
    
     Next Steps:
       - Review your new App\Entity\User class.
       - Use make:entity to add more fields to your User entity and then run make:migration.
       - Create a way to authenticate! See https://symfony.com/doc/current/security.html
    
  4. Observez les fichiers créés
  5. Observez les modifications apportées au fichier « security.yaml » avec le contrôle de version de PhpStorm
  6. Modifiez l'entité « User » à l'aide du « MakerBundle » pour lui ajouter les propriétés :
    • « firstname » de type « string » de taille « 100 » non « nullable »
    • « lastname » de type « string » de taille « 150 » non « nullable »
  7. Créez une migration avec la commande :
    bin/console make:migration
  8. Observez le fichier « src/Migrations/Version… » créé par le « MakerBundle »
  9. Effectuez la migration avec :
    bin/console doctrine:migrations:migrate
  10. Observez les principales modifications apportées à votre base de données dans phpMyAdmin

Authentification

Les utilisateurs ne peuvent actuellement pas s'authentifier. Vous allez à présent configurer la mécanique et construire les pages de connexion et déconnexion pour les utilisateurs.

Travail à réaliser
  1. Effectuez impérativement un « commit » si ce n'était pas fait
  2. Lisez les grandes étapes du chapitre « How to Build a Login Form » qui donne les grandes lignes de ce qui sera réalisé par le « MakerBundle »
  3. Générez le formulaire d'authentification en appliquant les mêmes valeurs que dans l'exemple suivant :
    bin/console make:auth
    
     What style of authentication do you want? [Empty authenticator]:
      [0] Empty authenticator
      [1] Login form authenticator
     > 1
    
     The class name of the authenticator to create (e.g. AppCustomAuthenticator):
     > LoginFormAuthenticator
    
     Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
     > 
    
     Do you want to generate a '/logout' URL? (yes/no) [yes]:
     > 
    
     Do you want to support remember me? (yes/no) [yes]:
     > no
    
     created: src/Security/LoginFormAuthenticator.php
     updated: config/packages/security.yaml
     created: src/Controller/SecurityController.php
     created: templates/security/login.html.twig
    
               
      Success! 
               
    
     Next:
     - Customize your new authenticator.
     - Finish the redirect "TODO" in the App\Security\LoginFormAuthenticator::onAuthenticationSuccess() method.
     - Review & adapt the login template: templates/security/login.html.twig.
    
  4. Observez les fichiers créés et modifiés tout en faisant le lien avec la documentation que vous venez de survoler
    Remarque importante

    Tous les fichiers générés sont indispensables au bon fonctionnement de l'authentification. Ajoutez-les à votre dépôt !

  5. Consultez la page de connexion « /login » et constatez qu'elle s'affiche correctement
  6. Modifier la vue associée pour traduire les textes en français
  7. Modifiez la taille du bouton de connexion pour l'étirer sur toute la largeur de son conteneur et appliquez-lui une marge supérieure
  8. Tentez une connexion qui échouera forcément afin de constater que le message d'erreur est en anglais
  9. Configurez la langue par défaut à « fr » dans « config/packages/translation.yaml »

« Fixtures » et rôles

Vous possédez à présent une gestion des entités « User » mais aucun utilisateur n'a été créé. Vous allez le faire par le biais de « fixtures » générées grâce à une forge « Foundry ». Elle va demander un peu d'attention dans la mesure où les mots de passe que vous allez injecter dans l'entité sont fournis en clair alors qu'ils doivent être encodés avant d'être stockés. L'encodage sera automatisé par la définition de la méthode « initialize() » du « UserFactory ».

Travail à réaliser
  1. Lancez la création d'une nouvelle forge de données :
    bin/console make:factory
  2. Lisez le chapitre « Factories as Services »
  3. Recopiez tous les éléments de l'exemple, sauf le contenu de la méthode « getDefaults() »
    Information

    Nous passons très rapidement sur le processus de configuration du hachage des mots de passe dans la forge. Ce point sera revu plus en détails au semestre 4.

  4. Dans la méthode « getDefaults() », fixez les valeurs du « Contact » :
  5. Créez une nouvelle classe de « fixtures » « UserFixtures » :
    bin/console make:fixtures UserFixtures
  6. Observez les fichiers créés
  7. Selon les modalités décrites dans la documentation, modifiez la classe « UserFixtures » afin qu'elle ajoute les utilisateurs :
    • « Tony Stark » dont l'email est « root@example.com » et dont le rôle est « 'ROLE_ADMIN' »
    • « Peter Parker » dont l'email est « user@example.com » et dont le rôle est « 'ROLE_USER' »
    • 10 utilisateurs aléatoires
  8. Générez une nouvelle version de la base de données :
    composer db
  9. Observez le contenu de la table « User »
  10. Mettez à jour la documentation du projet pour fournir les identifiants et mot de passe des utilisateurs de démonstration

Connexion et déconnexion

Tout est maintenant prêt pour vérifier la connexion et la déconnexion des utilisateurs.

Travail à réaliser
  1. Accédez à la ressource « /login »
  2. Tentez de vous connecter avec l'utilisateur « root@example.com » dont le mot de passe est « test »
    Information

    Puisque vous allez souvent vous connecter et vous déconnecter de l'application, mémorisez l'identifiant et le mot de passe dans votre navigateur.

  3. Constatez le message d'erreur :
    TODO: provide a valid redirect inside …/src/Security/LoginFormAuthenticator.php
  4. Rendez-vous dans le fichier concerné et effectuez une redirection vers la page d'accueil de l'application
  5. Déconnectez-vous en accédant à la ressource « /logout »
  6. Tentez de nouveau de vous connecter avec l'utilisateur « root@example.com » dont le mot de passe est « test »
  7. Vérifiez que la redirection s'est bien produite
  8. Observez l'information d'identification de l'utilisateur dans la « WDT »

Autorisations dans les contrôleurs et les vues

Le modèle authentification / autorisation fonctionne mais manque d'ergonomie. Vous allez modifier certaines vues pour ajouter un bandeau de connexion / déconnexion et restreindre l'affichage aux utilisateurs en fonction de leur rôle.

Travail à réaliser
  1. En vous inspirant de la fin du chapitre concernant les rôles et à l'aide d'un attribut PHP, autorisez l'accès à l'ensemble du contrôleur « CategoryController » uniquement aux utilisateurs connectés (voir rôle « IS_AUTHENTICATED_FULLY » ou « IS_AUTHENTICATED_REMEMBERED »)
  2. Déconnectez-vous en accédant à la ressource « /logout »
  3. Essayez d'accéder à la liste des catégories et constatez que vous êtes invité à vous connecter
  4. Modifiez le « template » de base en y ajoutant un couple de boutons pour la connexion ou la déconnexion selon que l'utilisateur est connecté ou non
    Information

    Bootstrap permet de donner un « style bouton » à des liens.

  5. Associez respectivement les routes de connexion et de déconnexion aux deux précédents boutons
  6. Essayez les boutons et vérifiez le nouvel affichage lorsqu'un utilisateur est connecté ou non connecté
  7. Lisez la documentation concernant l'utilisateur dans le chapitre « The App Global Variable »
  8. Modifiez le texte du bouton de déconnexion pour y ajouter le prénom de l'utilisateur
  9. Vérifiez si un utilisateur est administrateur pour afficher les liens d'édition et de suppression de contact dans la vue « templates/contact/_contacts.html.twig »
  10. Vérifiez si un utilisateur est connecté pour afficher le lien de création d'un contact dans la « NavBar »
    Remarque importante

    Masquer les liens vers les ressources ne limite en rien l'accès aux ressources. Il faut également limiter l'accès aux actions !

  11. Limitez l'accès aux actions concernées

Mise à jour des tests

La restriction d'accès à la création, la modification et la suppression d'un contact fait échouer les tests. Il convient de les mettre à jour.

Travail à réaliser
  1. Dans le test « form() » de « UpdateCest » :
  2. Ajoutez un test « accessIsRestrictedToAuthenticatedUsers() » dont l'objectif sera de vérifier que l'accès est bien restreint aux utilisateurs connectés :
    • Créez un contact
    • Accédez à la ressource permettant de le modifier
    • Faites l'assertion que vous êtes sur la route de connexion
    • Vérifiez que le test passe
  3. Ajoutez un test « accessIsRestrictedToAdminUsers() » dont l'objectif sera de vérifier que l'accès est bien restreint aux administrateurs :
    • Créez un contact
    • Créez un utilisateur ayant le role utilisateur
    • Connectez l'utilisateur créé
    • Accédez à la ressource permettant de le modifier
    • Faites l'assertion que la réponse HTTP est « FORBIDDEN »
    • Vérifiez que le test passe
  4. Effectuez les mêmes modifications pour « DeleteCest »
  5. Ajustez le test de création de contact et complétez avec un test de contrôle d'accès
  6. Vérifiez que l'ensemble des tests passe
    Information

    Il faudrait ajouter des tests pour valider le fonctionnement des divers formulaires mais nous n'en avons pas le temps…

Gestion de l'application : « back-office » avec EasyAdmin

Dans un site ou une application Web, le « back-office », littéralement l'arrière-boutique, représente toute la partie non visible par les utilisateurs dédiée à la gestion par les administrateurs. Vous pouvez développer entièrement ce « back-office » mais, puisqu'il consiste principalement en une multitude de « CRUD », c'est une tâche très ingrate qui est en partie automatisable. EasyAdmin est un « bundle » Symfony spécialement conçu pour construire la partie administration d'une application Symfony.

Installation de EasyAdmin

EasyAdmin est évidemment accessible par Composer. Vous allez donc en faire une dépendance du projet puis générer le tableau de bord.

Travail à réaliser
  1. Installez EasyAdmin dans votre projet :
    composer require admin
  2. Observez les modifications apportées au projet
  3. Créez le tableau de bord (« dashboard ») :
    php bin/console make:admin:dashboard
  4. Observez les modifications apportées au projet
  5. Accédez à la ressource « /admin »

Page d'accueil du « back-office »

Vous allez proposer un ensemble de fonctionnalités de gestion aux administrateurs de votre application. Elles seront regroupées dans la page d'accueil du « back-office ».

Travail à réaliser
  1. Lisez le code d'exemple en commentaire dans l'action « index() » du « DashboardController »
  2. Dans le tutoriel « SymfonyCasts » dédié à EasyAdmin, lisez la note concernant la page d'accueil depuis la version 4.0.3
  3. Supprimez les commentaires
  4. Remplacez l'instruction :
    return parent::index();
    par :
    return $this->render('admin/index.html.twig');
  5. Générez le fichier Twig à l'aide de PhpStorm
  6. Insérez le code suivant dans le « template » :
    {% extends '@EasyAdmin/page/content.html.twig' %}
  7. Vérifiez l'apparition du squelette du « back-office » lorsque vous accédez à la ressource « /admin »

« CRUD » pour les entités

Le squelette du back-office est fonctionnel mais vide. Vous devez donc y ajouter le « CRUD » de chacun de vos entités. Ceci va consister en la génération, par le « MakerBundle » d'un contrôleur dédié par entité.

Travail à réaliser
  1. Lisez l'introduction de « CRUD Controllers »
  2. Lancez la création d'un contrôleur pour l'entité « Category » :
    bin/console make:admin:crud
  3. Observez le fichier produit
  4. Sur le modèle du commentaire présent dans la méthode « configureMenuItems() » du « DashboardController », ajoutez un lien vers le « CRUD » de « Category »
    Information

    Le mot-clé « yield » permet de construire un générateur. Chaque appel à « yield » se comporte comme un « return » qui n'arrêterait pas l'exécution de la fonction (ou méthode) génératrice.

    Pour compléter l'exemple de la documentation officielle de PHP, le programme suivant : produit comme résultat :

    generate 1
    1
    generate 2
    2
    generate 3
    3
    

    Si vous n'êtes pas à l'aise avec ce principe, vous pouvez retourner un tableau, de la même façon que dans la documentation .

  5. Constatez l'apparition de nouvelles fonctionnalités en consultant la ressource « /admin »
  6. Lancez la création d'un contrôleur pour les entités « Contact » et « User » :
  7. Ajoutez deux nouvelles entrées au menu du « back-office »
  8. Constatez l'apparition des nouvelles fonctionnalités

Autorisations dans le « back-office »

Si vous n'êtes actuellement pas connecté, vous pouvez accéder au « back-office » et agir sur l'ensemble de la base de données. Cette situation n'est pas normale et doit être corrigée.

Travail à réaliser
  1. Lisez le chapitre « Restrict Access to the Entire Backend »
  2. Limitez l'accès au « back-office » en modifiant la configuration générale de sécurité de Symfony dans le fichier « config/packages/security.yaml »
  3. Vérifiez que la restriction d'accès est effective

Configuration du « CRUD » de « Contact »

L'interface générée par défaut par EasyAdmin ne reflète pas les relations. Vous devez les configurer manuellement.

Travail à réaliser
  1. En partant de « /admin », accédez à la liste des contacts
  2. Constatez l'absence de la catégorie de chaque contact
  3. Éditez à présent un contact
  4. Constatez l'absence de la catégorie du contact
  5. Lisez le paragraphe « Configuring the Fields to Display »
  6. Configurez les champs pour l'entité « Contact » en trouvant le type le plus approprié dans documentation
  7. Si vous avez configuré le champ « category », vous devriez constater l'erreur suivante :
    Object of class Proxies\__CG__\App\Entity\Category could not be converted to string
  8. Puisque vous avez déjà rencontré ce type d'erreur dans un formulaire, utilisez la même option « choice_label » que vous appliquerez à l'aide de la méthode « setFormTypeOption() »
  9. Vérifiez que la liste de choix des catégories est présente dans le formulaire d'édition du contact
  10. Constatez que la liste de choix des catégories n'est pas triée
  11. Réutilisez encore une fois vos reconnaissances passées pour appliquer un tri sur la source de données
  12. Vérifiez que la liste de choix des catégories est correcte
  13. Constatez que l'identifiant du contact est présent dans le formulaire et peut donc être modifié
  14. Lisez le début du paragraphe « Displaying Different Fields per Page »
  15. Masquez l'identifiant dans le formulaire
  16. Revenez sur la liste des contacts
  17. Constatez que la catégorie de chaque contact est affichée sous la forme « Category #id »
  18. Lisez le paragraphe « Formatting Options »
  19. Utilisez « formatValue() » pour retenir le nom de la catégorie comme valeur affichée dans le contact
    Remarque importante

    Puisque la catégorie peut être nulle, il est important d'effectuer un accès sûr (« nullsafe ») à son nom.

Configuration du « CRUD » de « User »

La gestion des utilisateurs dans le « back-office » est essentielle mais demande quelques ajustements pour gérer correctement le mot de passe. En effet, celui-ci est stocké sous une forme hachée et doit donc être traité lors de sa modification. Cependant, il faut veiller à ne pas hacher plusieurs fois le mot de passe lors de l'édition des autres champs.

Remarque importante

Proposer aux administrateurs de modifier le mot de passe d'un utilisateur n'est ni courant ni conforme aux bonnes pratiques de sécurité et de confidentialité. Cependant, dans ce sujet de TP, cela représente une occasion de découvrir comment effectuer des traitements sur les données des formulaires d'édition ou de création d'entité de EasyAdmin avant de mettre à jour la base de données.

Le fonctionnement va donc consister en la configuration du champ du mot de passe pour qu'il soit vide par défaut. S'il est reçu vide, aucune modification ne doit être faite. S'il est reçu non vide, il doit être haché avant d'être affecté à la propriété correspondante de l'entité en cours d'édition. Pour cela, le contrôleur devra utiliser le contexte qui permettra d'accéder à la requête HTTP et donc d'accéder aux valeurs saisies. Il devra également utiliser un « UserPasswordHasherInterface » pour hacher le mot de passe qui est saisi en clair. Le service de hachage de mot de passe sera injecté dans le constructeur du contrôleur.

Le contrôle de présence et le hachage du mot de passe devront être effectués à la mise à jour de l'utilisateur mais également lors de l'insertion d'un nouvel utilisateur. Ceci correspond respectivement aux méthodes « updateEntity() » et « persistEntity() » du « UserCrudController ».

Travail à réaliser
  1. Consultez la liste des utilisateurs
  2. Affichez la page d'édition d'un utilisateur
  3. Constatez que le mot de passe haché apparaît dans le formulaire
  4. Configurez les champs pour l'entité « User »
    Information

    Les rôles de l'utilisateur sont stockés dans un tableau PHP et sous forme d'une chaîne de caractères JSON dans la base de données. La correspondance est gérée par Doctrine 2. Le type de champ EasyAdmin correspondant est « ArrayField »

  5. Masquez l'identifiant dans le formulaire
  6. Donnez les caractéristiques suivantes au champ mot de passe :
  7. Ajoutez une propriété privée « UserPasswordHasherInterface » au « UserCrudController »
  8. Injectez cette propriété à la construction du « UserCrudController »
  9. Parcourez « Customizing CRUD Actions », et lisez en particulier le point « Creating, Persisting and Deleting Entities »
  10. À l'aide de l'autocomplétion de PhpStorm, surchargez la méthode « updateEntity() » (qui correspond à la mise à jour d'une entité) dans laquelle vous préserverez l'appel à la méthode parente
    Information

    Dans la classe et en dehors de toute méthode, vous pouvez obtenir la liste des méthodes « surchargeables » en utilisant le raccourci clavier « CTRL+SPACE ». Vous pouvez également saisir une partie du nom de la méthode à surcharger pour voir apparaître la liste des suggestions.

  11. Dans la méthode « updateEntity() », récupérez le mot de passe saisi par l'utilisateur en utilisant la requête HTTP accessible depuis le contexte (méthode « getContext() »)
    Information

    Inspectez le champ du mot de passe dans votre navigateur pour savoir ce que vous cherchez.

    Un champ de saisie HTML dont le nom est de la forme « name[key] » se trouvera du côté serveur, une fois analysé par le moteur PHP, comme un champ nommé « name » de type tableau et contenant une clé « key » permettant d'accéder à la valeur soumise par l'utilisateur.

  12. Si le mot de passe n'est pas vide, donnez la version hachée du mot de passe comme valeur de la propriété « password » de l'instance de l'entité, d'une manière similaire à ce qui a été réalisé avec « Foundry »
  13. Vérifiez que vous pouvez modifier correctement le mot de passe d'un utilisateur
    Information

    Vous devez modifier le mot de passe d'un utilisateur et essayer de vous connecter avec le nouveau mot de passe pour vérifier que cela fonctionne. Pour faciliter la procédure, utilisez deux navigateurs : un dans lequel vous êtes connecté comme administrateur pour modifier le mot de passe et un autre dans lequel vous effectuez les tentatives de connexion.

  14. Surchargez la méthode « persistEntity() » (qui correspond à l'ajout d'une entité) de la même manière que pour la méthode « updateEntity() »
    Information

    Vous vous apprêtez à reproduire le code de vérification et de hachage du mot de passe. Vous avez donc le choix entre copier le code et le factoriser. Vous allez évidemment le factoriser !

  15. Sélectionnez les quelques lignes de code dédiées au contrôle de présence du mot de passe et à son hachage dans la méthode « updateEntity() »
  16. Dans le menu « Refactor » de PhpStorm, sélectionnez « Refactor This… »
  17. Dans le menu contextuel choisissez « Extract Method… » et saisissez le texte « setUserPassword » comme nom de méthode
  18. Supprimez les annotations inutiles et donnez un type et un nom explicites au paramètre de la méthode
  19. Utilisez la méthode « setUserPassword » dans « persistEntity() »
  20. Vérifiez que l'ajout d'un utilisateur se déroule correctement

Un peu de style

La liste des utilisateurs peut être améliorée et c'est l'occasion de découvrir comment ajouter du style dans le « back-office ».

Travail à réaliser
  1. Revenez sur la liste des utilisateurs
  2. Vérifiez que les rôles des utilisateurs s'affichent bien sous la forme « ROLE_ADMIN, ROLE_USER » ou « ROLE_USER »
    Information

    Si vous consultez la base de données, vous constaterez que les utilisateurs ayant un tableau de rôles vide en base de données (« [] ») s'affichent ici avec un rôle « ROLE_USER ». Ceci est dû au fait que le rôle minimal est « ROLE_USER ».

  3. Pour commencer à remplacer cet affichage texte par des symboles Material, vous allez ajouter la feuille de style correspondante dans le « DashboardController »
  4. Utilisez la méthode « formatValue() » sur le champ associé aux rôles de l'utilisateur pour :
    • Retourner l'icône « manage_accounts » si le rôle « ROLE_ADMIN » est présent
    • Retourner l'icône « person » si le rôle « ROLE_USER » est présent
    • Retourner une chaîne vide sinon

Et après ?

Votre application de gestion de contacts est fonctionnelle mais peut être étoffée. Ce que vous avez réalisé est normalement suffisant pour mener à bien votre SAÉ.

Vous pouvez poursuivre ce TP en proposant la création, l'édition et la suppression de catégories, dans l'interface de l'application et dans le « back-office ». Vous pouvez compléter les tests de création, modification et suppression de contact. Vous pouvez proposer une page de profil utilisateur modifiable et toute autre fonctionnalité qui vous semble intéressante.