Guide de réalisation du projet

Table des matières

1. Introduction

Chaque section de ce guide va aborder une étape particulière de mise en œuvre du projet.

Il est recommendé de suivre ces étapes dans l’ordre présenté, qui correspond à la chronologie du déroulé des séances, qui présentent les différents concepts et outils.

Nous indiquons les prérequis par rapport à ce déroulé au début de chacune de ces étapes.

La densité de rédaction des opérations à effectuer n’est pas forcément proportionnelles au temps à passer pour y arriver.

Nous rédigeons en détail (dans le présent guide ou dans les supports de TP) les opérations la première fois que vous aurez à les effectuer, mais ensuite, la rédaction sera moins détaillée, vous laissant retrouver tout ce qu’il y a à faire (ce qui risque d’être plus long la deuxième fois).

Ce guide est long et parfois très détaillé. Il serait facile de se perdre en route.

Gardez une trace de votre avancement, au fur et à mesure du temps, pour savoir où vous en étiez dans les différentes étapes. Par exemple dans un fichier README à la racine de votre projet.

2. Étape proj-1 : Démarrage du projet Symfony

Procédons aux premières étapes de la réalisation pour appliquer ce qu’on a appris dans les séances de TP précédentes :

  1. création du projet Symfony
  2. mis en place du modèle de données initial

2.1. TODO Étape proj-1-a : Prise de connaissance du cahier des charges de l’application

  1. lisez le Cahier des charges de l’application;
  2. gardez un marque-page sur ce cahier des charges, pour vous y référer au fur et à mesure de votre avancement.

2.2. TODO Choix du domaine fonctionnel

Choisissez maintenant le domaine fonctionnel sur lequel vous souhaitez travailler (par exemple : « passionnés de guitare »).

Vous pouvez ainsi définir dès maintenant la nomenclature des classes du modèle de données de votre application. Ainsi, en reprenant l’exemple d’une application pour une communauté de passionnés de guitare (ou resp. de collectionneurs de cartes Pokémon), vous pouvez définir la nomenclature spécifique à votre projet :

  passionnés de guitare cartes Pokémon
[objet], guitare carte
[inventaire] rack collection
[galerie] showroom deck
   

Notez ces éléments dans un fichier de documentation que vous compléterez au fur et à mesure de l’avancement du projet. Nommez ce fichier README.txt (vous pouvez utiliser une syntaxe textuelle comme MarkDown (README.md) ou org-mode (README.org), du moment que c’est lisible sous forme de texte brut, et donc potentiellement gérable dans Git).

Vous n’avez plus qu’à vous lancer. Choisissez maintenant un nom de code pour votre projet (par exemple : « MyGuitars »).

2.3. TODO Étape proj-1-b : Initialisation du répertoire de travail du projet

  1. Créez un nouveau répertoire pour y sauvegarder le travail sur votre projet, par exemple dans ~/CSC4101/projet/ :

    mkdir $HOME/CSC4101/projet/
    

2.4. TODO Configuration de Git

Vous allez utiliser Git pour gérer les fichiers source du projet au fur et à mesure de votre avancement. Pour cela, Git doit être configuré de façon à ce que votre identité soit visible dans l’historique des instantannés (commits).

  1. Vérifiez si vous avez une configuration des variables de configuration de Git adaptée :

    git config user.email
    
  2. Si la commande précédente n’affiche rien, procédez à la définition de cette variable en remplaçant [votre email] par votre email école (...@telecom-sudparis.eu).

    git config --global user.email "[votre email]"
    
  3. Procédez de même pour votre nom :

    git config --global user.name "[votre Prénom Nom]"
    

2.5. TODO Création du projet Symfony

Récupérez une copie du squelette d’application Symfony, pour initier le projet, en fonction de votre choix de nom de code :

cd $HOME/CSC4101/projet/
symfony --debug new [nom-code] --version=lts --webapp

[nom-code] est à adapter (par exemple symfony --debug new myguitars --version=lts --webapp).

Un projet Symfony est créé dans le répertoire $HOME/CSC4101/projet/[nom-code]/, dans lequel vous allez travailler pour la suite.

2.5.1. Suppression de bundles inutiles

Exécutez la commande suivante pour supprimer un bundle Symfony qui ne nous servira pas :

cd [nom-code]/
symfony composer remove symfony/ux-turbo

2.5.2. Chargement dans l’IDE

Chargez ensuite le code du projet dans votre IDE.

Pour les utilisateurs d’Eclipse, il peut être intéressant de charger ce projet dans le même workspace que le code des différentes séances de TP, afin de faciliter les comparaisons de code, et le copier/coller.

2.6. TODO Étape proj-1-c : Tests de lancement de l’application

Testez le lancement de l’application avec symfony server:start comme déjà rencontré pour l’application de démo, dans la validation des opérations d’installation de votre environnement BYOD.

Consultez la page d’accueil affichée par Symfony, et notez les ressources documentaires proposées, que vous pourrez aller lire d’ici les prochaines séances de TP.

Nous vous suggérons de tracer une liste de choses à faire pour le projet, par exemple dans un fichier TODO.[txt|md|org] présent dans le répertoire du code de votre projet.

Notez également, dans ce fichier, les URL des supports de TP, et les n° des étapes auxquelles vous avez rencontré les consignes concernant ces choses à faire.

Pensez à ajouter dans cette liste la documentation Symfony à aller lire, ou les questions à poser en séances de TP aux encadrants…

2.7. TODO Étape proj-1-d : Mise à jour du référentiel Git

À partir de maintenant, nous vous incitons à vous appuyer sur le référentiel Git (local) qui a été créé par la commande symfony new. Cela va vous permettre de gérer le code de votre projet en utilisant Git, soit en ligne de commande, soit via l’IDE.

Dans un premier temps, quelques git add suffiront, au fil des modifications.

3. Étape proj-2 : Génération des premières entités du modèle de données

L’objectif de cette étape est de commencer la conception et la mise en œuvre des premières entités du modèle des données.

Vous allez commencer la génération du code PHP qui permettra de gérer, avec Doctrine, les premières entités nécessaires au stockage dans la base de données pour l’application.

Avant de décrire les étapes concrètes à effectuer, faisons quelques rappels.

3.1. Méthodologie : travail incrémental

Vous n’aurez pas assez de cette première séquence du projet pour générer toutes les entités du modèle de données issues du cahier des charges.

Commencez par une analyse large, mais par une réalisation minimale, selon les indications données, pour avancer par incréments au long du projet.

La méthodologie que nous vous conseillons d’adopter est :

  1. ajout au modèle de données de quelques entités fortement liées (sans être exhaustif sur leurs propriétés)
  2. ajout de données de test minimales à charger en base de données

Dans les séquences de travail suivantes vous pourrez continuer l’ajout de nouvelles entités du modèle, en suivant ces étapes, et en ajoutant les fonctions dédiées, une fois qu’on aura introduit les concepts et technos correspondantes :

  1. réalisation de premières interfaces de consultation de ces données
  2. raffinement des propriétés à gérer pour ces premières entités
  3. itération pour d’autres entités, en ajoutant au fil de l’eau des associations entre entités anciennes et nouvelles

3.2. TODO Préparation de l’ajout des premières entités

Vous aurez besoin d’un certain nombre d’entités dans le modèle des données de l’application, qui serviront à gérer les différentes fonctions, mais on ne va pas les ajouter toutes dès maintenant.

L’analyse du cahier des charges peut nous amener à dégager un ordre pour réaliser la mise en œuvre de façon itérative, afin de travailler et tester de façon progressive :

  1. première liste d’entités pour débuter le noyau de l’application :
    • [inventaire]
    • [objet]
  2. dans un second temps, on pourra ajouter, les entités restantes, pour lesquelles on a documenté des sections spécifiques du présent guide :

    • [galerie] (cf. « Ajout de l’entité [galerie] au modèle des données » à effectuer dans quelques temps)
    • membre (cf. « Ajout au modèle de données de l’entité membre… »)

    Respectez la chronologie indiquée par l’ordre des sections du guide: n’ajoutez les [galeries] qu’une fois que vous aurez réalisé les premières pages de consultation avec les gabarits pour [inventaire] et [objet].

Vous utiliserez les assistants de génération de code de Symfony pour générer le modèle pour Doctrine.

Avant de vous lancer dans le code, précisez (sur papier, dans un README, …) les propriétés que vous souhaitez utiliser pour ces premières entités, en vous aidant de l’exemple suivant :

3.2.1. Liste des propriétés des entités du modèle

Voici une première liste de propriétés mono-valuées pour les premières entités qu’on ajoute au modèle. On précise plus loin les propriétés multi-valuées (associations).

Vous êtes libres d’adapter cette liste à vos besoins, pour le domaine fonctionnel spécifique que vous avez choisi, ou pour améliorer selon vos goûts.

Nous vous conseillons cependant de démarrer avec un noyau minimal démontrant la faisabilité.

Vous ajouterez le reste des propriétés plus tard, au fur et à mesure de l’avancement, quand il s’agira d’obtenir des pages Web au contenu réaliste.

Si vous ajoutez beaucoup de propriétés dès le début, c’est autant de travail à faire/défaire en cas de modifications ultérieures. Soyez ambitieux, mais démarrez doucement.

Prenons un exemple : pour l’application Todo, étudiée en TP, il n’est pas essentiel, dès le début du travail d’avoir des propriétés comme le statut de la tâche (ouverte ou terminée, dans completed) ou les dates de création et de modification. Par contre, le titre de la tâche sert à avoir des données de test, ou on peut s’y retrouver. Une fois qu’on a finalisé le lien entre un projet et ses tâches (association OneToMany) et qu’on sait modifier toutes ces entités, dans des formulaires Web opérationnels, on peut ajouter un peu plus de propriétés à ces entités, pour avoir un site au contenu un peu moins squelettique.

3.2.1.1. Exemple pour [inventaire]
Nom propriété Type Contraintes Commentaire
description string notnull  

Les [inventaires] sont assez basiques et probablement peu différents d’un domaine fonctionel à l’autre.

Pour un membre donné, son [inventaire] unique n’a pas tellement de propriétés utiles… à part être le réceptacle des [objets], mais on va voir les associations plus loin. La description est donc presque facultative.

Pour les administrateurs, par contre, le fait d’avoir des descriptions qui distinguent les [inventaires] de différents membres peut être utile… et ça nous servira pour la mise-au-point.

3.2.1.2. Exemple pour [objet]
Nom propriété Type Contraintes Commentaire
description string notnull  
... propriétés spécifiques ...      

Là par contre, les propriétés des [objets] sont très spécifiques au domaine.

À nouveau, attention à ne pas réaliser immédiatement l’ajout de toutes ces propriétés. On n’est qu’en phase de planification, pour le moment.

3.2.2. Associations entre entités

Ensuite il y aura des associations à définir entre entités, résultant en des propriétés multi-valuées dans les classes. En général il faudra les à traduire dans le code PHP/Doctrine en annotations d’associations OneToMany ou ManyToMany, etc.

Par exemple, pour commencer, vous aurez à définir la première association :

  • [inventaire] (1) — (0..n) [objet] : OneToMany (un [inventaire] contient 0 à n [objet])

Notez qu’il y aura des contraintes à définir à chaque fois concernant la « force » de cette association (simple association, ou composition). Ici, on pourra choisir de contraindre les [objet] à appartenir obligatoirement à un [inventaire], ce qui signifiera une contraine non-nulle sur la référence d’un [objet] à son [inventaire] (nullable: false).

Plus tard, au fur et à mesure de la construction de l’application, vous ajouterez des fonctionnalités plus avancées et pourrez réutiliser les assistants de génération de code (makers) pour ajouter de nouvelles entités, ou de nouvelles propriétés aux entités.

Attention aux relations ManyToMany : elles sont potentiellement plus difficiles à gérer, donc n’en abusez-pas au début du projet.

Une fois que vous avez défini (sur le papier) les propriétés que vous souhaitez gérer dans les premières entités, ainsi que les premières associations entre ces entités, passons à la mise en œuvre dans le code PHP/Doctrine.

3.3. Étape proj2-a : Rappel au sujet des outils de mise en œuvre

Pour mémoire les étapes suivantes sont nécessaires pour l’ajout d’une nouvelle entité :

  1. définir dans le code des classes PHP des attributs Doctrine (ORM\...) gérant la persistence de l’entité en base de données
  2. re-générer la base de données de tests de l’environnement de développement en fonction des modifications que vous avez faites sur votre code

Avant de pratiquer, rappelons l’utilité et le principe de fonctionnement des différents outils qu’on utilisera, vus dans les deux premières séances de TP.

3.3.1. Fonctionnement du générateur de code make:entity

Pour mémoire, on peut écrire des classes PHP « à la main » puis ajouter les attributs PHP nécessaires à la persistence avec Doctrine.

