Guide de réalisation du projet

Table des matières

1. Introduction

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

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

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

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

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

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

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

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

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

  1. création du projet Symfony
  2. mis en place du modèle de données initial
  3. mise en place du back-office d’administration avec EasyAdmin

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

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

2.2. TODO Choix du domaine fonctionnel

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

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

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

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

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

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

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

    mkdir $HOME/CSC4101/projet/
    

2.4. TODO 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 new [nom-code] --version="6.3.*" --webapp

[nom-code] est à adapter (par exemple symfony new myguitars --version="6.3.*" --webapp).

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

Chargez ensuite le code du projet dans votre IDE.

2.5. 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 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.6. 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 issues du cahier des charges.

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

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

  1. ajout au modèle de données de quelques entités fortement liées (sans être exhaustif sur leurs propriétés)
  2. ajout de données de test minimales à charger en base de données
  3. test du fonctionnement sur des manipulations basiques, avec l’ajout du tableau de bord d’administration EasyAdmin

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

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

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

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

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

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

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

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

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

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

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

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

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 et qu’on sait modifier toutes ces entités, dans des formulaires Web opérationnels, on peut ajouter un peu plus de données 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.

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.

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, à traduire dans le code PHP/Doctrine en 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].

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

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

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

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

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

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

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

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

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

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

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

Il permet de créer de zéro, dans src/Entity/, un nouvelle classe PHP intégrant les attributs PHP 8 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 PHP existant sera mis à jour.

Bien-sûr make:entity reste optionnel pour 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 est nécessaire de (re-)générer la base de données de tests (SQLite) avec les outils de Doctrine.

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

La création se passe en deux temps :

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

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

  • si tout va bien, mise à jour directe via l’outil schema:update dédié (symfony console doctrine:schema:update), sans suppression des données existantes
  • en cas de soucis, on peut recréer la base de données (à vide) :
    1. suppression de la base (symfony console doctrine:database:drop)
    2. re-création de la base (symfony console doctrine:database:create),
    3. re-création du schéma (symfony console doctrine:schema:create)

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, donc rejouable à loisir, à chaque modification du modèle de données.

3.3.3. Étape proj2-b : Chargement de données de tests

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.

3.3.3.1. Rappel sur la gestion des références

Pour la gestion des associations entre entités, on pourra utiliser des références, telles qu’elles sont documentées dans la documentation du module DoctrineFixturesBundle.

Par exemple, pour l’ajout de guitares dans des racks on peut utiliser quelque chose du genre :

<?php

