Guide de réalisation du projet
Table des matières
- 1. Introduction
- 2. Étape proj-1 : Démarrage du projet Symfony
- 2.1. TODO Étape proj-1-a : Prise de connaissance du cahier des charges de l’application
- 2.2. TODO Choix du domaine fonctionnel
- 2.3. TODO Étape proj-1-b : Initialisation du répertoire de travail du projet
- 2.4. TODO Configuration de Git
- 2.5. TODO Création du projet Symfony
- 2.6. TODO Étape proj-1-c : Tests de lancement de l’application
- 2.7. TODO Étape proj-1-d : Mise à jour du référentiel Git
- 3. Étape proj-2 : Génération des premières entités du modèle de données
- 3.1. Méthodologie : travail incrémental
- 3.2. TODO Préparation de l’ajout des premières entités
- 3.3. Étape proj2-a : Rappel au sujet des outils de mise en œuvre
- 3.4. TODO Étape proj2-b : Configuration du type de base de données de tests
- 3.5. TODO Étape proj2-c : Ajoutez le support des DataFixtures
- 3.6. TODO Étape proj2-d : Création effective des premières entités
- 4. Étape proj-3 : Création des premières pages publiques, en consultation Après_TP_4
- 4.1. Méthodologie pour la génération du code pour ajouter des pages en consultation
- 4.2. TODO Étape proj3-a : Ajout d’un premier Contrôleur pour [inventaire]
- 4.3. TODO Étape proj3-b : Ajout de la méthode d’affichage de la liste des [inventaires]
- 4.4. TODO Étape proj3-c : Ajout de la consultation d’un [inventaire]
- 4.5. TODO Étape proj3-d : Ajout du lien de consultation, à partir de l’affichage de la liste
- 5. TODO Ajout des gabarits dans les pages Après_TP_5
- 6. Ajout habillage CSS dans les gabarits Après_TP_6
- 7. TODO Ajout au modèle de données de l’entité membre et du lien membre - [inventaire]
- 8. TODO Ajout du code de chargement des données de test des Membres
- 9. TODO Ajout de l’entité [galerie] au modèle des données
- 10. TODO Génération d’un nouveau contrôleur CRUD au front-office, pour [galerie] Après_TP_7
- 11. TODO État des lieux des contrôleurs
- 12. TODO Contextualisation de la consultation des [objets] depuis les [galeries] publiques
- 13. TODO Ajout des données de tests dans les Fixtures
- 14. Contextualisation de l’accès aux données d’un membre, et restriction des opérations permises
- 15. Contextualisation des opérations CRUD modifiant les [galeries]
- 16. TODO Contextualisation de l’ajout d’un [objet] à une [galerie]
- 17. TODO Ajout de la gestion de la mise en ligne d’images
- 18. TODO Ajout de l’authentification Après_TP_8
- 19. Ajout de contrôle d’accès effectif (optionnel)
- 20. Contextualisation du chargement des données en fonction de l’utilisateur courant
- 21. Compléter les mécanismes de suppression de données (optionnel)
- 22. Autres fonctionnalités
- 23. Annexes
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 :
- création du projet Symfony
- 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
- lisez le Cahier des charges de l’application;
- 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
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).
Vérifiez si vous avez une configuration des variables de configuration de Git adaptée :
git config user.email
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]"
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
où [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 :
- ajout au modèle de données de quelques entités fortement liées (sans être exhaustif sur leurs propriétés)
- 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 :
- réalisation de premières interfaces de consultation de ces données
- raffinement des propriétés à gérer pour ces premières entités
- 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 :
- première liste d’entités pour débuter le noyau de l’application :
- [inventaire]
- [objet]
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é :
- définir dans le code des classes PHP des attributs Doctrine (
ORM\...
) gérant la persistence de l’entité en base de données - 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 :
- création du fichier de stockage de la base (
symfony console doctrine:database:create
), - 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) :
- suppression de la base (
symfony console doctrine:database:drop
) - re-création de la base (
symfony console doctrine:database:create
), - re-création du schéma (
symfony console doctrine:schema:create
)
- suppression de la base (
- 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 :
- Ajout de l’entité (par ex. [inventaire]) avec ses propriétés
mono-valuées minimales :
- Appel à
make:entity
pour générer le nouveau code PHP (sauf pour « Membre », voir ci-dessous) - Application des changements sur le schéma de la base
- Ajout dans les Fixtures du code de chargement de données de tests minimales
- Chargement des nouvelles données de tests pour cette entité (ça fonctionne !)
- Appel à
Ajout de l’entité suivante (par ex. [objet]) avec ses propriétés mono-valuées minimales :
… (mêmes étapes que pour [inventaire])…
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.
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.- 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
. 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
lancez le serveur Web :
symfony server:start
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 ».
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 :
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
- 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.
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/
Configurez l’emplacement des ressources
assets
de Symfony pour pointer dans ce sous-répertoire. Modifiezconfig/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 :
- Créez le fichier de configuration
config/packages/bootstrap_menu.yaml
, en recopiant celui de l’applicationToDo
. Installez le bundle correspondant :
symfony composer require camurphy/bootstrap-menu-bundle
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 Twigmenu
placé avant le blocbody
, dansbase.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 #}
Vous pouvez ensuite insérer l’utilisation de
render_bootstrap_menu()
dans le<ul> </ul>
. Vous obtiendrez par exemple, dansbase.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 #}
- 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.
Exécutez l’assistant
make:user
, et dans le dialogue, appelez la classeMember
directement (et nonUser
) :$ 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 nonmake:entity
, car cette classeMember
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
Ensuite, avec le générateur de code
make:entity
, ajoutez l’association « membre (1
) — (1
) [inventaire] », de typeOneToOne
.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 avecmake: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 avecOneToOne
, 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 viamake: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 avecmake: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 :
- 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
)
- renommage de la méthode en conséquence :
- déplacement du gabarit
templates/[objet]/show.html.twig
verstemplates/[galerie]/[objet]show.html.twig
; - suppression du fichier source
src/Entity/[Objet]Controller.php
génération d’un nouveau contrôleur pour
[Objet]
avecmake: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) :
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>
- 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] ])
. 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 danstemplates/[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>
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
(attributsMapEntity
), 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 :
- ajout du membre
- ajout de l’[inventaire] de ce membre
- 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 (*)
- pour chacun des membres à ajouter :
- 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]
- [inventaire]
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
- pour tous les nuplets renvoyés par
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.
- 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). - Ajoutez deux méthodes
index()
etshow()
, 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.
- la liste des membres (celle-ci aura vocation à disparaître
ultérieurement). Inspirez-vous du code de
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>
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.
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].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>
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).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);
- 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
etSLASH_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 lerackDataGenerator()
renvoie l’identifiantOLIVIER_RACK
ouSLASH_RACK
qui est attribué viaaddReference()
suite aunew Rack()
- le
yield
dans leguitarsGenerator()
réutilise ces même identifiant, qui serviront à faire desgetReference()
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 utiliseraitfind()
.
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()
.