Ou bien on peut utiliser le générateur de code make:entity qui permet d’éviter bon nombre d’erreurs de copier-coller et d’accélérer ainsi le développement. Nous vous conseillons, au moins pour débuter, d’utiliser cet assistant.

Pour mémoire, l’assistant make:entity a été introduit dans les premiers TP et est documenté dans « Databases and the Doctrine ORM » (symfony console make:entity --help).

Il permet de créer de zéro, dans src/Entity/, un nouvelle classe PHP intégrant les attributs PHP 8 des annotations pour Doctrine.

Mais avec le même outil make:entity, on peut également compléter une classe existante, pour lui ajouter de nouvelles propriétés, ou des associations vers des classes existantes : Il suffit d’appeler make:entity en re-précisant le nom de la classe existante : le code des classes PHP existant dans src/Entity/ sera mis à jour.

Bien-sûr make:entity reste optionnel, s’il s’agit de compléter une classe existante : on peut également modifier le code PHP manuellement (copier-coller et adaptations, par exemple). C’est parfois plus simple pour ajouter des propriétés ou changer certains paramètres, au lieu de réinvoquer l’assistant générateur de code.

Après chaque mise à jour des classes Doctrine, on va tester l’exécution, dans l’environnement de développement Symfony, pour vérifier si l’application continue à fonctionner. Avant de tester l’ajout de données de tests, il sera nécessaire de (re-)générer la base de données de tests (SQLite) avec les outils de Doctrine.

3.3.2. Création ou mise-à-jour du schéma de la base de données

La création se passe en deux temps :

  1. création du fichier de stockage de la base (symfony console doctrine:database:create),
  2. création du schéma (tables et index dans la base) à partir des attributs Doctrine définis dans les classes PHP (symfony console doctrine:schema:create)

Au fur et à mesure, comme on travaille de façon incrémentale, on a déjà une base de donnée existante, qui correspondant à une version antérieure de nos classes PHP. Il faut donc mettre à jour (ou recréer) le schéma de la base de données utilisées pour nos tests (ajout de tables, de colonnes, etc.) :

  • si tout va bien, mise-à-jour directe via l’outil dédié : schema:update (symfony console doctrine:schema:update), sans suppression des données existantes
  • en cas de soucis, on peut recréer la base de données (à vide) :
    1. suppression de la base (symfony console doctrine:database:drop)
    2. re-création de la base (symfony console doctrine:database:create),
    3. re-création du schéma (symfony console doctrine:schema:create)
  • l’utilisation des « migrations » Symfony n’est pas utile tant qu’on n’a pas déployé l’application en production, donc les indications ci-dessus sont préférables, dans le contexte du projet.

Pour plus de fiabilité, il est recommandé de ne pas ajouter des données manuellement, mais de s’appuyer sur l’automatisation du chargement de données de tests, codé dans le projet (Fixtures), donc rejouable à loisir, à chaque modification du modèle de données.

3.3.3. Chargement de données de tests avec les Fixtures

On peut finalement ajouter du code permettant de charger des données de test, avec le module de Fixtures (également introduit dans les premiers TP), pour vérifier à l’exécution que toutes les entités sont bien définies dans le code, ainsi que leurs associations.

On écrit le chargement des données via des classes PHP présentes dans src/DataFixtures/, en procédant comme pour les exemples vus sur « ToDo » (avec l’instruction yield, par exemple, cf. ci-dessous).

Ensuite, on charge les données dans la base avec symfony console doctrine:fixtures:load.

Note : pour gérer les associations entre entités, il est recommandé d’utiliser des références, dans les fixtures. Cf. Gestion de « références » dans les Fixtures en annexe. Cela sera re-détaillé plus bas.

3.4. TODO Étape proj2-b : Configuration du type de base de données de tests

Configurez, dans le fichier .env la base de données, afin d’utiliser SQLite. La procédure pour ce faire a été vue en TP 1.

3.5. TODO Étape proj2-c : Ajoutez le support des DataFixtures

Pour cela, il faut ajouter le module doctrine/doctrine-fixtures-bundle dans le projet :

symfony composer require --dev orm-fixtures

3.6. TODO Étape proj2-d : Création effective des premières entités

Créez maintenant, de façon itérative, les premières entités, et leurs données de tests, en exécutant à chaque incrément les opérations suivantes :

  1. Ajout de l’entité (par ex. [inventaire]) avec ses propriétés mono-valuées minimales :
    1. Appel à make:entity pour générer le nouveau code PHP (sauf pour « Membre », voir ci-dessous)
    2. Application des changements sur le schéma de la base
    3. Ajout dans les Fixtures du code de chargement de données de tests minimales
    4. Chargement des nouvelles données de tests pour cette entité (ça fonctionne !)
  2. Ajout de l’entité suivante (par ex. [objet]) avec ses propriétés mono-valuées minimales :

    … (mêmes étapes que pour [inventaire])…

  3. Ajout de l’association OneToMany entre les deux entités précédentes :

    … /mêmes étapes, globalement, sauf qu’on rappelle make:entity sur l’entité [inventaire] existante pour la modifier;

    On va lui ajouter une propriété multi-valuée, de type relation (OneToMany), qu’on pourra nommer « [objets] » (au pluriel);

    On codera les Fixtures chargeant ces associations avec des références (comme expliqué ci-dessus).

    Exemple d’exécution du générateur de code :

    $ symfony console make:entity
    
     Class name of the entity to create or update (e.g. GrumpyGnome):
     > Rack
    
     Your entity already exists! So let's add some new fields!
    
     New property name (press <return> to stop adding fields):
     > guitars
    
     Field type (enter ? to see all types) [string]:
     > relation
    
     What class should this entity be related to?:
     > Guitar
    
    What type of relationship is this?
     ------------ ------------------------------------------------------------------- 
      Type         Description                                                        
     ------------ ------------------------------------------------------------------- 
      ManyToOne    Each Rack relates to (has) one Guitar.                             
                   Each Guitar can relate to (can have) many Rack objects.            
    
      OneToMany    Each Rack can relate to (can have) many Guitar objects.            
                   Each Guitar relates to (has) one Rack.                             
    
      ManyToMany   Each Rack can relate to (can have) many Guitar objects.            
                   Each Guitar can also relate to (can also have) many Rack objects.  
    
      OneToOne     Each Rack relates to (has) exactly one Guitar.                     
                   Each Guitar also relates to (has) exactly one Rack.                
     ------------ ------------------------------------------------------------------- 
    
     Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
     > OneToMany
    
     A new property will also be added to the Guitar class so that you can access and set the related Rack object from it.
    
     New field name inside Guitar [rack]:
     > 
    
     Is the Guitar.rack property allowed to be null (nullable)? (yes/no) [yes]:
     > no
    
     Do you want to activate orphanRemoval on your relationship?
     A Guitar is "orphaned" when it is removed from its related Rack.
     e.g. $rack->removeGuitar($guitar)
    
     NOTE: If a Guitar may *change* from one Rack to another, answer "no".
    
     Do you want to automatically delete orphaned App\Entity\Guitar objects (orphanRemoval)? (yes/no) [no]:
     > yes
    
     updated: src/Entity/Rack.php
     updated: src/Entity/Guitar.php
    
     Add another property? Enter the property name (or press <return> to stop adding fields):
     > 
    
    
    
      Success! 
    
    
     Next: When you're ready, create a migration with symfony console make:migration
    
    

Une fois les premières entités [inventaire] et [objet] ajoutées, avec leur association OneToMany fonctionnelle, vous pouvez passer à l’étape suivante, puis à l’ajout d’un tableau de bord Web.

Note : on détaille plus loin la création de l’entité [galerie], qui peut n’intervenir que dans un temps ultérieur (cf. Ajout de l’entité [galerie] au modèle des données).

Vous continuerez l’ajout d’autres entités plus tard, en revenant à cette étape, incrément par incrément.

Enfin, bien plus tard, quand les pages Web, formulaires et autres choses à faire seront terminées, vous fignolerez le modèle de données en réutilisant make:entity pour ajouter les propriétés non-essentielles mais qui rendent le modèle de données réaliste. Et vous ajouterez donc dans la foulée les données de tests dans les Fixtures.

En cas de problèmes, lisez les suggestions des messages d’erreur, et référez-vous aux supports de TP pour les choses complémentaires à régler dans le code (ajout d’annotations, de méthodes, etc.). Nous ne redétaillons pas toutes les opérations fines, déjà vues en TP, dans ce support.

4. Étape proj-3 : Création des premières pages publiques, en consultation   Après_TP_4

L’objectif de cette étape est d’ajouter les premières vues de l’application en consultation, pour les entités qui ont été ajoutées au modèle.

Prérequis : avoir étudié le rôle des contrôleurs (séance de cours 4), et observé le fonctionnement de l’interface Web fonctionnant grâce aux contrôleurs Symfony dans Todo (début de la séance de TP n°4).

Il va s’agir maintenant de gérer des pages Web du « front-office » de l’application, via l’ajout de classes contrôleurs Symfony spécifiques, que vous allez générer, puis customiser.

En ajoutant des controleurs Symfony dédiés dans l’application (puis, bientôt, des gabarits avec Twig), on souhaite pouvoir maîtriser complètement les interactions avec l’utilisateur.

Pour l’instant on ne gérera pas encore la partie mis en forme qui sera ajoutée plus tard, au moment où on aura étudié HTML et les gabarits Twig, et surtout CSS.

Cette section est rédigée pour que ces opérations soient effectuées une fois que le rôle des contrôleurs est explicité, mais avant que les gabarits aient été étudiés. En avançant au rythme des premiers TPs.

Si vous êtes en retard sur la réalisation du projet, et que vous avez déjà pratiqué en TP les mécanismes de gabarits, au moment de réaliser cette étape du guide de réalisation, ne suivez pas ces instructions littéralement pour la génération du contenu HTML des pages, et utilisez plutôt les gabarits.

4.1. Méthodologie pour la génération du code pour ajouter des pages en consultation

Lisez cette introduction avant de passer à la mise en oeuvre dans la section suivante.

Vous allez générer ci-après le code des premières classes contrôleurs Symfony pour mettre en place la consultation des premières entités, comme par exemple :

  • la consultation d’une liste d’[objets] dans un [inventaire] particulier;
  • la consultation d’une fiche d’un [objet] particulier.

Les pages et les routes gérées par ces contrôleurs concerneront la partie « front-office » de l’application. C’est à dire celle utilisée par les membres de la communauté hébergée sur le site.

Nous vous incitons à utiliser dans un premier temps l’assistant générateur de code make:controller, qui génère un squelette de classe contrôleur, qu’on peut ensuite adapter.

Nous verrons un peu plus tard qu’on pourra aussi utiliser un autre générateur de code (make:crud), qui permettra de créer le squelette de classes contrôleurs supportant des interactions CRUD, et génèrera pour nous des gabarits Twig prêts à l’emploi.

Cela augmentera encore plus notre productivité, mais il faut d’abord qu’on ait expérimenté le fonctionnement des controleurs, en consultation seulement, et Twig.

Dès que vous aurez étudié Twig en cours, passez à l’utilisation de make:crud (même sans attendre d’avoir étudié les CRUD).

On va commencer avec des étapes détaillées pour la première entité, [inventaire], puis vous deviendrez plus autonomes.

Pour chaque entité, le même processus se répétera : une fois le code du contrôleur généré, vous enchainerez sur la conception du contenu des pages, dans les premiers gabarits (quand vous aurez appris Twig et HTML).

Quand vous maîtriserez l’ensemble de ces technologies, le processus sera le même pour l’ajout de chacune des pages de consultation.

Ne générez pas trop vite des contrôleurs pour toutes les entités du modèle de données.

Chaque chose en son temps : suivez le rythme suggéré dans ce guide, et vous devriez arriver lentement mais sûrement à un projet qui évoluera d’abord doucement, puis plus rapidement au fil du temps.

Si vous écrivez dès maintenant plein de code pour construire « à la main » l’affichage en HTML dans plein de pages de consultation… et que vous découvrez plus tard dans les séances de TP, les outils de génération de gabarits Twig, vous devrez recommencer une partie du travail.

Inutile d’aller trop vite, tant qu’on n’a pas encore eu le temps d’étudier toutes les technos utiles en TP.

Ces avertissements étant faits, passons au concret sur les premières pages de consultation.

4.2. TODO Étape proj3-a : Ajout d’un premier Contrôleur pour [inventaire]

Vous allez maintenant réaliser l’ajout d’un premier contrôleur Web Symfony pour la consultation des [inventaires].

Pour l’instant, on va afficher deux pages permettant de consulter :

  • la liste des [inventaires], qui sera destinée aux administrateurs ;
  • la consultation d’un [inventaire] particulier, destinée elle au membre propriétaire de cet inventaire.

Plutôt que d’écrire le code « from scratch », ici aussi on va faire appel à un assistant Symfony du « maker bundle » pour générer un Contrôleur Symfony : make:controller.