namespace App\DataFixtures;

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

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

    /**
     * Generates initialization data for film recommendations:
     *  [film_title, film_year, recommendation]
     * @return \\Generator
     */
    private static function guitarsGenerator()
    {
        yield [self::OLIVIER_RACK_1, "Epiphone SG Special P-90"];
        yield [self::OLIVIER_RACK_1, "Ibanez SA360NQM"];
        yield [self::SLASH_RACK_1, "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);

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

3.4. TODO 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 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 Création effective des premières entités

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

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

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

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

    mêmes étapes, globalement, sauf qu’on rappelle make:entity sur l’entité [inventaire] existante pour la modifier, avec une propriétés multi-valuée de type relation (OneToMany), et qu’on gère les Fixtures avec des références (cf. ci-dessus)

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

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.

4. Ajout d’une interface EasyAdmin avec les 2 premiers contrôleurs Crud [inventaire] et [objet]   Après_TP_3

Vous allez ajouter un tableau de bord d’administration fonctionnant grâce à EasyAdmin.

Il permettra de faire fonctionner le futur module back-office de l’application, et surtout de tester les modifications dans votre modèle de données au fur-et-à-mesure de l’avancement du projet.

Il est utile d’avoir fait le TP « mini-allociné » en amont de cette étape, puisque les opérations décrites ici auront été faites déjà une première fois dans ce TP.

4.1. Ajout du contrôleur d’administration principal

Pour cela, déroulez les instructions suivantes (en vous référant à la documentation https://symfony.com/bundles/EasyAdminBundle/current/index.html pour plus de détails) :

  1. ajout du bundle EasyAdmin au projet, avec Composer :

    cd $HOME/CSC4101/projet/[nom-code]
    symfony composer req admin
    
  2. création du controleur du tableau de bord d’administration src/Controller/Admin/DashboardController.php :

    symfony console make:admin:dashboard
    
  3. Lancez le serveur Web de l’environnement de développement Symfony :

    symfony server:start
    
  4. Testez dans le navigateur qu’une page « Welcome to EasyAdmin 4 » s’affiche bien sur l’URL /admin (sur http://locahost:8000/admin par exemple).

4.2. Ajout du contrôleur CRUD pour [inventaire]

Vous allez ajouter au tableau de bord d’administration un premier contrôleur « CRUD » d’EasyAdmin qui servira à afficher et modifier les entités [inventaire].

Il ne s’agit pas encore, pour l’instant, d’afficher les [objet] d’un inventaire, mais juste de pouvoir ajouter, modifier, supprimer et consulter les propriétés mono-valuées des [inventaires] dans la base.

Pour mémoire, on utilise ici la syntaxe générique « [mot] », mais vous devez adapter à votre propre nomenclature, décidée au démarrage du projet, quand vous avez choisi un domaine fonctionnel particulier.

Attention au copier coller : des chemins ou noms de classes avec des caractères « [ » ou « ] », ça ne devrait pas le faire…

  1. création du contrôleur de gestion CRUD des [inventaire] src/Controller/Admin/[Inventaire]CrudController.php :

    symfony console make:admin:crud
    
  2. réalisez ensuite le « câblage » de [Inventaire]CrudController dans l’affichage de la page principale (suivez les instructions données par la page /admin).

    Vous pouvez vous inspirer du code des classes de src/Controller/Admin/ dans ToDo pour voir comment contourner certaines difficultés.

    Ne jonglez pas trop avec toutes les customisations de configureFields() : pour l’instant on souhaite juste un affichage basique. Vous améliorerez les détails plus tard.

  3. Testez que cela fonctionne et que les modifications sont possibles en base de données pour des [inventaires].

4.3. Ajout du contrôleur CRUD pour [objet]

De la même façon, ajoutez ensuite au tableau de bord d’administration, le second contrôleur CRUD, pour l’entité [objet].

Si vous testez le fonctionnement de l’ajout d’objets, vous allez vite constater qu’il n’est pas possible de créer des objets isolés, en-dehors d’un inventaire (en tout cas si la référence entre objet et inventaire a été déclaré comme devant être non-nulle.

Vous allez donc ajouter la gestion de la référence à l’[inventaire] dans le contrôleur CRUD de l’[objet], pour cette association ManyToOne. Le principe consiste à ajouter, dans les champs du [Objet]CrudController d’EasyAdmin, un champ de sélection de type AssociationField, qui affichera alors l’entité [inventaire] liée :

class [Objet]CrudController extends AbstractCrudController
{

    // ...

    public function configureFields(string $pageName): iterable
    {
        return [
            //IdField::new('id'),
            TextField::new('description'),
            AssociationField::new('[inventaire]')
        ];
    }

}

Une fois ajouté ce champ AssociationField via la méthode configureFields() de [Objet]CrudController, on peut tester l’ajout d’un nouvel objet, mais aboutit à une erreur indiquant qu’il n’y a pas de méthode pour convertir [inventaire] en chaîne de caractères (« Object of class App\Entity\[Inventaire] could not be converted to string »).

Il suffit de corriger cela en ajoutant une méthode __toString() dans [inventaire].

Une fois que ça fonctionne, on s’aperçoit qu’EasyAdmin affiche bien une nouvelle colonne dans la liste des [objets], qui affiche leurs [inventaires].

On ajoutera la gestion du lien entre un [inventaire] et ses [objets] ultérieurement, lorsqu’on aura étudié plus en détail les contrôleurs Web Symfony.

5. 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 la nouvelle entité « Membre ».

Nous n’allons pas détailler ici toutes les opérations nécessaires (déjà vues en TP), vous laissant le soin de les retrouver.

Ajoutez l’entité membre au modèle de données Doctrine en utilisant l’assistant générateur de code. Appelez-la Member directement, sans spécialiser : 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.

Voici quelques possibilités pour les attributs de l’entité Membre :

Nom attribut Type Contraintes Commentaire
nom string notnull  
description string nullable  

Attention à la charge de travail, et à bien respecter les étapes du cours :

  • on ne gérera pas ici, pour l’instant, l’ensemble des attributs d’un profil de membre (pour gagner du temps),
  • on essaiera encore moins de réfléchir à des attributs comme login et mot-de-passe, qui seront introduits bien plus tard, lorsqu’on aura vu la gestion de l’authentification dans Symfony

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

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.

Note : on pourrait aussi commencer avec une association de type OneToOne, mais ce n’est pas indispensable : qui peut le plus peut le moins ? À vous de choisir ce qui vous semble préférable dans le contexte fonctionne choisi pour votre application, si OneToOne vous semble plus réaliste.

6. É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.

Bien évidemment, vous venez de mettre en œuvre des pages Web en consultation avec EasyAdmin, mais de telles pages sont relativement figées et ne permettent pas de gérer une grande sophistication dans les traitements (listes d’entités, formulaires, présentation tabulaire). On les réservera donc pour une partie backoffice qui ne sera pas accessible aux utilisateurs ordinaires de l’application.

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.

6.1. Étape 4-a : Génération du code pour ajouter des pages en consultation

Vous allez générer 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 (alors qu’EasyAdmin gère le back-office). C’est à dire celle utilisée par les membres de la communauté hébergée sur le site.

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

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 relatives à la consultation des autres entités du site.

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 Controleur supportant des interactions CRUD. Cela augmentera encore plus notre productivité, mais il faut d’abord qu’on ait expérimenté le fonctionnement des controleurs, en consultation seulement.

Ne générez pas dès maintenant 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.

6.2. TODO Étape 5 : 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 à un membre de retrouver :

  • une liste de ses [inventaires] ;
  • la consultation d’un [inventaire] particulier.

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

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

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

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

    symfony console make:controller [Inventaire]Controller
    

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

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

    symfony console debug:router
    

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

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

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

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

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

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

    Succès ! \o/

6.3. Étape 5.d : 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 /my/[inventaire]/, par exemple. En effet, in fine, il s’agira d’afficher dans le front-office les [inventaires] d’un membre particulier qui se sera connecté à l’application. Mais, pour l’instant, consultons l’ensemble des entités [inventaire] présentes dans la base de données.

Vous allez donc modifier la méthode correspondante de la classe controleur. 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

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] :
    <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.

Continuons avec d’autres pages gérées par le même controleur.

6.4. TODO Étape 5.e : 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}.

On souhaite afficher à peu près les mêmes informations que ce qu’on avait sur la ligne de commande avec app:show-todo, mais dans une page Web.

Vous allez ajouter 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)
{
        $[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 ToDo). 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.

6.5. TODO Étape 5.f : 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.

Mais avant de s’attaquer à d’autres entités, revenons à notre backoffice réalisé avec EasyAdmin.

7. TODO Ajout au tableau de bord d’administration, des liens des associations OneToMany

Vous avez mis en place dans une étape précédente, le mécanisme permettant de naviguer depuis une page d’affichage d’une liste d’entités vers la page d’affichage d’une fiche pour une entité spécifique de cette liste. Ceci était codé dans les contrôleurs « ordinaires » de Symfony pour la partie front-office.

Vous allez maintenant travailler à ajouter le même genre de liens, mais au sein du tableau de bord d’administration du backoffice, pour gérer le lien à dérouler entre une entité principale et des entités cibles d’une association OneToMany.

Ainsi, on pourra cliquer sur un lien pour naviguer entre une entité [inventaire] et les entités [objets] qu’elle contient, dans les pages produites avec EasyAdmin.

Le principe consiste à ajouter, dans les champs du [Inventaire]CrudController d’EasyAdmin, un champ multi-valué de type AssociationField, qui affichera alors les entités [objet] liées.

Une fois ajouté un champ AssociationField via la méthode configureFields() de [Inventaire]CrudController, on s’aperçoit qu’EasyAdmin affiche bien une nouvelle colonne dans la liste des [inventaires], qui donne un nombre. Ce nombre décompte le nombre d’[objets] de chaque [inventaire] (selon ce que vous avez mis dans la base via les data fixtures).

C’est déjà un premier pas. Vous pouvez vérifier dans la barre d’outil Symfony que les chargement de données dans Doctrine décomptent bien les [objets] liés à chaque inventaire.

7.1. Rendu de la page de modification d’un [inventaire]

Pour voir la liste des objets en question, il faut aller dans le menu (« … ») et sélectionner « Edit ». À ce stade, il faut régler éventuellement un problème récurrent de représentations des classes sous forme textuelle. Une fois que ce problème est réglé, la page de modification de l’[inventaire] affiche bien la liste des [objets] qu’il contient.

On aimerait bien consulter également la liste des [objets] dans une page d’affichage d’une « fiche » pour chaque inventaire, avec des liens cliquables… mais il est encore un peu tôt pour faire cela.

Pour l’instant, on va se contenter de cette page de modification, et on reviendra vers l’affichage d’une page de « fiche » pour chaque inventaire, quand on aura étudié les gabarits Twig.

8. TODO Customisation des affichages dans le tableau de bord (optionnel à ce stade)

EasyAdmin n’est pas destiné à produire un résultat très élégant, donc il n’est pas forcément nécessaire de customiser en détail l’affichage des différentes pages, mais vous pouvez le faire, pour éviter des erreurs de modifications ou de saisie, et rendre le tout plus proche de ce qu’on pourrait attendre sur une application prête pour la production.

Vous pouvez consulter la documentation qui explique comment gérer l’apparition (ou pas) des différents attributs, selon les « formulaires » index, edit, ou show : https://symfony.com/bundles/EasyAdminBundle/current/fields.html. Mais vous constatez vite qu’il y a beaucoup d’options possibles, et la doc n’est pas forcément évidente à comprendre.

Pour vous donner un avant-goût des possibilités, ça se passe dans la méthode configureFields() des CrudControllers, avec quelques pistes potentiellement utiles dans ce premier exemple :

class [Inventaire]CrudController extends AbstractCrudController
{

        public function configureFields(string $pageName): iterable
        {
                return [
                        IdField::new('id')->hideOnForm(),
                        TextField::new('description'),
                        AssociationField::new('[objets]')
                                ->onlyOnDetail(),
                ];
        }

Cela permet de ne pas afficher l’ID des entités dans toutes les pages, ni l’association (pour l’instant le nombre d’[objets]) dans les listes d’[inventaires] (cf. https://symfony.com/bundles/EasyAdminBundle/current/fields.html#displaying-different-fields-per-page).

On reviendra sur d’autres customisations plus tard.

9. TODO Ajout au tableau de bord d’administration des entités actuelles

Complétez le tableau de bord d’administration EasyAdmin pour y ajouter la gestion des entités existantes de votre modèle de données.

Ajoutez ainsi le contrôleur Admin\\MembreCrudController pour l’entité /membre. Puis, ajoutez le lien entre les entités membre et [inventaire(s)] (ainsi que le lien inverse d’inventaire vers son membre propriétaire).

10. 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 (front-office) 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 (normal, il est d’ailleurs utilisé par EasyAdmin).

Vous trouverez aussi dans templates/admin/ les gabarits de votre backoffice qui ont été générés par le maker pour EasyAdmin (on verra plus tard dans le projet qu’on sera amenés à s’en servir), mais pour l’instant travaillons sur le front-office.

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 :
    • index() : templates/[inventaire]/index.html.twig
    • listAction() : templates/[inventaire]/list.html.twig
    • showAction() : templates/[inventaire]/show.html.twig
  • puis [Objet]Controller, resp. :
    • showAction() : 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.

10.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 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].

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

10.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.

10.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.

10.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].

10.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> 

10.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>

Une fois que ça fonctionne bien dans votre front-office vous pouvez passer à l’équivalent dans le backoffice avec EasyAdmin.

11. Ajout de l’affichage des [objets] d’un [inventaire] dans le backoffice

Il est maintenant temps de terminer la navigation entre entités liées par des relations OneToMany dans le backoffice réalisé avec EasyAdmin.

Avant le TP 5, on en était resté au fait que la page d’affichage de la liste des [inventaires] comportait le nombre d’[objets] et on ne savait pas afficher la liste de ces objets.

Voyons comment faire.

11.1. Ajout d’une page d’affichage des détails d’un [inventaire]

On va modifier les actions possibles sur une entité, dans EasyAdmin, pour passer de la liste des entités à l’affichage d’une seule entité.

Étonnament, EasyAdmin ne nous propose pas, par défaut, d’afficher une entité, bien qu’il nous permette de l’éditer ou de la supprimer…

On peut corriger ça en ajoutant la méthode suivante configureActions() à notre [Inventaire]CrudController (cf. https://symfony.com/bundles/EasyAdminBundle/current/actions.html)

class [Inventaire]CrudController extends AbstractCrudController
{
    //...

    public function configureActions(Actions $actions): Actions
    {

        return $actions
            ->add(Crud::PAGE_INDEX, Action::DETAIL)
        ;
    }

    //...

Cela permet d’obtenir un nouveau menu « Show » pour invoquer l’affichage d’une fiche d’[inventaire].

On peut ensuite customiser plus ou moins l’affichage des champs dans cette page (cf. « Customisation des affichages dans le tableau de bord » qui était une section optionnelle auparavant).

11.2. Ajout de la liste des entités liées dans la page de détails

Maintenant qu’on sait afficher une « fiche » avec les détails d’un [inventaire] on va y afficher la liste des [objets] de cet inventaire, au lieu du champ donnant le nombre d’[objets]

Cette séquence est un peu hardue, et nécessite d’avoir bien compris le fonctionnement des gabarits Twig. Il est recommandé d’attendre d’avoir bien digéré les gabarits avant de s’y attaquer (post séance TP 5).

Le principe consiste à configurer l’affichage de l’attribut contenant la collection des [objets] pour lui donner un élément de gabarit Twig spécifique, comme dans le code ci-dessous :

class [Inventaire]CrudController extends AbstractCrudController
{
      //...
          public function configureFields(string $pageName): iterable
          {
                  return [
                          IdField::new('id')->hideOnForm(),
                          TextField::new('description'),
                          AssociationField::new('[objets]')
                                  ->onlyOnDetail()
                                  ->setTemplatePath('admin/fields/[inventaire]_[objets].html.twig')

                  ];
          }

Ce code indique que notre attribut [Inventaire]::[objets] de type OneToMany dans le modèle de données, est affiché sous forme de champ AssociationField, et qu’on ne souhaite l’afficher que dans cette nouvelle page « DETAILS » (menu « Show »), et que l’affichage de la page va devoir prendre en compte un gabarit qui spécialise une partie de l’affichage de cette page.

En effet, on va créer dans notre projet, un fichier de gabarit Twig admin/fields/[inventaire]_[objets].html.twig qui contiendra quelque chose du type :

{# source: https://stackoverflow.com/a/65082524/1814910 #}
  <ul>
{% for [object] in field.value %}
  {%- set url = ea_url()
    .setController('App\\Controller\\Admin\\[Object]CrudController')
    .setAction('detail')
    .setEntityId([object].id)
  -%}
  <li>
  	<a href="{{ url }}">
        {{ [object] }}
  	</a>
  </li>
{% else %}
</ul>  
  <span class="badge badge-secondary">None</span>
  <ul>
{% endfor %}
  </ul>

Mettons les choses au point : vos profs n’ont pas trouvé ça tous seuls, et se sont inspirés de l’exemple donné par « Flash » (résident à Kyiv en Ukraine ?)… (nous aussi on utilise StackOverflow ;-)

Attention à adapter à votre nomenclature : [objet] n’est pas à garder tel-quel, évidamment.

Vous pouvez observer, via l’outil Twig de la barre d’outils Symfony la façon dont une page « DETAILS » est compilée à partir des gabarits de EasyAdmin. C’est très touffu et assez vertigineux… mais cela explique pourquoi EasyAdmin permet d’afficher, de base, autant de widgets graphiques sans avoir à coder en HTML dans des gabarits Twig.

Mais ici, il nous faut bien customiser cet affichage par défaut et ajouter nos propres bouts, en ne surchargeant que le morceau nécessaire.

Au lieu d’afficher le nombre d’éléments dans la collection des [objets] on va afficher une liste à puce <ul><li>...</li> ... </ul>, en itérant pour chaque [objet] de notre champ AssociationField (donc on itère sur les valeurs du champ : field.value). Il ne reste qu’à (vite dit) construire l’URL qui pointe vers l’affichage d’une page de detail pour un [objet]. Et cette URL, c’est celle de l’action detail du controleur EasyAdmin [Object]CrudController… et on passe à cette route la valeur de l’id de notre [objet]… simple à comprendre (?)… difficile à trouver (sur StackOverflow… ou dans la doc, mais nous ne l’avons pas trouvé dans la doc).

Le reste est cosmétique pour que ça soit joli dans l’affichage avec BootStrap que fait EasyAdmin.

Vous venez de voir ainsi un mécanisme qu’on pourra reproduire à loisir dans une backoffice EasyAdmin dès qu’on souhaitera customiser l’apparence.

Maintenant que vous savez le faire, vous pourrez dupliquer cela pour toutes les relations OneToMany nécessaires (sans en abuser non-plus), ou pour tout autre élément de customisation du backoffice.

12. 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é.

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

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

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

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

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

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

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

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

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

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

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

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

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

12.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">
        <title>{% block title %}Welcome!{% endblock %}</title>
        <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.

12.3. Correction des chemins des assets pour EasyAdmin

Une fois que votre front-office s’affiche bien avec le CSS de bootstrap, il se peut fort que votre backoffice ne s’affiche plus bien.

Pour corriger cela :

  1. Modifiez la configuration des assets dans config/packages/framework.yaml pour régler celà :

    framework:
        ...
        assets:
            packages:
                bootstrap:
                    base_path: '/startbootstrap-bare-gh-pages'
    
  2. Modifiez à son tour le gabarit de base de l’application (base.html.twig) pour changer les appels à asset() pour ajouter en argument le nom du paquetage qu’on vient d’ajouter dans la configuration ci-dessus (’bootstrap’), pour utiliser par exemple : asset('css/styles.css', 'bootstrap')
  3. Invoquez la commande : symfony console assets:install

Normalement, l’ensemble des présentations fonctionnent correctement.

12.4. Intégration des menus pour Bootstrap

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

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

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

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

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

    {# ... #}
    <body>
    
         {% block menu %}
         <!-- Navigation -->
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container px-4 px-lg-5">
                    <a class="navbar-brand" href="{{ path('inventory_list') }}">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">
    
                        </ul>
                    </div>
                </div>
            </nav>
         {% endblock %}{# menu #}
    
        {% block body %}
    
          {# ... #}
    
        {% endblock %}{# body #}
    
    
  4. Vous pouvez ensuite insérer l’utilisation de render_bootstrap_menu() dans le <ul> </ul>. Vous obtiendrez par exemple, dans base.html.twig quelque chose du style :

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

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

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

13. 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.

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 :

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

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

14. Ajout de [galerie] dans le tableau de bord d’administration (optionnel)

Comme pour l’[inventaire] des [objets] on peut facilement ajouter au tableau de bord d’administration EasyAdmin, la gestion de la [galerie] des [objets].

Attention toutefois à ne pas permettre l’ajout, dans une [galerie] d’un membre, d’[objets] présents dans l’[inventaire] d’un autre membre.

Pour ce faire, on pourra, dans un premier temps, limiter les valeurs chargées dans le champ de choix des [objets] à associer à une [galerie].

Avec EasyAdmin 3.x ce genre de choses est faisable, mais un peu complexe à mettre en place, avec un code du type :

class [Galerie]CrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return [Galerie]::class;
    }

    public function configureFields(string $pageName): iterable
    {

        return [
            IdField::new('id')->hideOnForm(),
            AssociationField::new('[creator]'),
            BooleanField::new('published')
                ->onlyOnForms()
                ->hideWhenCreating(),
            TextField::new('description'),

            AssociationField::new('[objets]')
                ->onlyOnForms()
                // on ne souhaite pas gérer l'association entre les
                // [objets] et la [galerie] dès la crétion de la
                // [galerie]
                ->hideWhenCreating()
                ->setTemplatePath('admin/fields/[inventaire]_[objets].html.twig')
                // Ajout possible seulement pour des [objets] qui
                // appartiennent même propriétaire de l'[inventaire]
                // que le [createur] de la [galerie]
                ->setQueryBuilder(
                    function (QueryBuilder $queryBuilder) {
                        // récupération de l'instance courante de [galerie]
                        $current[Galerie] = $this->getContext()->getEntity()->getInstance();
                        $[createur] = $current[Galerie]->get[Createur]();
                        $memberId = $[createur]->getId();
                        // charge les seuls [objets] dont le 'owner' de l'[inventaire] est le [createur] de la galerie
                        $queryBuilder->leftJoin('entity.[inventaire]', 'i')
                            ->leftJoin('i.owner', 'm')
                            ->andWhere('m.id = :member_id')
                            ->setParameter('member_id', $memberId);    
                        return $queryBuilder;
                    }
                   ),
        ];
    }

Explications :

  • on peut ajouter une fonction spécifique pour le chargement des données proposées dans l’interface de l’AssociationField, pour la modification de la collection des [objets] liés à notre [galerie], via l’attribut multivalué [Galerie]::[objets]. C’est faisable via l’appel à AssociationField::setQueryBuilder()
  • ce chargement des [objets] des inventaires d’un même propriétaire que le créateur de notre [galerie]= peut se faire via une requête SQL de type :

    select * from [objet]
           LEFT JOIN [inventaire i] on [inventory]_id [i].id
           LEFT JOIN [member m] on i.owner = m.id
           WHERE m.id = :member_id
    

    member_id est la valeur de l’identifiant du [créateur] de la [galerie] en cours de modification

  • la partie non-documentée la plus délicate pour arriver à ce résultat a été de récupérer l’instance courante de [galerie] à l’intérieur de la fonction passée à setQueryBuilder()

Si l’écriture et la mise au point de ce chargement particulier s’avère trop délicate, on peut s’en passer et garder le risque de produire une base de données qui n’ait pas toutes les propriétés voulues, si on la modifie avec le tableau de bord d’administration.

Dans tous les cas, on va ajouter, ci-après, des fonctions au projet pour gérer cela comme il faut, indépendamment de EasyAdmin. Le code à y écrire sera plus simple.

Après-tout, les administrateurs seront les seuls à jouer avec, et espérons qu’ils ne mélangeront pas les [objets] des différents membres s’ils doivent aller modifier le contenu des [galeries] via l’admin.

15. 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.

15.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]/

15.2. 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 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.

16. Ajout de méthodes de modification aux controleur Symfony du front-office existants

Vous aviez déjà ajouté des contrôleurs au front-office, qui offraient des fonctions en lecture seule, pour les premières entités du modèle, comme [inventaire].

Vous pouvez maintenant compléter ces classes Contrôleurs pour y ajouter la gestion des formulaires pour les opérations CRUD.

Les membres pourront ainsi ajouter, modifier ou supprimer leurs [inventaires], et les [objets] de ces [inventaires], depuis leur espace personnel dans le front-office.

16.1. Ajout méthode création de nouveau [inventaire]

Recopiez les méthodes et fichiers de création de [galerie] pour les ajouter dans le contrôleur d’inventaire :

  • [Galerie]Controller::new() dans [Inventaire]Controller (en renommant les noms de variables, classes et chemins et noms de routes)
  • templates/[galerie]/new.html.twig dans templates/[inventaire]/new.html.twig
  • templates/[galerie]/_form.html.twig dans templates/[inventaire]/_form.html.twig
  • src/Form/[Galerie]Type.php dans src/Form/[Inventaire]Type.php

17. Ajout de la consultation des [objets] depuis les [galeries] publiques

La consultation des [objets] gérée par le contrôleur [Objet]Controller (généré par make:crud) va être réservée à la consultation d’un objet par son propriétaire, authentifié.

On va ajouter une deuxième page de consultation différente, qu’on va placer dans le contrôleur des [galeries] (avec son template associé). Elle fera l’affichage « public » des [objets] à partir du moment ou leur propriétaire a décidé de les rendres visibles aux autres membres, en les plaçant 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.

Ainsi, dans le gabarit d’affichage de l’[objet], dans templates/[galerie]/[objet]_show.html.twig on affichera le lien correspondant à la [galerie] :

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

Il faudra donc transmettre 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 argument.

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

<h2>[Objets]</h2>
<ul>
   {% for [objet] in [galerie].[objets] %}
   <li>
     <a href="{{ path('app_[galerie]_[objet]_show', {'[galerie]_id': [galerie].id, '[objet]_id' : [objet].id}) }}">{{ [objet] }}</a>
   </li>
   {% endfor %}
</ul>

Ainsi, la méthode du contrôleur [Galerie]Controller::[objet]Show recevra donc deux entités Doctrine en argument. On va spécifier le mapping correspondant des ID et des classes via des attributs /MapEntity/ afin qu’il n’y ait pas d’ambigüité sur le mapping entre identifiants dans le chemin de l’URL, pour le mécanisme EntityValueResolver du contrôleur :

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]
       ]);
   }

On n’a plus qu’à dupliquer le code du gabarit d’affichage templates/[objet]/show.html.twig en templates/[galerie]/[objet]_show.html.twig, et adapter le rendu à l’intérieur du gabarit, pour sortir du contexte CRUD et faire un simple affichage de l’[objet]. Le tour est joué !

Attention : si on laisse ce code tel-quel, on crée 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 va corriger cela en vérifiant que l’[objet] et la [galerie] en question sont cohérents, et que cette dernière est publique :

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 (codes de réponse 404 et 403).

Plus tard 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.

18. 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.

18.1. 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 ou ses [inventaires], et de là aux [objets], etc.

  1. Ajoutez une classe Controleur pour l’entité Membre, avec make:controller
  2. Ajoutez deux méthodes index() et show() pour afficher la liste des membres et la fiche de chaque membre, et le contenu des gabarits correspondants, comme on l’a fait auparavant pour d’autres entités

    Dans la fiche d’un membre, on affichera la liste de ses [inventaires]. Par exemple, on obtiendrait ceci dans le gabarit templates/member/show.html.twig :

    {% for [inventory] in member.[inventories] %}
      <li><a href="{{ path('app_inventory_show', {'id' : inventory.id}) }}">{{ inventory }}</a></li>
    {% endfor %}
    
  3. Supprimez l’accès à la consultation des inventaires

    L’affichage de la liste des [inventaires] devra être restreinte, pour ne pas afficher tous les [inventaires] de la base de données, mais seulement le ou les [inventaires] du membre.

    On pourra ainsi désactiver l’affichage de la liste des inventaires (route app_[inventaire]_index). Plus tard, une fois qu’on aura géré l’authentification, on pourra la réactiver pour contextualiser le chargement de la liste des [inventaires] en le restreignant aux données du membre particulier lié à l’utilisateur connecté.

Pour accéder à la page de consultation d’un inventaire, on utilisera alors des liens placés dans la page de consultation du membre. Le routage d’accès à la liste des [inventaires] va donc changer pour prendre en argument l’identifiant du membre courant.

On ajustera également la création d’un nouvel inventaire, qui dépendra alors du membre courant.

Dans templates/member/show.html.twig, on ajoutera donc un lien :

<a href="{{ path('app_inventory_new', {'id': member.id}) }}">add new inventory</a>

18.2. Contextualisation de la création de l’[inventaire] d’un membre

La route app_inventory_new changera donc pour prendre en compte ce nouveau contexte où on passe l’identifiant du membre en argument. Extrait de src/Controller/InventoryController.php :

#[Route('/[inventaire]/new/{id}', name: 'app_[inventory]_new', methods: ['GET', 'POST'])]
public function new(Request $request, [Inventory]Repository $inventoryRepository, Member $member): Response
{
        $inventory = new Inventory();
        $inventory->setOwner($member);

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

On peut alors ajouter une customisation du formulaire de création pour ne pas autoriser la modification de l’attribut membre d’un [inventaire]. Ainsi dans la classe gérant le formulaire src/Form/InventoryType.php :

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

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

18.3. Contextualisation de la création d’un [objet]

On peut donc maintenant appliquer le même genre de modifications la consultation et la création des [objets] à partir de la consultation d’un [inventaire] pour garder le contexte courant (Membre -> [inventaire] -> [objet]), plutôt que de créer les entités « en vrac ».

19. Gestion de l’affectation des [objets] dans les [galeries]

Comme on l’a vu précédemment, 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.

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

Tout d’abord, on doit modifier la création d’une nouvelle [galerie] comme on l’a fait pour les [inventaires] pour qu’elle soit contextualisée à partir du membre (cf. étape précédente).

19.2. 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]).

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

Enfin, modifions le formulaire de modification de [galerie]. 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 :

class [Galerie]Type extends AbstractType
{
        public function buildForm(FormBuilderInterface $builder, array $options): void
        {
                //dump($options);
                $[galerie] = $options['data'] ?? null;
                $member = $[galerie]->getCreator();

                $builder
                        ->add('description')
                        ->add('published')
                        ->add('creator', null, [
                                'disabled'   => true,
                        ])
                        ->add('[objets]', null, [
                                'query_builder' => function ([Objet]Repository $er) use ($member) {
                                                return $er->createQueryBuilder('o')
                                                ->leftJoin('o.[inventaire]', 'i')
                                                ->andWhere('i.owner = :member')
                                                ->setParameter('member', $member)
                                                ;
                                        }
                                ])
                ;
        }

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 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é.

20. 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 fois la classe User ajoutée, on peut créer un lien OneToOne entre User et Membre, permettant de charger le membre authentifié, et d’avoir accès ainsi à ses données. Le fait d’avoir dissocié les deux classes n’est pas obligatoire. Outre les contraintes liées à la chronologie de l’avancement des TP et des cours, on peut estimer que l’on peut ainsi gérer (au niveau des administrateurs de l’application) des utilisateurs qui ne correspondent à aucun utilisateur se connectant réellement à l’application. Si un compte est supprimé pour interdire à un membre de se connecter à nouveau, on garderait éventuellement ainsi ses données.

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, ou le back-office pour créer des utilisateurs et leurs membres dans la base de données.

20.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 9 pour ajouter la génération des utilisateurs dans les Data Fixtures.

Pour intégrer les données de tests des utilisateurs avec le reste de la base de données, on est alors potentiellement confronté à l’ordre de chargement. 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 charger d’abord les instances de User, puis s’y relier à la création des Membres associés, en chargeant les User par leur email, avec findOneByEmail() :

foreach (self::MembersDataGenerator() as [$name, $useremail] ) {
        $member = new Member();
        if ($useremail) {
                $user = $manager->getRepository(User::class)->findOneByEmail($useremail);
                $member->setUser($user);
        }
        $member->setName($name);
        $manager->persist($member);
}
$manager->flush();

21. 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.

21.1. Accès en lecture aux seuls membres du site

On pourra ainsi restreindre l’accès aux contrôleurs [Objet]Controller, [Inventaire]Controller, MemberController,… uniquement aux utilisateurs authentifiés sur le site. Seul le controlleur des [Galeries] n’est pas concerné, car il devra, lui, être accessible à tous les visiteurs du site, même non-authentifiés.

Exemple d’annotation dans une classe contrôleur, nécessitant un login pour tout accès aux routes dont le chemin est préfixé par /[objet] :

#[Route('/[objet]')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class [Objet]Controller extends AbstractController
{

Comme la consultation des [objets] fait partie des pages en libre accès aux visiteurs, quand on y arrive via les [galeries], on va distinguer la consultation des [objets] pour leur propriétaire (dans [Objet]Controller, sous contrôle d’accès), de celle qu’on ajoutera dans [Galerie]Controller (par exemple) qui sera en lecture seule, et en accès ouvert (voir plus loin).

21.2. 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()->getMember() == $[inventaire]->getOwner());
if(! $hasAccess) {
    throw $this->createAccessDeniedException("You cannot access another member's [inventory]!");
}

21.3. 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).

21.4. 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), mais comme l’accès n’est pas contrôlé globalement à toutes les routes de ce contrôleur, contrairement aux autres, 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 {
                $user = $this->getUser();
                if( $user ) {
                        $member = $user->getMember();
                        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 très verbeux, mais compliqué à écrire car on n’est pas sûr qu’un utilisateur soit connecté, ni qu’il ait un membre associé, et on doit donc progresser en testant l’existance des instances avant d’appeler leur méthodes.

En PHP 8, on pourrait raccourcir en utilisant l’opérateur nullsafe qui permet d’éviter ce genre de tests et d’écrire quelque chose comme $this->getUser()?->getMember()?->....

21.5. 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 {
                $user = $this->getUser();
                  if( $user ) {
                          $member = $user->getMember();
                          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 est très complexe, et pourrait probablement receler beaucoup de 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.

22. 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.

22.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é (ayant un membre associé) :

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

22.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()->getMember();
                $[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]().

23. 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 reste à 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

23.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).

23.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.

24. 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) :

  • ajout de la gestion de la mise en ligne d’images pour des photos dans les [objet]
  • 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
  • ajouter une entité Commentaire au modèle des données pour gérer des messages envoyés aux propriétaires des [objets] (attention à la complexité de gestion d’un forum, la modération, etc.)

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

Auteur: Olivier Berger (TSP)

Created: 2023-11-13 Mon 11:22

Validate