Attention aux nomenclatures classiques en orienté objet : les noms de classes commencent par des majuscules, avec un jeu de CamelCase en cas de juxtaposition de plusieurs mots.

Comme d’habitude, vous pouvez vous inspirer utilement du code que vous avez étudié sur l’application ToDo en TP.

  1. Utilisez l’assistant make:controller pour générer le code d’une classe contrôleur, dont le nom lui est passé en argument :

    symfony console make:controller [Inventaire]Controller
    

    adaptez au nom de votre classe [inventaire] particulière, par exemple : symfony console make:controller RackController pour consulter les racks de guitares que gère chaque membre.

  2. Consultez dans l’IDE le code de la classe qui a été ajoutée dans le fichier généré : src/Controller/[Inventaire]Controller.php.
  3. Consultez la table de routage de l’application :

    symfony console debug:router
    

    Comme vous pouvez le voir, l’assistant make:controller se base sur le nom de la classe contrôleur qu’on lui a donné en argument, pour en extraire un chemin d’URLs. Pour cela il le découpe en mots, en analysant les séquences de lettres commençant par une capitale.

    Notez sur quelle URL on peut trouver la page servie par le contrôleur qu’on vient d’ajouter :

    symfony console debug:router --show-controllers
    
  4. lancez le serveur Web :

    symfony server:start
    
  5. Testez l’URL http://127.0.0.1:8000 sur laquelle le serveur s’est lancé.

    Vous devez voir apparaître un message de Symfony, qui affiche « Your application is now ready ».

  6. Testez l’URL servie par le nouveau contrôleur. Vous obtenez une page par défaut « Hello [Inventaire]Controller! »

    Succès ! \o/

4.3. TODO Étape proj3-b : Ajout de la méthode d’affichage de la liste des [inventaires]

On va adapter le code généré par l’assistant, pour mettre en œuvre un affichage rudimentaire de la liste des [inventaires] présents dans la base de données, lorsqu’on consulte la route « / » de notre serveur Web.

Plus tard, on pourra changer cette route, pour que cela apparaisse dans /admin/[inventaire]/, par exemple. En effet, in fine, il s’agira d’afficher aux administrateurs, dans le back-office, les [inventaires] de tous les membres. Mais, pour l’instant, consultons simplement l’ensemble des entités [inventaire] présentes dans la base de données.

Procédez aux opérations suivantes :

  1. Examinez à quelle méthode correspond cette route (par exemple dans un autre onglet du terminal lancé depuis le même répertoire, pour ne pas interrompre le serveur HTTP) :

    symfony console debug:router --show-controllers
    
  2. Modifiez la méthode correspondante de la classe controleur, pour réaliser l’affichage des [inventaires]

Le chargement depuis la base de données ne devrait pas vous sembler très complexe à ajouter : on procède de la même façon que pour l’interface « console » des commandes étudiées précédemment, en faisant appel à Doctrine pour le chargement des données, en s’inspirant fortement du code présent dans ToDo.

Nous devons donc générer le contenu une page HTML à transmettre dans la réponse HTTP, mais nous n’avons pas encore étudié le langage HTML en détails.

Voici donc quelques éléments pour maquetter quelque chose qui y ressemble. Inspirez-vous de ce qui est fait dans le code de ToDo (dans listAction() dans src/Controller/TodoController.php), pour obtenir quelque chose du genre :

<html>
  <body>Liste des [inventaires] de tous les membres :
    <ul>
      <li>...premier [inventaire]...</li>
      ....
    </ul>
  </body>
</html>

Ce code HTML n’est pas idéal, mais il fonctionne à peu près, suffisamment pour que votre navigateur affiche la liste des [inventaires]. On travaillera sur la présentation ultérieurement.

Une fois que vous avez ajouté l’affichage de la liste des [inventaires], vous pourrez continuer avec d’autres pages gérées par le même controleur.

4.4. TODO Étape proj3-c : Ajout de la consultation d’un [inventaire]

Ajoutez le code de la consultation de la « fiche » d’un [inventaire], accédé via une URL du type /[inventaire]/{id}.

Ajoutez une nouvelle méthode au contrôleur qui permettra la consultation via une route /[inventaire]/{id} :

/**
 * Show a [inventaire]
 *
 * @param Integer $id (note that the id must be an integer)
 */
#[Route('/[inventaire]/{id}', name: '[inventaire]_show', requirements: ['id' => '\d+'])]
public function show(ManagerRegistry $doctrine, $id) : Response
{
        $[inventaire]Repo = $doctrine->getRepository([Inventaire]::class);
        $[inventaire] = $[inventaire]Repo->find($id);

        if (!$[inventaire]) {
                throw $this->createNotFoundException('The [inventaire] does not exist');
        }

        $res = '...';
        //...

        $res .= '<p/><a href="' . $this->generateUrl('[inventaire]_index') . '">Back</a>';

        return new Response('<html><body>'. $res . '</body></html>');
}

Modifiez ce code, notamment la valeur prise par $res, afin d’essayer de produire le contenu à afficher avec du code HTML (toujours en vous inspirant du code de ToDoController vu en TP). Attention à ne pas laisser des crochets « [...] », en copiant-collant, et à respecter la syntaxe PHP, les noms de vos classes, etc.

Une façon simple, mais pas très évoluée pour fabriquer ce HTML, consiste à concaténer une succession de chaînes de caractères, encadrées par les balises HTML, ouvrante et fermante, d’un paragraphe <p> et </p>. C’est plutôt brutal, et pas la façon la plus élégante de procéder, mais pour l’instant, c’est à notre portée. On verra bientôt comment programmer cela de façon plus élégante, avec les gabarits.

Testez l’accès à l’URL http://localhost:8000/[inventaire]/1234.

L’argument et $id de la méthode show() est extrait automatiquement de la route, conformément à la spécification l’attribut Route qui a été placé immédiatement au-dessus de la définition de la méthode.

Notez que lorsque tout se passe bien, on renvoit un objet Response. Quand une erreur survient, on peut interrompre les traitements en déclenchant l’apparition d’une exception avec throw, ce qui aboutit au renvoi au client du code de réponse HTTP correspondant.

Si tout ça ne fonctionne pas… c’est peut-être juste qu’il manque des données de test dans votre projet (cf. DataFixtures), ou bien qu’il y a un bug.

4.5. TODO Étape proj3-d : Ajout du lien de consultation, à partir de l’affichage de la liste

Ajoutez, dans le code de fabrication de la liste des [inventaires], la génération des liens permettant d’accéder à la consultation des fiches de chaque [inventaire]. Cela fait le lien entre les deux étapes précédentes.

On peut ajouter une balise HTML <a href="...">...</a> pour créer un tel lien, du style :

Liste des [inventaires] :
...
   <li><a href="/[inventaire]/42">un [inventaire] (42)</a></li>

Vous utiliserez la méthode generateUrl() du contrôleur, qui sait générer une URL conforme à une définition de Route(...).

Ainsi, pour la route [inventaire]_show définie par #[Route('/[inventaire]/{id}', name: '[inventaire]_show')], qui utilise un attribut pour générer l’URL, on passera la valeur correspondante via :

$url = $this->generateUrl(
    '[inventaire]_show',
    ['id' => $[inventaire]->getId()]);

Testez que cela fonctionne.

Notez que les URL générées, cible des liens, qui sont présentes dans les attributs href des balises <a> sont des URL relatives au site, qui ne contiennent donc pas l’ensemble des composantes d’une URL complète. Ici, seul le chemin au sein du site est nécessaire.

Vous avez donc ajouté à votre projet deux ensembles de pages, en consultation, de type liste d’entités et fiche d’une entité. Cette fois, vous l’avez réalisé pour [inventaire], et vous allez ensuite pouvoir en faire autant pour d’autres entités.

Répétons-nous : ne générez pas trop vite des contrôleurs pour toutes les entités du modèle de données.

Si vous écrivez dès maintenant plein de code pour construire « à la main » l’affichage en HTML dans plein de pages de consultation (comme on vient de le décrire)… et que vous découvrez très bientôt les outils de génération de gabarits Twig, vous devrez recommencer une partie du travail.

Selon si la séance de TP est proche, attendre un peu et passez à l’utilisation des gabarits Twig pour les [inventaires] avant de continuer à générer d’autres controleurs.

5. TODO Ajout des gabarits dans les pages   Après_TP_5

Prérequis : avoir étudié les gabarits Twig (séance de cours 5-6), et expérimenté avec l’interface Web et l’ajout de gabarits dans Todo (TP n°5)

Il est maintenant temps de générer le contenu du HTML des pages de l’application via des gabarits Twig, plutôt qu’en faisant des concaténations de chaînes de caractères, comme on l’a fait auparavant.

Normalement, vous trouvez un fichier de gabarit de base dans templates/base.html.twig.

Vous allez maintenant créer des fichiers de gabarits pour chacune des pages à afficher, qui seront utilisées par les méthodes des contrôleurs que vous ajouterez dans le front-office. Vous les organisez par sous-répertoires de templates/, sur le modèle de ce qu’on a fait dans l’application fil-rouge ToDo.

En fait, le maker make:controller de Symfony a déjà fait le job pour vous, avec un sous-répertoire par contrôleur créé, et un premier gabarit dedans.

Vous avez par exemple déjà un fichier templates/[inventaire]/index.html.twig qui a été créé en même temps que la classe [Inventaire]Controller :-)

Au final, vous aurez par exemple, pour le front-office :

  • [Inventaire]Controller qui utilise pour ses méthodes, les gabarits correspondants :
    • list() : templates/[inventaire]/list.html.twig
    • show() : templates/[inventaire]/show.html.twig
  • puis [Objet]Controller, resp. :
    • show() : templates/[objet]/show.html.twig

Et ainsi de suite au fur et à mesure de l’ajout de pages au front-office.

On ajoutera plus tard la gestion des formulaires et d’autres choses qui nécessiteront aussi d’autres gabarits.

Pour l’instant, n’ajoutez pas des classes contrôleurs et des gabarits pour toutes les entités de votre modèle de données. C’est du travail répétitif, qui n’apporte pas grand chose pour l’instant. Restons-en à deux-trois contrôleurs maximum.

Gardez en réserve les choses à faire dans votre liste de tâches.

Et dès que vous avez maîtrisé les gabarit Twig pour l’habillage de la première entité, vous pouvez encore augmenter votre productivité en passant à make:crud au lieu de make:controller.

5.1. TODO Ajout d’un gabarit pour l’affichage d’un [inventaire] dans le front-office

Vous ajoutez donc maintenant un premier gabarit pour la consultation des propriétés d’un [inventaire]. Inspirez-vous de la façon dont on a ajouté l’affichage des propriété d’une tâche dans la page de consultation des tâches dans l’application Todo, dans le TP 5.

On peut faire cela simplement, par exemple en construisant un tableau HTML avec une ligne par propriété de l’[inventaire], dans le gabarit templates/[inventaire]/show.html.twig :

{# ... #}
{% block title %}[Inventaire] n°{{ [inventaire].id }}{% endblock %}

{% block body %}
    <h1>[Inventaire]</h1>

       {% dump [inventaire] %}

    <table class="table">
        <tbody>
            <tr>
                <th>Id</th>
                <td>{{ [inventaire].id }}</td>
            </tr>
            <tr>
                <th>Propriété 1</th>
                <td>{{ [inventaire].prop_1 }}</td>
            </tr>
            ...
        </tbody>
    </table>

    <a href="{{ path('[inventaire]_index') }}">back to list</a>

{% endblock %} {# body #} 
{# ... #} 

Pour l’instant on n’affiche que les propriétés mono-valuées, et on verra un peu plus bas, l’ajout de l’affichage des [objets] de cet [inventaire].

Ajoutez l’appel à render() de Twig correspondant, dans la méthode du Controleur des [inventaires].

Testez que cela fonctionne, et réglez les éventuels soucis de noms de routes à ajuster.

5.2. TODO Ajout de l’affichage des [objets] d’un [inventaire] dans le front-office

Au fur et à mesure de l’ajout des entités dans le modèle de données, vous réaliserez, une à la fois, l’ajout des pages correspondantes dans le front-office.

5.2.1. Ajout d’un controlleur pour l’entité [objet] dans le front-office

En vous inspirant de ce qui a été fait précédamment pour [inventaire], ajoutez un nouveau controlleur au front-office pour l’entité [objet], avec l’assistant générateur de code make:controller. Appelez-le [Objet]Controller.

Pour les premières pages de consultation, lorsque vous travaillez pour la première fois sur les gabarits Twig, il vaut mieux utiliser l’assistant générateur de code make:controller de Symfony.

Mais assez rapidement après, pour la suite des entités, nous vous conseillons d’utiliser directement un autre assistant générateur de code, symfony console make:crud qui générera pour vous les gabarits, ainsi que les formulaires pour la modification. N’hésitez pas à l’utiliser même si on n’a pas vu en cours tous les détails des formulaires, car cela vous permettra d’éviter de devoir changer plus tard les noms de routes et emplacement des gabarits. Il suffira d’ignorer pour l’instant la partie gestion des formulaires, en attendant d’avoir fait le TP 7.

5.2.2. Ajout de la consultation d’une entité [objet]

Cette fois, pour [objet], on a besoin d’une page de consultation d’une fiche d’[objet], mais pas d’afficher dans ce contrôleur la liste de tous les [objets] de la base de données. En effet, dans le front-office, cette liste sera découpée via chacun des [inventaires] de chacun des membres.

Renommez donc le gabarit templates/[objet]/index.html.twig généré, pour l’appeler show.html.twig, puis incorporez le code Twig inspiré de celui de [inventaire]/show.html.twig pour afficher toutes les propriétés d’un [objet].

Supprimez la méthode index() du controlleur [Objet]Controller, puis ajoutez une méthode [Objet]Controller::show(), en recopiant le code PHP de la méthode [Inventaire]Controller::show(), adapté au chargement d’une entité [objet].

5.2.3. Ajout de la navigation d’un [inventaire] vers un de ses [objets]

Ajoutez, dans le template show affiché par [Inventaire]Controller::show(), la liste des [objets] de cet [inventaire].

Il suffit de modifier le gabarit de consultation d’un [inventaire] (templates/[inventaire]/show.html.twig) pour y afficher le contenu de la collection des [objets] de cet [inventaire], sous forme de liste à puces.

Cette collection est contenue dans la propriété multi-valuée correspondant à l’association OneToMany qui a été définie dans le modèle de données doctrine de [inventaire]. Cette propriété est probablement nommée « [objets] » (avec un « s ») si vous avez utilisé la valeur par défaut proposé par l’assistant générateur de code. En cas de doute, pour retrouver le nommage de l’attribut correspondant, vous pouvez ajouter un {% dump [inventaire] %} dans le code Twig du gabarit.

Ensuite, rien de très compliqué : on ajoute un <ul> dans le code HTML du gabarit, un for Twig à l’intérieur pour les valeurs de cet attribut, qui génère autant de <li> qu’il y a d’[objets] dans la collection chargée en base. Par exemple :

{% block body %}
      <h1>[Inventaire]</h1>

      {% dump [inventaire] %}

      <table class="table">
          <tbody>
              <tr>
                  <th>Id</th>
                  <td>{{ [inventaire].id }}</td>
              </tr>
              <tr>
                {# ... #}
              </tr>
              <tr>
                  <th>[Objets]</th>
                  <td>
                  <ul>
                  {%  for [objet] in [inventaire].[objets] %}
                  <li>
                  {{ [objet] }}
                  </li>
                  {% endfor %} {# [inventaire].[objets] #}
                  </ul> 
                  </td>
              </tr>
          </tbody>
      </table>

Ajoutez enfin les liens de navigation vers la page d’affichage d’un des [objets] de cet inventaire : il faut juste ajouter l’appel à path() pour bien naviguer dans les routes.

Vous obtenez ainsi quelque chose comme :

<ul>
  {%  for [objet] in [inventaire].[objets] %}
    <li>
      <a href="{{ path('[objet]_show', {'id': [objet].id}) }}">{{ [objet] }}</a>
    </li>
  {% endfor %} {# [inventaire].[objets] #}
</ul> 

5.2.4. Ajout de la navigation inverse

On doit pouvoir remonter depuis l’affichage d’un [objet] jusqu’à son [inventaire], via le lien inverse.

Pour cela, ajoutez quelque chose comme :

<a href="{{ path('[inventaire]_show', {'id': [objet].[inventaire].id}) }}">back to [inventaire]</a>

6. Ajout habillage CSS dans les gabarits   Après_TP_6

Commencer l’ajout d’une mise en forme sur votre projet, en intégrant les feuille de style CSS avec Bootstrap.

Prérequis : avoir étudié les feuilles de style CSS (séance de cours 5-6), et ajouté le CSS avec Bootstrap dans ToDo (TP n° 6)

Vous allez continuer la réalisation de votre projet pour intégrer dans les gabarits Twig les éléments nécessaires pour une mise en forme via des feuilles de style, en utilisant le framework CSS Bootstrap.

Inspirez-vous du code de ToDo pour ajouter les éléments nécessaires dans votre projet.

Concentrez-vous d’abord sur la structure Bootstrap. Vous fignolerez les détails de présentation dans la customisation du CSS ultérieurement.

Le « look » nous importe moins que la clarté de la structure et du code associé.

6.1. TODO Téléchargement de « Start Bootstrap - Shop Homepage »

Récupération dans le projet d’une distribution de BootStrap prête à l’emploi.

Start Bootstrap propose différentes distributions de Bootstrap prêtes à l’emploi. Nous vous suggérons de partir de cette distribution, en suivant les étapes ci-après, mais vous pourriez également partir de la distribution « standard » de bootstrap disponible sur le site du projet.

L’avantage de Start Bootstrap est de proposer des « thèmes » prêts à l’emploi.

Pour ToDo, dans le TP 6, on a utilisé une distribution très basique : « StartBootstrap Bare ».

Pour le projet, nous vous suggérons de récupérer par exemple la variante de la distribution : « Shop Homepage », qui propose l’affichage de liste de « produits ».

Vous pourrez adapter la mise en forme ultérieurement selon vos goûts, en explorant d’autres variantes de mise en forme, de thèmes Bootstrap.

  1. Récupérez la distribution Bootstrap Shop Homepage (par exemple avec wget en ligne de commande) :

    cd public
    wget https://github.com/startbootstrap/startbootstrap-shop-homepage/archive/gh-pages.zip
    unzip gh-pages.zip
    

    les ressources statiques ont été extraites dans public/startbootstrap-shop-homepage-gh-pages/

  2. Configurez l’emplacement des ressources assets de Symfony pour pointer dans ce sous-répertoire. Modifiez config/packages/framework.yaml pour ajouter :

    framework:
      ...
        assets:
            base_path: '/startbootstrap-shop-homepage-gh-pages'
    

Bootstrap est prêt à être intégré dans les gabarits pour que le HTML l’utilise.

6.2. Intégration de Bootstrap dans vos templates

Ajout du chargement de Bootstrap dans les gabarits Twig du projet.

Pour ajouter BootStrap dans une page HTML, il faut que celle-ci charge les ressources statiques correspondant à ses feuilles de style CSS, et aux scripts JavaScript associés (dont ceux de la bibliothèque JQuery).

Vous allez ainsi devoir ajouter les en-têtes HTML « classiques » nécessaires dans votre gabarit de base, en indiquant les chemins des CSS via la macro asset() de Twig :

<!-- Core theme CSS (includes Bootstrap)-->
<link href="{{ asset('css/styles.css') }}" rel="stylesheet">

Vous ajouterez ensuite l’invocation des scripts Javascript, à la fin du contenu de la balise <body> du code HTML, également via la macro asset() de Twig :

<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="{{ asset('js/scripts.js') }}"></script>

Pour construire ces éléments de gabarit, vous pouvez vous inspirer du contenu du fichier HTML d’exemple qui est fourni dans la distribution « Shop homepage », présent dans le répertoire public/startbootstrap-shop-homepage-gh-pages/index.html pour reprendre ces éléments HTML et les adapter pour Twig.

Au final, le code de base.html.twig ressemble à :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <meta name="description" content="" />
        <meta name="author" content="" />
        <title>{% block title %}Welcome!{% endblock %}</title>
        <!-- Favicon-->
        <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">

        {% block stylesheets %}
              <!-- Bootstrap icons-->
              <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet" />
              <!-- Core theme CSS (includes Bootstrap)-->
          <link href="{{ asset('css/styles.css') }}" rel="stylesheet">
        {% endblock %} {# stylesheets #}
    </head>
    <body>
       {# ... #}

       {% block body %}

         {# ... #}

       {% endblock %}{# body #}

        {% block javascripts %}
          <!-- Bootstrap core JS-->
          <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
          <!-- Core theme JS-->
          <script src="{{ asset('js/scripts.js') }}"></script>
        {% endblock %} {# javascripts #}
    </body>
</html>


Si vous rechargez les pages de l’application, elles incluent désormais la mise en forme par défaut de BootStrap.

6.3. Intégration des menus pour Bootstrap

Ajout d’un composant de gestion de menus de navigation dans l’application.

Pour intégrer le composant de fabrication de menus compatible BootStrap, qui a été observé pour l’application ToDo en TP 6, procédez aux étapes suivantes :

  1. Créez le fichier de configuration config/packages/bootstrap_menu.yaml, en recopiant celui de l’application ToDo.
  2. Installez le bundle correspondant :

    symfony composer require camurphy/bootstrap-menu-bundle
    
  3. Ajoutez le code HTML pour le menu Bootstrap dans votre gabarit de base, pour insérer des menus dans les <nav> des pages de l’application.

    Inspirez-vous du code de public/startbootstrap-shop-homepage-gh-pages/index.html, pour l’ajouter dans un bloc Twig menu placé avant le bloc body, dans base.html.twig. Vous ne gardez que la partie englobante du <nav>, jusqu’au <ul> </ul>, qui ressemblera donc à :

    {# ... #}
    <body>
    
         {% block menu %}
         <!-- Navigation -->
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container px-4 px-lg-5">
                    <a class="navbar-brand" href="{{ path('[inventory]_list') }}">[nom-code]</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0 ms-lg-4">
    
                        </ul>
                    </div>
                </div>
            </nav>
         {% endblock %}{# menu #}
    
        {% block body %}
    
          {# ... #}
    
        {% endblock %}{# body #}
    
    
  4. Vous pouvez ensuite insérer l’utilisation de render_bootstrap_menu() dans le <ul> </ul>. Vous obtiendrez par exemple, dans base.html.twig quelque chose du style :

    {# ... #}
    <body>
    
         {% block menu %}
         <!-- Navigation -->
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container px-4 px-lg-5">
                    <a class="navbar-brand" href="{{ path('inventory_list') }}">My app</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0 ms-lg-4">
                          {{ render_bootstrap_menu('main') }}
                        </ul>
                    </div>
                </div>
            </nav>
         {% endblock %}{# menu #}
    
        {% block body %}
    
          {# ... #}
    
        {% endblock %}{# body #}
    
    
  5. Configurez enfin les menus à afficher, en ajoutant les routes correspondantes, dans config/packages/bootstrap_menu.yaml.

Suivez la documentation du README pour plus de détails sur son fonctionnement.

Pour l’instant, commencez avec un menu minimal, que vous pourrez enrichir au fur et à mesure de l’ajout de nouvelles fonctionnalités dans les contrôleurs de l’application.

7. TODO Ajout au modèle de données de l’entité membre et du lien membre - [inventaire]

L’objectif de cette section est de préciser certains détails relatifs à l’ajout au modèle de données de l’entité « Membre ».

Ajoutez l’entité membre au modèle de données Doctrine en utilisant l’assistant générateur de code make:user (et pas make:entity, comme pour les autres entités).

Contrairement à d’autres entités du modèle, il n’y a pas besoin de spécialiser son nom en fonction du domaine fonctionnel particulier que vous avez choisi.

  1. Exécutez l’assistant make:user, et dans le dialogue, appelez la classe Member directement (et non User) :

    $ symfony console make:user
    
    The name of the security user class (e.g. User) [User]:
    > Member
    
    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/Member.php
    created: src/Repository/MemberRepository.php
    updated: src/Entity/Member.php
    updated: config/packages/security.yaml
    
    
     Success! 
    
    
    Next Steps:
      - Review your new App\Entity\Member class.
      - Use make:entity to add more fields to your Member entity and then run make:migration.
      - Create a way to authenticate! See https://symfony.com/doc/current/security.html
    

    On a utilisé le générateur de code make:user et non make:entity, car cette classe Member va jouer un rôle particulier. Elle fait à la fois partie du modèle de données, pour gérer les [inventaires] disjoints de différents membres, mais aussi pour permettre la gestion du contrôle d’accès des utilisateurs qui accèdent à l’application.

    On verra le rôle de l’email comme identifiant de login et le mot-de-passe, qui seront introduits bien plus tard, lorsqu’on aura vu la gestion de l’authentification dans Symfony

  2. Ensuite, avec le générateur de code make:entity, ajoutez l’association « membre (1) — (1) [inventaire] », de type OneToOne.

    Le générateur de code make:entity permet de générer une classe à partir de rien, mais aussi d’enrichir des classes existantes en leur ajoutant des attributs nouveaux ou des associations avec d’autres entités du modèle de données. Y compris une classe générée spécialement avec make:user.

    Ici, on a une association 1-1, car chaque membre a son propre [inventaire]. Globalement, un telle association fonctionne comme une association OneToMany, mais « abatardie ».

    Nous n’allons pas détailler ici toutes les opérations nécessaires, pour compléter cet ajout, vous laissant le soin de les retrouver.

Attention à la charge de travail, et à bien respecter les étapes du cours : on ne gérera pas, pour l’instant, l’ensemble des attributs d’un profil de membre (pour gagner du temps).

On verra plus tard à ajouter des propriétés plus réalistes à l’entité Membre, une fois qu’on se sera assuré que l’authentification fonctionne bien :

Nom attribut Type Contraintes Commentaire
nom string notnull  
description string nullable  

8. TODO Ajout du code de chargement des données de test des Membres

Vous pouvez désormais ajouter aux Fixtures la génération de données pour l’entité Member.

Pour l’instant, il suffit d’initialiser des nouveaux membres de façon un peu triviale. Comme la classe Member introduit des éléments liés à l’authentification, il faut donner des mots-de-passe aux membres créés.

On peut utiliser le code suivant, à ajouter à AppFixtures.php, qui effectue le branchement avec le gestionnaire d’authentification de Symfony :

<?php

// ...

use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

//...

class AppFixtures extends Fixture
{
    private UserPasswordHasherInterface $hasher;

    public function __construct(UserPasswordHasherInterface $hasher)
    {
        $this->hasher = $hasher;
    }

    //...
}

Ensuite, on peut ajouter une méthode qui génère des données pour la création des membres, et l’ajout des entités à la base, avec le code suivant à ajouter aux bons endroits :

<?php

// ...

class AppFixtures extends Fixture
{
    //...

    /**
     * Generates initialization data for members :
     *  [email, plain text password]
     * @return \\Generator
     */
    private function membersGenerator()
    {
        yield ['olivier@localhost','123456'];
        yield ['slash@localhost','123456'];
    }

    //...
    public function load(ObjectManager $manager): void
    {
        foreach ($this->membersGenerator() as [$email, $plainPassword]) {
            $user = new Member();
            $password = $this->hasher->hashPassword($user, $plainPassword);
            $user->setEmail($email);
            $user->setPassword($password);

            // $roles = array();
            // $roles[] = $role;
            // $user->setRoles($roles);

            $manager->persist($user);
        }
        $manager->flush();

        //...

Ce code sera complété ultérieurement quand il s’agira du contrôle d’accès.

9. TODO Ajout de l’entité [galerie] au modèle des données

L’entité [galerie] peut maintenant être ajoutée au modèle de données.

Si vous avez déjà commencé à l’ajouter, ce n’est pas grave. Mais on n’en a pas parlé précédamment, car la galerie est censée servir à un affichage « public » aux autres membres, et on souhaite donc avoir un présentation plus soignée.

Ajoutez l’entité avec le générateur make:entity de façon classique, en la spécialisant selon votre domaine thématique.

Évitez de choisir le nom de classe « Collection » pour ne pas entrer en conflit avec un nom déjà utilisé dans une classe de bibliothèque de Symfony.

On peut avoir un ensemble de propriétés mono-valuées minimal pour [galerie] dans un premier temps :

Nom attribut Type Contraintes Commentaire
description string notnull  
publiée boolean notnull publiée ou non par son créateur

On ajoutera également les associations suivantes, au fil de l’eau :

  • [créateur] pour enregistrer qui gère cette [galerie] : relation « Membre (1) — (0..n) [galerie] » : OneToMany (on pourrait commencer avec OneToOne, mais qui peut le plus peut le moins). Celle-ci est assez classique et ressemble à celle qu’on a déjà mise en place pour le lien entre [inventaire] et [objet].
  • une association plus délicate « [galerie] (0..n=) — (0..n) [objet] » : ManyToMany, permettant à un membre d’afficher le même [objet] dans différentes [galeries].

    En soi, cette seconde association ManyToMany n’est pas très complexe à ajouter via make:entity par contre, elle s’avère plus délicate à gérer dans les règles de gestion qu’on va devoir mettre en place, comme on va le voir ci-après.

10. TODO Génération d’un nouveau contrôleur CRUD au front-office, pour [galerie]   Après_TP_7

Sur le modèle de ce qui a été fait pendant le TP 7 sur l’application ToDo, vous allez maintenant ajouter au front-office un Contrôleur Symfony pour gérer l’ensemble des opérations CRUD sur les [galeries].

Cela permettra de proposer aux membres un ensemble de pages permettant de gérer leurs [galeries].

Vous utiliserez l’assistant générateur de code make:crud pour créer cette nouvelle classe contrôleur et ses gabarits (on oublie désormais make:controller).

10.1. Génération du code du Contrôleur pour [galerie]

Exécutez l’assistant Symfony make:crud, pour la classe [Galerie] du modèle de données

symfony console make:crud

Vous pouvez tester que si les formulaires gérant le CRUD fonctionnent bien, sur http://localhost:8000/[galerie]/

10.2. Ajout de la consultation de la liste des [objet] d’une [galerie]

Modifiez le gabarit de la page de consultation d’une [galerie], pour y afficher la liste des [objets] présents dans cette [galerie].

Vous pouvez vous inspirer de l’affichage d’une liste similaire que vous avez déjà faite dans templates/[inventaire]/show.html.twig :

<tr>
    <th>[Objets]</th>
    <td>
        <ul>
          {% for [objet] in [galerie].[objets] %}
          <li>
            {{ [objet] }}
          </li>
          {% endfor %}
        </ul>
    </td>
</tr>

10.3. Customisation de l’affichage

Poursuivez sur les étapes découvertes en TP 7, pour customiser un peu l’affichage dans les gabarits générés par l’assistant, afin d’avoir un affichage correct des [galeries] avec Bootstrap.

Vous pourrez améliorer encore cela après le TP 8, pour gérer l’affichage des messages flash.

À ce stade, les formulaires de modification des données des [galeries] ont un comportement erroné, car ils permettent de construire une [galerie] à partir des [objets] d’un autre membre.

Nous vous proposons de ne pas essayer de régler cela pour l’instant, et d’attendre un peu plus tard pour corriger cela pour de bon.

11. TODO État des lieux des contrôleurs

Faisons le point sur la façon d’avancer.

À ce stade, vous avez probablement plusieurs contrôleurs fonctionnels :

  • pour [inventaire] : un contrôleur (créé « à la main », avec make:controller) gérant :
    • la consultation d’une liste d’[inventaires] : cela doit devenir un outil du back-office réservé aux administrateurs du site (seuls les administrateurs peuvent lister des [inventaires], puisqu’il n’y a qu’un [inventaire], pour un membre « normal »)
    • la consultation d’un [inventaire] donné (et de la liste de ses [objets], qui sera accessible à son membre propriétaire)
  • pour [objet] : un contrôleur [Objet]Controller (probablement créé également avec make:controller) gérant la consultation d’un [objet]… il lui manque des fonctions CRUD pour qu’un membre puisse gérer les [objets] de son [inventaire].
  • pour [galerie] : un contrôleur CRUD complet

Nous souhaiterions maintenant ajouter des fonctions CRUD pour les [objets] d’un membre, dans son [inventaire]. On va ajouter cela avec l’assistant make:crud pour nous simplifier le travail.

Mais comme il existe déjà un [Objet]Controller, en générer un nouveau risque de causer des conflits.

Nous allons donc modifier notre code, pour laisser la place à make:crud.

Or ça tombe bien, car nous souhaitons aussi pouvoir afficher les [objets] des [galeries] publiques dans le site. Cela va permettre de recycler le code existant de consultation d’un [objet].

Procédez aux opérations suivantes :

  1. déplacement de la méthode [Objet]Controller::show() dans la classe [Galerie]Controller :
    • renommage de la méthode en conséquence : [object]Show(), par exemple
    • renommage de la route pour l’aligner avec les routes de [galerie] (par exemple : app_[galerie]_[objet]_show)
  2. déplacement du gabarit templates/[objet]/show.html.twig vers templates/[galerie]/[objet]show.html.twig;
  3. suppression du fichier source src/Entity/[Objet]Controller.php
  4. génération d’un nouveau contrôleur pour [Objet] avec make:crud

    Il ne devrait pas y avoir de conflit, à la génération des fichiers :

    created: src/Controller/[Objet]Controller.php
    created: src/Form/[Objet]Type.php
    created: templates/[objet]/_delete_form.html.twig
    created: templates/[objet]/_form.html.twig
    created: templates/[objet]/edit.html.twig
    created: templates/[objet]/index.html.twig
    created: templates/[objet]/new.html.twig
    created: templates/[objet]/show.html.twig
    

La consultation des [objets] gérée par ce nouveau contrôleur [Objet]Controller (qui vient d’être généré avec make:crud) va être réservée à la gestion d’un objet par son membre propriétaire, authentifié, dans le contexte de son [inventaire].

12. TODO Contextualisation de la consultation des [objets] depuis les [galeries] publiques

Revenons sur la page de consultation d’un [objet], qu’on a déplacée ci-dessus dans le contrôleur des [galeries] (avec son template associé).

Elle va faire l’affichage « public » des [objets] à partir du moment ou leur propriétaire a décidé de les rendres visibles aux autres membres, en les ayant placés dans une de ses [galeries] publiques.

Cette page va être accessible depuis la page de consultation de différentes [galeries], car on a une association ManyToMany permettant d’afficher un même [objet] dans plusieurs [galeries] (du même propriétaire).

Comme on peut souhaiter revenir à la [galerie] d’origine depuis cette page de consultation d’un [objet], on va devoir contextualiser les chemins de routes associées.

Voici les modifications à effectuer (en partant du gabarit et en remontant vers les contrôleurs) :

  1. Ainsi, dans le gabarit d’affichage de l’[objet], dans templates/[galerie]/[objet]show.html.twig on affichera le lien correspondant à la bonne [galerie]. On modifie l’ancien lien qui permettait de revenir à l’[inventaire], pour qu’il donne maintenant ceci :

    <a href="{{ path('app_[galerie]_show', { 'id': [galerie].id}) }}">back to [galerie]</a>
    
  2. Il faudra donc transmettre l’identifiant précis de cette [galerie] à l’appel à render() (du style : $this->render('[galerie]/[objet]show.html.twig', [ '[objet]' => $[objet], '[galerie]' => $[galerie] ]).
  3. Mais cela nécessite donc que la méthode du contrôleur reçoive à la fois l’[objet] et la [galerie] en arguments (ils seront donc présents dans le chemin d’accès à la ressource, dans l’URL consultée lors de la requête HTTP GET).

    Dans la route d’accès à la page d’affichage de l’objet, on ajoutera donc les deux identifiants de [galerie] et d’[objet], avec des arguments nommés [galerie]_id, et resp. [objet]_id. Exemple dans templates/[galerie]/show.html.twig :

    <h2>[Objets]</h2>
    <ul>
       {% for [objet] in [galerie].[objets] %}
       <li>
         <a href="{{ path( 'app_[galerie]_[objet]_show',
                           {
                             '[galerie]_id': [galerie].id,
                             '[objet]_id' : [objet].id
                           }
                         ) }}">{{ [objet] }}</a>
       </li>
       {% endfor %}
    </ul>
    
  4. Ainsi, la méthode du contrôleur [Galerie]Controller::[objet]Show recevra donc deux entités Doctrine en argument, pour charger depuis la base de données les entités correspondant aux arguments de la route [galerie]_id et [objet]_id.

    Afin qu’il n’y ait pas d’ambiguïté, sur le mapping entre identifiants dans le chemin de l’URL, on va spécifier le mapping des ID et des classes, pour le mécanisme EntityValueResolver du contrôleur :

    • [galerie]_id qui chargera une entité dans [Galerie]
    • [objet]_id, dans [Objet]

    Cela donne le code suivant qui utilise le décorateur MapEntity (attributs MapEntity), dans la déclaration des arguments de la méthode :

    use Symfony\Bridge\Doctrine\Attribute\MapEntity;
    
    //...
    
       #[Route('/{[galerie]_id}/[objet]/{[objet]_id}',
               methods: ['GET'],
               name: 'app_[galerie]_[objet]_show')]
       public function [objet]Show(
           #[MapEntity(id: '[galerie]_id')]
           [Galerie] $[galerie],
           #[MapEntity(id: '[objet]_id')]
           [Objet] $[objet]
       ): Response
       {
           return $this->render('[galerie]/[objet]_show.html.twig', [
               '[objet]' => $[objet],
               '[galerie]' => $[galerie]
           ]);
       }
    

    Example de code réel, si on consulte le code d’un projet de gestion de [Galerie] nommée « Rig », avec [Objet] nommé « Guitar » :

    /**
     * Show a guitar in the context of a rig
     *
     * @param Rig $rig              the rig which diplays the guitar (galerie)
     * @param Guitar $guitar    the guitar to display (objet)
     */
    #[Route('/{rig_id}/guitar/{guitar_id}/', 
        name: 'app_rig_guitarshow', 
        requirements: ['rig_id' => '\d+',
                       'guitar_id' => '\d+'
                      ])]
    public function guitarShow(
        #[MapEntity(id: 'rig_id')]
        Rig $rig,
        #[MapEntity(id: 'guitar_id')]
        Guitar $guitar) : Response
    {
        return $this->render('rig/guitarshow.html.twig',
            [ 'guitar' => $guitar,
              'rig' => $rig,
            ]
            );
    }
    

Voilà, à ce stade, on peut tester que la navigation sur la consultation entre une [galerie] et les [objets] qui y apparaissent est cohérente, et ne renvoie plus vers les [inventaires].

Attention : si on laissait ce code tel-quel, on créerait une faille permettant l’accès à n’importe quel [objet] de la base de données qu’il soit ou non référencé dans la [galerie] (publique ou pas), et qu’il ait ou non le bon propriétaire !

On peut corriger cela en vérifiant que l’[objet] et la [galerie] en question sont cohérents, et que cette dernière est publique. Adaptez le code suivant, qui s’appuie sur les exceptions pour effectuer les vérifications nécessaires :

public function [objet]Show([Galerie] $[galerie], [Objet] $[objet]): Response
{
        if(! $[galerie]->get[Objets]()->contains($[objet])) {
                throw $this->createNotFoundException("Couldn't find such a [objet] in this [galerie]!");
        }

        // if(! $[galerie]->isPublished()) {
        //   throw $this->createAccessDeniedException("You cannot access the requested ressource!");
        //}

        return $this->render('[galerie]/[objet]_show.html.twig', [
                '[objet]' => $[objet],
                  '[galerie]' => $[galerie]
          ]);
}

On voit ici l’utilisation de deux exceptions différentes générant des codes de réponse appropriés : 404 (« Not found »), ou 403 (« Access denied »).

Plus tard si le temps n’est pas compté, on pourrait affiner (optionnellement) pour permettre à un membre d’accéder à un objet d’une de ses galeries privées, ou à l’administrateur d’accéder à tout.

13. TODO Ajout des données de tests dans les Fixtures

Il est peut-être temps de refaire un point sur l’usage des Data Fixtures qui permettent de peupler la base de données avec des données de test réalistes.

Vous avez maintenant ajouté toutes les entités utiles au modèle de données, et on voit qu’on commence à aborder la phase où un membre, en fonction de son profil (administrateur ou pas), ne devra pouvoir accéder qu’aux données qui le concerne : son [inventaire] et ce qu’il contient, ou les [objets] des [galeries] des autres membres (pourvu qu’elles soient publiques), et ses propres [galeries].

Pour tester tout cela, il est nécessaire d’avoir beaucoup de données liées par les associations 1-N ou M-N dans la base de données de tests.

Pour coder l’ajout des objets dans les Data Fixtures, il y a deux façons possibles de coder l’algorithme de chargement :

  • première variante :
    • pour chacun des membres à ajouter :
      1. ajout du membre
      2. ajout de l’[inventaire] de ce membre
      3. ajout d’une liste d’[objets] dans cet inventaire
    • une fois que tout cela est en mémoire, sauvegarder
    • ajouter les [galeries] des membres et les liens avec les [objets] existants (*)
  • ou, deuxième variante :
    • ajouter tous les membres (et sauvegarder)
    • ajouter tous les [inventaires] de chacun des membres (et sauvegarder)
    • ajouter tous les [objets] dans leurs [inventaires] (et sauvegarder)
    • ajouter les [galeries] des membres et les liens avec les [objets] existants (*)

La première variante peut être codée plus simplement, avec des générateurs contenant des structures de données arborescentes :

  • membre
    • [inventaire]
      • liste des [objets] de l’inventaire
    • [galerie]

La deuxième variante peut être codée avec plusieurs générateurs, mais en gérant des références entre les entités :

  • membres
  • [inventaires] (référençant les bons membres)
  • [objets] (référançant les bons [inventaires])
  • [galeries] (référençant les bons membres)

Dans tous les cas, l’association M-N des [objets] dans les [galeries] (notée (*), ci-dessus), nécessite de gérer des références, entre des entités existant déjà.

Le code gérant l’association de deux entités existantes (en mémoire) peut ressembler à ce qui suit. Prenons l’exemple de l’ajout d’un [objet], dans un [inventaire], pourvu que l’[inventaire] existe déjà, et que la l’attribut qui identifie un [inventaire] soit par exemple name :

  • soit un générateur générateur[Objets](), qui renvoie un nuplet ([inventaire]_name, [objet]_name, …)
  • le code de création des [objets] est :
    • pour tous les nuplets renvoyés par générateur[Objets]() :
      • $[objet] = new [objet]
      • $[objet]->setName([objet]_name)
      • recherche de l’[inventaire] identifié par name : $[inventaire] = $[inventaire]Repository->findByName([inventaire]_name)
      • ajout dans le bon [inventaire] : $[inventaire]->add[Objet]($[objet])
      • sauvegarde

Le code permettant de charger des entités Doctrine depuis leur repository, dans la méthode load() de la classe de fixtures ressemblerait alors à ceci :

//...

use App\Entiry\[Inventaire];

class AppFixtures extends Fixture
{

    // ...

    public function load(ObjectManager $manager)
    {
        $[inventaire]Repository = $manager->getRepository([Inventaire]::class);

        // ...

Nous vous suggérons d’utiliser une autre façon de faire, comme alternative à cette recherche de l’entité [inventaire] existante avec un find() : s’appuyer plutôt sur le mécanisme des références des DataFixtures, présenté en annexe : Gestion de « références » dans les Fixtures.

Quelle que soit la façon dont vous codez ce chargement de données, en utilisant beaucoup de références, ou en préférant la première variante, prenez le temps de vérifier que les données en base sont cohérentes, avec SQLite (par exemple avec symfony console dbal:run "select * from ...".

Pour la mise au point du code de chargement, appuyez-vous sur dump() et les logs dans var/log/dev.log qui tracent les requêtes INSERT…

14. Contextualisation de l’accès aux données d’un membre, et restriction des opérations permises

Une fois qu’on a ajouté des contrôleurs CRUD dans le front-office, l’application permet de modifier un peu toutes les données. De plus, on peut consulter tous les [inventaires], tous les [objets].

Or, le principe d’une telle application est de permettre de gérer les données de multiples utilisateurs dans une même base de données, mais que chaque utilisateur n’ait accès qu’à ce qui le concerne. Seuls les administrateurs, qui utilisent le back-office, peuvent avoir accès à tout.

Nous allons donc devoir adapter le fonctionnement des contrôleurs tels qu’ils ont été générés par défaut, afin de restreindre les fonctionnalités permises.

14.1. TODO Ajout d’un Controleur pour l’entité Membre

On va ainsi définir un « point d’entrée » dans l’application, qui sera la consultation d’un Membre. Depuis la consultation d’un Membre, on aura accès à son [inventaire], et de là aux [objets], etc.

  1. Ajoutez une classe Contrôleur pour l’entité Membre, avec make:controller (cette fois, il s’agit de consulter les membres, dans le front-office : pas besoin d’un CRUD. S’il y a un CRUD des membres, ce sera pour le back-office).
  2. Ajoutez deux méthodes index() et show(), et le contenu des gabarits correspondants, comme on l’a fait auparavant pour d’autres entités, pour afficher :
    • la liste des membres (celle-ci aura vocation à disparaître ultérieurement). Inspirez-vous du code de [Galerie]Repository::index(), par exemple;
    • et la fiche de chaque membre. Inspirez-vous de [Galerie]Repository::show(), par exemple.
  3. Dans la fiche d’un membre, affichez un lien vers la consultation de son inventaire :

    Par exemple, on obtiendrait ceci dans le gabarit templates/member/show.html.twig :

    <a href="{{ path('app_[inventaire]_show', {'id' : member.[inventaire].id}) }}"> mon [inventaire] </a>
    
  4. Dans la consultation d’un inventaire, le lien de retour doit revenir vers le membre et non-plus vers la liste des inventaires

    <a href="{{ path('app_member_show', {'id': [inventaire].member.id}) }}">back to member</a>
    

14.2. TODO Contextualisation de la création d’un [objet] dans l’[inventaire] d’un membre

On doit maintenant ajuster la création d’un nouvel [objet], qui dépendra alors du membre courant, pour l’ajouter dans le bon inventaire.

La route app_[objet]_new changera donc pour prendre en compte ce nouveau contexte où on passe l’identifiant de l’[inventaire] en argument.

  1. Modifiez la route CRUD de création d’un nouvel [objet] :

    Extrait de src/Controller/[Objet]Controller.php :

    #[Route('/[objet]/new/{id}', name: 'app_[objet]_new', methods: ['GET', 'POST'])]
    public function new(Request $request, EntityManagerInterface $entityManager, [Inventory] $inventory): Response
    {
        $objet = new [Objet]();
        $objet->setInventory($inventory);
    
        //...
    
        if ($form->isSubmitted() && $form->isValid()) {
            //...
    
            return $this->redirectToRoute('app_[inventaire]_show',
                                          ['id' => $inventory->getId()],
                                          Response::HTTP_SEE_OTHER);
        }
    

    On a modifié la méthode new() pour ajouter le chargement de l’[inventaire] passé dans l’argument de la route, et initialiser le nouvel [objet] comme appartenant à cet [inventaire].

  2. Déplacez le lien de création d’[objet] : supprimez-le de la page de liste des [objets] (templates/[objet]/index.html.twig) pour le placer dans l’affichage de l’[inventaire] (templates/[inventaire]/show.html.twig), et modifiez l’appel de la route :

    <a href="{{ path('app_[objet]_new', {'id': [inventaire].id}) }}">Add new</a>
    
  3. On peut alors ajouter une customisation du formulaire de création pour ne pas autoriser la modification de l’attribut [inventaire] d’un [objet], puisqu’il est fixé par le contexte d’invocation.

    Ainsi dans la classe gérant le formulaire src/Form/[Objet]Type.php :

    class [Objet]Type extends AbstractType
    {
            public function buildForm(FormBuilderInterface $builder, array $options): void
            {
                 $builder
                         ->add('description')
                  //...;
    
              $builder->add('[inventory]', null, [
                                 'disabled'   => true,
                         ])
                 ;
            }
    

    Ici, l’attribut faisant le lien, [inventory], est désactivé. Il apparaît dans le formulaire de création de l’[objet], mais il est grisé (et renseigné à la bonne valeur, puisqu’on a appliqué le setter à l’initialisation de l’objet en mémoire).

  4. Modifiez la redirection en fin de modification d’un [objet] pour rediriger de la même façon vers la page de son [inventaire], plutôt que vers la liste des [objets] :

    ...redirectToRoute('app_[inventaire]_show', ['id' => $[objet]->get[Inventaire]()->getId()], Response::HTTP_SEE_OTHER);

  5. Modifiez la redirection en fin de suppression d’un objet pour revenir à la page de son [inventaire]

15. Contextualisation des opérations CRUD modifiant les [galeries]

Les opération en modification des [galeries] doivent être contextualisées par rapport au Membre qui est leur créateur.

15.1. TODO Affichage de la liste des [galeries] dans la consultation du membre

Ajoutez la consultation de la liste des [galeries] d’un membre, dans la page de consultation du Membre.

Copiez le code du gabarit gabarit utilisé par index() dans le contrôleur [Galerie]Controller, pour l’ajouter dans celui utilisé par MemberController::show().

Adaptez, pour afficher la liste des galeries du membre.

15.2. TODO Contextualisation de la création d’une [galerie]

On doit ensuite modifier la création d’une nouvelle [galerie] pour qu’elle soit contextualisée à partir du membre (cf. étape ci-dessus pour la contextualisation de la création des [objets] dans un [inventaire]).

On ajoute enfin le lien de création dans la page de consultation du Membre, et on le supprime de la page de consultation de la liste des galeries.

15.3. TODO Contextualisation des redirections en modification d’une [galerie]

De façon similaire, on doit répercuter le même genre de changements sur les redirections en fin de modification ou suppression d’une [galerie].

15.4. TODO Contextualisation de l’affichage des [galeries] publiques

Ensuite, on peut judicieusement modifier l’affichage de la liste des [galeries], pour ne charger que les galeries déclarées comme publiques. Il suffit pour cela de replacer le findAll() appelé sur le [Galerie]Repository, en findBy(['published' => true]).

16. TODO Contextualisation de l’ajout d’un [objet] à une [galerie]

Comme on peut le voir en testant la modification, le formulaire de modification d’une [galerie], tel que généré par l’assistant make:crud permet de saisir des [objets] quelconques pour les placer dans une galerie.

Nous allons essayer de corriger cela, pour charger à la place, uniquement des [objets] du membre.

Il s’agit d’agir dans la classe gestionnaire du formulaire de modification d’une [galerie], [Galerie]Type (dans src/Form/[Galerie]Type.php), pour obtenir quelque chose du type :

use App\Repository\[Object]Repository;

//...
class [Galerie]Type extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        //dump($options);
        // Get the current [object] from 'data' option passed to the form    
        $[galerie] = $options['data'] ?? null;
        // get the [galerie]'s creator
        $member = $[galerie]->getCreator();

        $builder
            ->add('description')
            ->add('published')
            ->add('creator', null, [
                    'disabled'   => true,
                  ])
            ->add('[objets]', null, [
                // adjust the loading of possible [objects] to those of the current member's [inventory]
                // the use helps pass the member to the lambda
                'query_builder' => function ([Objet]Repository $er) use ($member) {
                                      return $er->createQueryBuilder('o')
                                                ->leftJoin('o.[inventaire]', 'i')
                                                ->leftJoin('i.member', 'm')
                                                ->andWhere('m.id = :memberId')
                                                ->setParameter('memberId', $member->getId())
                                                ;
                                        }
                                ])
                ;
        }

Le principe est de construire le champ de formulaire [objets], qui gère la modification de la collection des [objets] apparaissant dans la [galerie], en spécifiant une méthode particulière de chargement des données, au lieu de charger tous les [objets] de la base. On utilise donc l’option query_builder des formulaires pour lui passer une fonction (anonyme) qui va construire la requête en base.

La requête doit faire une jointure avec les [inventaires] du même membre. Le membre propriétaire de la [galerie] est récupérable à partir des données du formulaire, présentes dans $options['data'] (une instance de la classe [Galerie]; en cas de doute, utilisez dump() pour vérifier). Une « astuce » à bien noter est l’utilisation du use ($member) dans la définition de la fonction anonyme, qui permet de transférer une copie du membre à la fonction, pour qu’elle s’en serve dans le WHERE.

Vous devrez ajuster les noms d’attributs, d’entités et de classes pour coller à votre modèle de données particulier.

Ce mécanisme de « fonctions anonymes » (closures) est issu du paradigme de programmation fonctionnelle, qui est également disponible en PHP, mais qu’on retrouve dans d’autres contextes également, par exemple en Javascript. L’utilisation du use pour faire le lien avec une variable du contexte d’exécution courant est décrite dans la documentation PHP : https://www.php.net/manual/en/functions.anonymous.php.

Ce mécanisme avancé de programmation est loin d’être trivial, et montre encore une fois que Symfony intègre des patrons de conception et d’implémentation modernes.

Vérifiez dans la barre d’outils Symfony, avec l’icône et la section « Forms », les détails des options du formulaire du sous-formulaire du champ « [objets] » pour voir si le « query builder » est bien défini, et dans l’onglet Doctrine, la requête transmise à la base de données.

Attention : Si vous modifiez le formulaire « edit » du controleur CRUD des [objets] pour y ajouter la gestion des [galeries], vous risquez de rencontrer des erreurs ou des problèmes de sauvegarde.

On peut probablement régler ces problèmes par l’utilisation d’une option « magique » (pour nous) des types de champs de formulaire by_reference. La documentation de EntityType Field n’est pas hyper claire sur le sujet… voir par exemple le cours The by_reference Form Option de SymfonyCasts qui approfondit ça en détails, si on a le temps de tout lire.

En modifiant les options du constructeur de formulaire pour le champ de l’association manyToMany, on obtient alors un code du style :

//use Symfony\Bridge\Doctrine\Form\Type\EntityType;

class [Galerie]Type extends AbstractType
{
    //...
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            //...
            // par défaut, si on ne spécifie pas le type de champ de formulaire
            // c'est un EntityType
            //->add('[objets]')
            ->add('[objets]',
                  //EntityType::class,
                  null,
                  // options :
                  [
                      // avec 'by_reference' => false, sauvegarde les modifications
                      'by_reference' => false,
                      // classe pas obligatoire
                      //'class' => [Object]::class,
                      // permet sélection multiple
                      'multiple' => true,
                      // affiche sous forme de checkboxes
                      'expanded' => true
                  ]
            )
            ;
        ;
    }
}

Ça permet de gérer les objets au sein du formulaire de modification de la galerie… Mais plutôt cryptique si on n’a pas tous les commentaire !

On voit là la limite des mécaniques de base fournies par Symfony… mais si on y réfléchit bien, gérer la modification d’une collection d’entités liées via un simple champ de formulaire… ce n’est pas ce qu’on trouve habituellement dans les applications Web usuelles.

Dans une « vraie application » on distinguera la page de modification des propriétés d’une [galerie], de la page d’ajout du contenu de la galerie… ou du moins, c’est géré dans du code différent des mécanismes de formulaires Symfony « basiques » tels qu’ils sont générés par make:crud.

Des solutions sont possibles, avec les formulaires avancés, des dialogues, un enchaînement de pages, du Javascript… mais pas les formulaires CRUD simples de Symfony. Ça demanderait beaucoup plus de travail, donc nous n’avons pas demandé cela dans le projet.

On est donc ici en limite de ce qu’on peut aborder en détails dans le cours (faute de temps), donc de ce qu’on peut attendre dans les projets.

Si ça marche, tant mieux… sinon, ce n’est pas pénalisable. Vous avez fait de votre mieux, avec ce qu’on a étudié.

17. TODO Ajout de la gestion de la mise en ligne d’images

En vous inspirant du support vu en TP ou en travail en autonomie, ajoutez des propriétés à votre classe [Objet], et modifiez le contrôleur CRUD, pour permettre d’ajouter des photos dans les [objets].

Vous pouvez aussi modifier la page de consultation des [galeries] pour afficher les images des [objets] de la [galerie].

18. TODO Ajout de l’authentification   Après_TP_8

Ajoutez le support de l’authentification tel que découvert en TP 8 sur l’application fil-rouge.

Une partie des étapes a déjà été faite, puisque la classe « User » a déjà ajoutée (Member)

Il faut maintenant dérouler le reste des modifications (formulaire de login, etc.)

Une fois que les utilisateurs seront connectés, on pourra connaître l’utilisateur courant, et modifier le comportement du login, pour directement afficher les informations du membre correspondant, plutôt que d’avoir à naviguer dans la liste des membres pour entamer l’utilisation de l’application.

On ne gérera pas de procédure d’inscription au site, et de création de profil de membre, ce qui allongerait le projet, et n’est pas utile sur une application pédagogique. On peut utiliser les fixtures, pour ajouter des utilisateurs et leurs membres dans la base de données.

18.1. Note relative aux dépendences des Fixtures

Vous pouvez reprendre le code de chargement de données de tests des utilisateurs fourni en TP 8 pour ajouter la génération des utilisateurs dans les Data Fixtures.

Au lieu de tout rassembler dans le code d’une seule classe AppFixtures, on peut séparer le code dans différents fichiers source. Il suffit alors d’ajouter, à côté du fichier src/DataFixtures/AppFixtures.php existant, un nouveau fichier source UserFixtures.php récupéré du code de Todo, en y adaptant le nom de la classe (en remplaçant User par Member).

Si on utilise ainsi deux fichiers source, on est alors potentiellement confronté à l’ordre de chargement des données des fixtures des Membres par rapport à celles des [Inventaires]. Pour s’assurer que les données sont chargées dans le bon ordre, on peut définir les fixtures d’AppFixtures comme dépendantes de celles de UserFixtures :

// ...
use Doctrine\Common\DataFixtures\DependentFixtureInterface;


class AppFixtures extends Fixture implements DependentFixtureInterface
{

        // ...

        public function getDependencies()
        {
                return [
                        UserFixtures::class,
                ];
        }

Ainsi, on pourra, dans AppFixtures charger d’abord les instances de Member, puis s’y relier à la création des [Inventaires] associés, en chargeant les Membres par leur email, avec findOneByEmail() :

foreach (self::[Inventaire]DataGenerator() as [$name, $memberemail] ) {
        $[inventaire] = new [Inventaire]();
        if ($memberemail) {
                $member = $manager->getRepository([Inventaire]::class)->findOneByEmail($memberemail);
                $[inventaire]->setUser($member);
        }
        $[inventaire]->setName($name);
        $manager->persist($[inventaire]);
}
$manager->flush();

19. Ajout de contrôle d’accès effectif (optionnel)

Une fois que les utilisateurs sont authentifiés, on doit restreindre les fonctions de l’application aux utilisateurs autorisés.

Comme cela représente un travail important, cette étape du projet est optionnelle. Évidamment, dans une application réelle, elle serait fondamentale.

19.1. Accès en consultation au seul propriétaire (ou administrateur)

La consultation des entités via les méthodes show() des contrôleurs CRUD doit être réservée aux seuls propriétaires de ces données, ou aux administrateurs. Ainsi, on peut refuser l’accès avec un code du type :

$hasAccess = $this->isGranted('ROLE_ADMIN') ||
    ($this->getUser() == $[inventaire]->getOwner());
if(! $hasAccess) {
    throw $this->createAccessDeniedException("You cannot access another member's [inventory]!");
}

19.2. Accès en modification au seul propriétaire des données

Les méthodes des contrôleurs gérant les formulaires de CRUD (new(), edit() et delete() doivent être adaptés également, pour s’assurer que l’utilisateur authentifié peut légitimement les invoquer, car il est propriétaire des données.

Certaines devront être réservées aux administrateurs, ou simplement supprimées, car on imaginera un autre mécanisme pour la désinscription d’un membre, que d’appeler la méthode delete() du contrôleur MemberController dans formulaire de suppression tel que généré par make:crud. Dans tous les cas, le back-office sera là également en cas de besoin.

En parlant de suppression de données, on devra faire attention à ce que les suppressions ne laissent pas de scories en base de données (voir plus loin).

19.3. Accès aux [galeries] non publiques

Pour les [galeries] non publiées, on doit restreindre l’accès aux utilisateur propriétaires de ces [galeries] (ou aux administrateurs), il faudrait mettre en place un code un peu plus fin, du type :

#[Route('/{id}', name: 'app_[galerie]_show', methods: ['GET'])]
public function show([Galerie] $[galerie]): Response
{
        $hasAccess = false;
        if($this->isGranted('ROLE_ADMIN') || $[galerie]->isPublished()) {
                $hasAccess = true;
        }
        else {
                $member = $this->getUser();
                if ( $member &&  ($member == $[galerie]->getCreator()) ) {
                    $hasAccess = true;
                }
        }
        if(! $hasAccess) {
                throw $this->createAccessDeniedException("You cannot access the requested resource!");
        }
        return $this->render('[galerie]/show.html.twig', [
                '[galerie]' => $[galerie],
        ]);
}

Notez que ce code est verbeux, mais compliqué à écrire car on n’est pas sûr qu’un utilisateur soit connecté, et on doit donc progresser en testant l’existance des instances avant d’appeler leur méthodes.

19.4. Accès aux [objets] des [galeries] non publiques.

De façon similaire, on va retoucher le code de l’accès aux [objets] dans une [galerie] non publique, si elle appartient à l’utilisateur courant :

public function [objet]Show([Galerie] $[galerie], [Objet] $[objet]): Response
{
        if(! $[galerie]->get[Objets]()->contains($[objet])) {
                throw $this->createNotFoundException("Couldn't find such a [objet] in this [galerie]!");
        }

        $hasAccess = false;
        if($this->isGranted('ROLE_ADMIN') || $[galerie]->isPublished()) {
                $hasAccess = true;
        }
        else {
                $member = $this->getUser();
          if ( $member &&  ($member == $[galerie]->getCreator()) ) {
              $hasAccess = true;
          }
        }
        if(! $hasAccess) {
                throw $this->createAccessDeniedException("You cannot access the requested ressource!");
        }

        return $this->render('[galerie]/[objet]_show.html.twig', [
                '[objet]' => $[objet],
                  '[galerie]' => $[galerie]
          ]);
}

Au final, l’algorithme devient complexe, et pourrait probablement receler beaucoup des bugs.

Plutôt que de coder ainsi ce genre de conditions d’accès complexes, par rapport au statut d’un utilisateur, on pourrait utiliser (si on avait le temps de les étudier) le mécanisme des voteurs de Symfony, pour définir des permissions et les vérifier, en débarassant le code d’une partie de la complexité. Cf. How to Use Voters to Check User Permissions, pour votre culture.

20. Contextualisation du chargement des données en fonction de l’utilisateur courant

Une fois que l’authentification des utilisateurs est opérationnelle, on va pouvoir modifier le fonctionnement des méthodes index() des contrôleurs qui chargent l’ensemble des données pour [objet], [inventaire] … afin qu’elles ne chargent que les données accessibles à l’utilisateur connecté.

Si l’utilisateur est un administrateur, il a accès à toutes les données de la base de données. On peut alors garder le fonctionnement initialement généré par le maker make:crud : charger tout avec un findAll().

Par contre, si l’utilisateur authentifié est un utilisateur normal, associé à un membre du site, on ne veut charger que ses données.

20.1. Affichage des [galeries]

Seules les [galeries] publiques sont visibles par tous les utilisateurs.

Les [galeries] non publiques sont visibles uniquement par leur propriétaire.

L’administrateur peut tout consulter.

Exemple de code de sélection des [galeries] privées d’un utilisateur authentifié :

$private[Galeries] = array();
$member = $this->getUser();
if($user) {
        $private[Galeries] = $[galerie]Repository->findBy(
                [
                      'published' => false,
                      'creator' => $member
                ]);
}

20.2. Affichage des [objets] et [inventaires]

On peut donc modifier le code des méthodes de chargement en utilisant un code du style (dans [Objet]Controller) :

#[Route('/', name: 'app_[objet]_index', methods: ['GET'])]
public function index([Objet]Repository $[objet]Repository): Response
{
        if ($this->isGranted('ROLE_ADMIN')) {
                $[objets] = $[objet]Repository->findAll();
        }
        else {
                $member = $this->getUser();
                $[objets] = $[objet]Repository->findMember[Objets]($member);
        }

On va donc s’appuyer sur une méthode ajoutée au Repository de l’entité [objet] pour charger les données d’un membre.

Par exemple, ici, dans src/Repository/[Objet]Repository.php :

/**
 * @return [Objet][] Returns an array of [Objet] objects for a member
 */
 public function findMember[Objets](Member $member): array
{
        return $this->createQueryBuilder('o')
                 ->leftJoin('o.[inventory]', 'i')
                 ->andWhere('i.owner = :member')
                 ->setParameter('member', $member)
                 ->getQuery()
                 ->getResult()
         ;
}

Cette méthode va charger uniquement les [objets] du membre passé en argument.

Pour charger le ou les [inventaires] d’un membre, par contre, c’est plus simple, car on a déjà un lien entre membre et [inventaire] dans le modèle de données, donc pas besoin d’ajouter une méthode de chargement supplémentaire dans le repository. On utilise directement $[inventaires] = $member->get[Inventaires]().

21. Compléter les mécanismes de suppression de données (optionnel)

Par défaut, les makers liés à Doctrine génèrent les données correctement, sans avoir besoin de trop retoucher le code.

Par contre, il nous resterait à vérifier que les suppressions fonctionnent correctement, en fonction des règles de notre application, pour ne pas laisser des scories en base de données

21.1. Suppression des [objets] d’un [inventaire] supprimé

Lorsqu’un [inventaire] est supprimé, on va décider qu’on ne doit pas garder en base de donnée les [objets] qu’il contenait. Dans une application réelle, on pourrait procéder différemment, par exemple en proposant à l’utilisateur de transférer ses fiches d’[objets] dans une « poubelle », permettant éventuellement de les récupérer pour les remettre dans un autre inventaire (ou de gérer le don à un autre utilisateur, etc.)

Ici, pour supprimer les [objets] contenus, on va modifier le comportement de la méthode remove() du repository [Inventaire]Repository, qui est appelée par le gestionnaire de la suppression dans le contrôleur. On codera quelque chose du style :

public function remove([Inventaire] $entity, bool $flush = false): void
{
        $[objet]Repository = $this->getEntityManager()->getRepository([Objet]::class);

        // clean the [objets] properly
        $[objets] = $entity->get[Objets]();
        foreach($[objets] as $[objet]) {
                $[objet]Repository->remove($[objet], $flush);
        }
        $this->getEntityManager()->remove($entity);

        if ($flush) {
                $this->getEntityManager()->flush();
        }
}

On appelle, de façon « transitive », la méthode remove() sur le repository des [objets].

On aurait pu obtenir un résultat équivalent avec le mécanisme de cascade de Doctrine, mais ce code nous semble plus explicite, moins « auto-magique ».

Ensuite, de façon similaire on va modifier cette même méthode pour défaire proprement les associations ManyToMany de l’[objet] supprimé (ci-dessous).

21.2. Gestion des ManyToMany (optionnel)

Nos [objets] sont associés à différentes entités [galeries] par des associations ManyToMany dans le modèle de données.

Si on n’ajoute pas la suppression effective, il y a des chances que la base de données contienne de nombreuses scories pour des liens référençant des [objets] qui auront été supprimés.

Pour s’en aperçevoir, on peut supprimer tous les [inventaires] et donc tous leurs [objets] (cf. ci-dessus), et faire des requêtes SQL. On voit alors qu’il reste de nombreux couples dans les tables d’association des ManyToMany, qui référencent encore les [object]_id supprimés.

On n’aura probablement pas à constater des bugs pour autant (d’où le fait que cette étape soit optionnelle), car les jointures faites par Doctrine au chargement, par exemple, ne feront pas apparaître ces liens manquant, ne prenant en compte que les données existantes dans la table d’[objet]. Néanmoins, on court un risque de saturation inutile de la base de données.

Pour faire le ménage proprement, il convient de modifier la méthode remove() de l’[Objet]Repository, un peu comme pour le cas précédent.

Première étape : supprimer l’association entre l’[objet] et les [galeries] dans lesquelles il était présent :

public function remove([Objet] $entity, bool $flush = false): void
{
        $[galerie]Repository = $this->getEntityManager()->getRepository([Galerie]::class);

        // get rid of the ManyToMany relation with [galeries]
        $[galeries] = $[galerie]Repository->find[Objet][Galeries]($entity);   
        foreach($[galerie]s as $[galerie]) {
                $[galerie]->remove[Objet]($entity);
                $this->getEntityManager()->persist($[galerie]);
        }
        if ($flush) {
                  $this->getEntityManager()->flush();
        }
        //...

Si nous avons une méthode permettant de supprimer un [objet] d’une [galerie], [Galerie]::remove[Objet](), il nous manquait encore le moyen de charger les galeries référençant un [objet]. C’est le rôle de la méthode de chargement ajoutée au [Galerie]Repository, find[Objet][Galeries]() :

/**
 * @return [Galerie][] Returns an array of [Galerie] objects
 */
 public function find[Objet][Galeries]([Objet] $[objet]): array
{
    return $this->createQueryBuilder('g')
        ->leftJoin('g.[objets]', 'o')
        ->andWhere('o = :[objet]')
        ->setParameter('[objet]', $[objet])
        ->getQuery()
        ->getResult()
    ;
}

Cette méthode renvoit les [galeries] qui référencent un [objet] passé en argument.

Une fois qu’on a cette méthode, on appelle [Galerie]::remove[Objet]() pour chacune des galeries, et on prend soin de bien faire un persist() sur ces galeries ainsi « modifiées ».

On ajoute un flush() au cas où un remove($entity) juste après annulerait le principe des persist().

Enfin, on peut supprimer l’[objet] lui-même :

    //...

        $this->getEntityManager()->remove($entity);

        if ($flush) {
                $this->getEntityManager()->flush();
        }
}

Vérifiez que cela fonctionne bien, en chargeant les fixtures, et en supprimant à nouveau tous les [inventaires] et leurs [objets] : les tables d’association des différentes ManyToMany doivent être vides. Vous pouvez aussi vérifier ce qui se passe à la suppression d’un [objet] dans un formulaire, en regardant les requêtes DELETE SQL qui sont envoyées, dans l’outil « Doctrine » de la barre d’outils Symfony.

22. Autres fonctionnalités

Vous pouvez vous inspirer du contenu des séances de TP pour ajouter des fonctions supplémentaires à l’application (de façon optionnelle) :

  • utilisation des messages flash pour tous les formulaires des CRUDs
  • ajout d’une gestion de marque-pages dans le front-office pour mettre des « likes » ou faire une liste d’[objets] préférés dans les galeries publiques des autres membres

Vous pourriez également penser à ajouter une entité Commentaire au modèle des données pour gérer des messages envoyés aux propriétaires des [objets] consultés dans les galeries publiques… mais attention à la complexité de gestion d’un forum, la modération, etc. Nous vous déconseillons de vous lancer dans une telle entreprise vue la charge de travail que cela impliquerait.

Demandez plus de détails aux encadrants en cas de doute sur la faisabilité de telles fonctionnalités.

23. Annexes

23.1. Gestion de « références » dans les Fixtures

On va devoir coder le chargement de données liées entre-elles. Pour cela, yield va nous permettre d’itérer dans des tableaux définis dans le code.

Pour la gestion des entités liées entre-elles, on devra identifier des entitées dans ces tableaux. Pour simplifier celà, on pourra utiliser des références à des objets, une des fonctionnalités du module de Fixtures. C’est documenté dans la documentation du module DoctrineFixturesBundle.

Par exemple, pour l’ajout de guitares dans des racks, supposons qu’on a deux entités, resp. Guitar et Rack.

Mettons qu’on ait 2 racks :

  • « Matos guitare d’Olivier »
  • « 400 guitars collection »

On peut avoir 3 guitares, qu’on souhaite répartir ainsi :

  • « Epiphone SG Special P-90 » et « Ibanez SA360NQM » dans le rack « Matos guitare d’Olivier »
  • « Gibson Les Paul 1960 », dans le rack « 400 guitars collection »

Le code des fixtures va commencer par charger l’entité Rack dans la base, puis faire le chargement de l’autre entité Guitare.

Au moment de créer les guitares, les racks existent déjà, et pour ranger les guitares dans leurs racks on souhaterait identifier les racks de façon simple dans notre code. Plutôt que de devoir gérer le nom littéral de chaque rack, on va leur associer des identifiants, qui seront des constantes de la classe. Cela aura l’avantage de permettre à l’IDE de faire de la complétion, sur le nom de ces constantes, plutôt que sur des chaînes où on peut faire des erreurs de typo.

On utilisera addReference() et getReference() pour donner un tel identifiant à chaque rack.

On peut donc utiliser un code du genre :

<?php

namespace App\DataFixtures;

class AppFixtures extends Fixture
{
    // defines reference names for instances of Rack
    private const SLASH_RACK = 'slash-inventory';
    private const OLIVIER_RACK = 'olivier-inventory';

    /**
     * Generates initialization data for racks : [title, rack reference]
     * @return \\Generator
     */
    private static function rackDataGenerator()
    {
        yield ["Matos guitare d'Olivier", self::OLIVIER_RACK];
        yield ["400 guitars collection", self::SLASH_RACK];
    }

    /**
     * Generates initialization data for guitars :
     *  [rack reference, guitar description]
     * @return \\Generator
     */
    private static function guitarsGenerator()
    {
        yield [self::OLIVIER_RACK, "Epiphone SG Special P-90"];
        yield [self::OLIVIER_RACK, "Ibanez SA360NQM"];
        yield [self::SLASH_RACK, "Gibson Les Paul 1960"];
    }

    public function load(ObjectManager $manager)
    {
        // $inventoryRepo = $manager->getRepository(Rack::class);

        foreach (self::rackDataGenerator() as [$title, $rackReference] ) {
            $rack = new Rack();
            $rack->setTitle($title);
            $manager->persist($rack);
            $manager->flush();

            // Once the Rack instance has been saved to DB
            // it has a valid Id generated by Doctrine, and can thus
            // be saved as a future reference
            $this->addReference($rackReference, $rack);
        }

        foreach (self::guitarsGenerator() as [$rackReference, $model])
        {
            // Retrieve the One-side instance of Rack from its reference name
            $rack = $this->getReference($rackReference);
            $guitar = new Guitar();
            $guitar->setDescription($model);
            // Add the Many-side Guitar to its containing rack
            $rack->addGuitar($guitar);

            // Require that the ORM\OneToMany attribute on Rack::guitars has "cascade: ['persist']"
            $manager->persist($rack);
        }
        $manager->flush();
    }
}

Quelques explications :

  • OLIVIER_RACK et SLASH_RACK sont nos constantes qui jouent le rôle d’identifiant, dans cette classe seulement. Ici, ce qui importe c’est qu’elles aient des valeurs (chaînes de caractères) uniques, quelconques. On va les manipuler dans le code qu’on écrit, par leur nom de constante, connu de l’IDE car déclaré (private const ...) : cela évite des typos.
  • le yield dans le rackDataGenerator() renvoie l’identifiant OLIVIER_RACK ou SLASH_RACK qui est attribué via addReference() suite au new Rack()
  • le yield dans le guitarsGenerator() réutilise ces même identifiant, qui serviront à faire des getReference() pour rechercher le rack ($rack) sur lequel faire le $rack->addGuitar($guitar) qui associe la guitare au bon rack. Cela évite un code plus lourd à écrire, qui utiliserait find().

Normalement, ça se lit comme de l’objet, et ça évite de devoir gérer des identifiants interne en base de données où à faire des requêtes de recherche des racks dans la base.

Attention à bien faire des flush() au bon endroit (dans les boucles internes, juste après le persist()), pour que les identififants soient générés par la base de données, avant qu’on puisse appeler le addReference().

Author: Olivier Berger (TSP)

Date: 2024-10-25 Fri 13:17

Emacs (Org mode)