Blog ENI : Toute la veille numérique !
🎁 Jusqu'au 25/12 : 1 commande de contenus en ligne
= 1 chance de gagner un cadeau*. Cliquez ici
🎁 Jusqu'au 31/12, recevez notre
offre d'abonnement à la Bibliothèque Numérique. Cliquez ici

La couche modèle avec Doctrine

Introduction

Après avoir exploré les contrôleurs et les vues, intéressons-nous maintenant à une partie très importante de notre application : le modèle de données.

La plupart du temps (même si ce n’est pas obligatoire), les données sont stockées dans des bases de données. Voyons comment Symfony fonctionne avec une base de données.

Les bases de données

Qu’est-ce qu’une base de données ?

Ce sont tout simplement des fichiers dans lesquels sont structurées les données de manière à pouvoir en extraire une information rapidement et de manière sécurisée.

Les données sont structurées sous forme d’un ensemble de tables de données. Les tables de données sont structurées comme des feuilles Excel. Elles comportent un certain nombre de colonnes (qu’on appelle champs) et un certain nombre de lignes (qu’on appelle enregistrements).

Voici un exemple de table de données :

images/16RI1.png

Les tables de données ont souvent un index (ici, c’est id_voiture) qui permet d’améliorer la performance de recherche dans la table.

Il existe différentes structures de bases de données.

Les plus utilisées sont :

  • Mysql (ou aujourd’hui son équivalent, Maria DB)

  • Oracle (la structure la plus complète, mais payante)

  • PostgreSQL (qui utilise des types de données modernes)

  • SQL Server (la base de données de Microsoft, payante)

Le langage SQL

Toutes les actions sur une base de données se font à l’aide du langage SQL.

Il existe des variantes de ce langage suivant le système de base de données : MySQL, SQL Oracle, SQL Server... mais les instructions sont très similaires.

Nous allons utiliser le serveur de base de données fourni par WAMP (ou équivalent : XAMPP sur Linux et MAMP sur macOS). Le langage utilisé pour ces serveurs est MySQL. Vous entendrez parler de MariaDB qui est l’équivalent de mySQL.

Nous n’apprendrons pas ce langage ici, ce n’est pas l’objet de l’ouvrage. Si vous souhaitez avoir plus d’informations, vous pouvez aller sur le site de MySQL :

https://dev.mysql.com/doc/refman/8.0/en 

Il faut juste savoir que le langage MySQL fonctionne sous forme de requêtes de données. Une requête est une instruction utilisant le langage MySQL.

Prenons un exemple.

Pour récupérer les données de la table Voiture affichée ci-dessus, il faut exécuter la requête :

SELECT marque,modele,couleur FROM voiture; 

Nous pouvons aussi insérer un nouvel enregistrement dans la table :

INSERT INTO voiture (marque,modele,couleur) VALUES ('Porsche',  
'Carrera','noire'); 

ou modifier un enregistrement :

UPDATE voiture SET couleur='bleue' WHERE marque='Porsche'; 

Et enfin, nous...

L’ORM de Symfony : Doctrine

Doctrine est l’intermédiaire entre notre application et les bases de données. Doctrine supporte tous les langages : MySQL, PostGreSQL… C’est une couche intermédiaire qui nous permet de nous affranchir d’utiliser les langages de gestion de bases de données. Doctrine permet d’assurer aussi une sécurité contre les failles qui atteignent le plus souvent l’accès aux bases. Il est donc conseillé de l’utiliser.

Doctrine est un outil indépendant de Symfony. Vous pouvez installer directement Doctrine pour un script PHP standard.

La documentation officielle de Doctrine est sur : https://www.doctrine-project.org/projects/doctrine-orm/en/current/tutorials/getting-started.html

Sous Symfony, Doctrine est installé par défaut. Il n’est pas besoin de charger des packages supplémentaires.

La configuration de Doctrine se fait grâce à la variable d’environnement DATABASE_URL dans le fichier .env :

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/
db_name?serverVersion=5.7 

Vous pouvez configurer .env pour accéder à d’autres bases de données comme MariaDB sqlite, PostgreSQL ou Oracle suivant vos besoins. Veuillez vous référer à la page : https://symfony.com/doc/current/doctrine.html

Nous allons configurer Doctrine pour...

Les entités

Comme nous l’avons dit, nous allons passer par des classes PHP pour créer les tables dans la base de données. Il ne sera pas nécessaire de créer des requêtes MySQL, ces requêtes seront générées automatiquement par Symfony.

Ces classes un peu particulières s’appellent des entités.

Nous pouvons créer les entités à la main, mais le mieux est d’utiliser une ligne de commande sur le terminal :

php bin/console make:entity 

Un système de questions/réponses vous permet de construire votre entité.

Nous allons créer, par exemple, une entité Produit qui va contenir un nom, une quantité, un prix et une variable booléenne rupture (qui indiquera s’il y a rupture de stock ou pas).

Voici les réponses à apporter aux questions/réponses sur le terminal :

php bin/console make:entity 
Class name of the entity to create or update (e.g. BravePuppy): 
> Produit 
Add the ability to broadcast entity updates using Symfony UX  
Turbo? (yes/no) [no]:no 
New property name (press <return> to stop adding fields): 
> nom 
Field type (enter ? to see all types) [string]: 
> string 
Field length [255]: 
> 200 
Can this field be null in the database (nullable) (yes/no) [no]: 
> no 
Add another property? Enter the property name (or press <return> 
to stop adding fields): 
> prix 
Field type (enter ? to see all types) [string]: 
> float 
Can this field be null in the database (nullable) (yes/no) [no]: 
> no 
Add another property? Enter the property name (or press <return> 
to stop adding fields): 
> quantite 
Field type (enter ? to see all types) [string]: 
> integer 
Can this field be null in the database (nullable) (yes/no) [no]: 
> no 
updated: src/Entity/Produit.php 
Add another property? Enter the property name (or press <return> 
to stop adding fields): 
> rupture 
Field type (enter ? to see all types) [string]: 
> boolean 
Can this field...

Les migrations

Pour pouvoir créer la table en base de données, il faut passer par une migration. Une migration est une classe qui décrit comment faire l’opération.

Pour créer toutes les migrations de toutes les entités créées, c’est très facile. Il suffit d’exécuter, sur le terminal ([Ctrl]+ ù), la commande :

php bin/console make:migration 

Toutes les entités créées sont scrutées pour générer les migrations correspondantes.

Si vous ouvrez le dossier Migrations, vous trouvez un fichier dont le nom ressemble à : Versionnumerodeversion.php.

numerodeversion étant un entier unique généré à partir de la date, de l’heure, de la minute et de la seconde de l’instant présent.

Ce fichier est unique pour chaque entité.

Il contient une classe possédant deux méthodes : up() et down(). La méthode up() contient la requête SQL qui va générer ou modifier la table correspondant à l’entité.

En l’occurrence, ici, il s’agit de créer la table produit. Nous retrouvons dans cette méthode la requête SQL qui permet de créer la table :

  public function up(Schema $schema): void 
    { 
        $this->addSql('CREATE TABLE produit (id INT AUTO_INCREMENT 
NOT NULL, nom VARCHAR(200) NOT NULL, prix DOUBLE PRECISION NOT 
NULL, quantite INT NOT NULL...

Les fixtures

Les fixtures vont nous permettre de remplir notre table produit grâce à une commande sur le terminal. Cela dit, tout ce que nous allons coder dans la fixture peut très bien être placé dans l’action d’un contrôleur.

Le seul avantage des fixtures est qu’il existe une commande sur le terminal qui permet d’exécuter toutes les fixtures existantes d’un seul coup.

Pour utiliser les fixtures, il faut installer un package supplémentaire. Si vous vous rendez sur Symfony Recipes Server : https://github.com/symfony/recipes/blob/flex/main/RECIPES.md et que vous recherchez fixture, vous trouverez ce package : doctrine/doctrine-fixtures-bundle.

Nous voyons que nous pouvons utiliser l’alias orm-fixtures (ou ormfixtures). 

Nous allons donc exécuter sur le terminal :

php composer.phar require --dev orm-fixtures 

La commande a créé un dossier src/DataFixtures et, à l’intérieur, un fichier exemple AppFixtures.php.

Toutes les fixtures héritent de la classe Fixture du FixtureBundle, qui est une classe abstraite (voir chapitre Le langage Objet, section Les classes abstraites et les interfaces - Les classes abstraites). La méthode load() de la classe Fixture est aussi abstraite, il faudra donc la redéfinir dans toutes les fixtures. 

C’est cette méthode qui sera exécutée automatiquement lorsque nous lancerons les fixtures.

Nous allons créer une Fixture propre à notre entité Produit.

Créez, dans src/DataFixtures, un fichier ProduitFixtures.php et collez-y le contenu de AppFixtures.php.

Puis renommez la classe ProduitFixtures :

<?php 
 
namespace App\DataFixtures; 
 
use Doctrine\Bundle\FixturesBundle\Fixture; 
use Doctrine\Common\Persistence\ObjectManager; 
 
class ProduitFixtures extends Fixture 
{ 
    public function load(ObjectManager $manager) 
    { 
        // $product = new Product(); 
        // $manager->persist($product); 
 
        $manager->flush(); 
    } 
} 

Les données des produits peuvent venir d’un fichier Excel ou autre. En guise d’exemple, nous allons créer un fichier...

La récupération des données à partir de la base

La récupération des données est aussi facile à faire grâce à Doctrine.

Pour ce faire, nous allons créer un nouveau contrôleur ListeProduitsController. Rappelez-vous comment nous créons un contrôleur :

php bin/console make:Controller ListeProduits 

Supprimons l’action par défaut index() et créons une action liste() avec la route /liste :

<?php 
 
namespace App\Controller; 
 
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 
use Symfony\Component\HttpFoundation\Response; 
use Symfony\Component\Routing\Annotation\Route; 
 
class ListeProduitsController extends AbstractController 
{ 
    #[Route('/liste', name: 'liste')] 
    public function liste()  
    { 
        return $this->render('liste_produits/index.html.twig', [ 
            'controller_name' => 'ListeProduitsController', 
        ]); 
    } 
 
} 

Pour récupérer les données de la table produit, nous allons utiliser le fichier Repository : src/Repository/ProduitRepository.php.

Ce fichier peut être récupéré à partir de l’entité Produit. Pour cela, il faut identifier la classe Produit :

use App\Entity\Produit; 

puis, récupérer l’EntityManager :

use Doctrine\ORM\EntityManagerInterface; 

l’injecter dans les paramètres de la fonction liste() :

public function liste(EntityManagerInterface $entityManager) 
{ 
... 
} 

et enfin, dans la fonction liste(), récupérer le Repository :

$produitsrepository=$entityManager->getRepository(Produit::class); 

L’objet $produitsrepository hérite d’une classe mère ServiceEntityRepository, qui possède les méthodes pour récupérer les données.

Par exemple, la méthode findAll() va chercher tous les enregistrements de la table :

  $listeproduits=$produitsrepository->findAll(); 

$listeproduits sera un objet de type ArrayCollection (une collection d’objets) de l’ensemble des données issues de la table.

Il ne reste plus qu’à transmettre $listeproduits à la vue :

 return $this->render('liste_produits/index.html.twig', [ 
            'listeproduits' => $listeproduits, 
        ]); 

Au final, la classe ListeProduitsController ressemble à cela :

<?php 
 
namespace App\Controller; 
 
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 
use Symfony\Component\HttpFoundation\Response; 
use Symfony\Component\Routing\Annotation\Route; 
 
use Doctrine\ORM\EntityManagerInterface; 
use App\Entity\Produit; 
 
class ListeProduitsController extends AbstractController 
{ 
    #[Route('/liste'...

Les méthodes du Repository

Nous avons vu que pour récupérer des données de la base, il fallait utiliser les méthodes héritées du Repository.

La méthode findAll() permet de récupérer toutes les entités d’une table. Cette méthode retourne un objet ArrayCollection.

Un objet ArrayCollection dépend d’une classe qui contient une propriété de type Array incluant la liste des entités. L’avantage de cette classe (au lieu d’un simple Array de PHP) est qu’elle met à notre disposition un certain nombre de méthodes permettant de parcourir le tableau des données.

Vous pouvez visualiser les différentes méthodes de cette classe.

Pour retrouver une classe sur VSCode faites [Ctrl] et P puis tapez le nom de la classe, par exemple, ici, Collection. Vous devriez tomber automatiquement sur le contenu de la classe.

Si vous ne la trouvez pas, voici son emplacement : vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php

Il existe d’autres méthodes de récupération des entités. Pour avoir la liste, vous pouvez vous fier à cette page de Doctrine : https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-objects.html#querying

Vous y trouvez notamment les méthodes :

  • find(id) : retourne l’entité...

Le langage DQL

Doctrine Query Language est un langage qui s’apparente au SQL, à la différence près qu’il s’applique sur les entités (les objets PHP) et non sur la base de données. 

Vous trouverez toute la syntaxe du DQL sur la page : https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html

L’utilisation du DQL va se faire dans une méthode du fichier Repository à partir de la méthode prepare() de l’EntityManager.

Syntaxe :

$query = $em->createQuery('La requête DQL '); 
$resultats = $query->getResult();  

La méthode getResult() récupère l’ensemble des résultats de la requête.

Vous avez d’autres méthodes de récupération possibles.

  • getSingleResult() : retourne un seul objet. Si la requête retourne plusieurs objets, une erreur apparaîtra. Si la requête ne retourne aucun objet, une erreur apparaîtra également.

  • getOneOrNullResult() : retourne un seul objet. Si la requête retourne plusieurs objets, une erreur apparaîtra. Si la requête ne retourne aucun objet, une valeur null sera retournée.

  • GetArrayResult() : retourne les résultats sous forme de tableaux imbriqués au lieu de renvoyer un ArrayCollection.

  • GetScalarResult() : retourne...

Le Query Builder

C’est la deuxième méthode pour personnaliser la récupération de données. Le QueryBuilder permet d’exécuter uniquement des méthodes de classe pour générer la requête. Nous restons dans la logique objet de PHP, nous n’avons plus besoin d’écrire une requête en dur.

Le Query Builder est une méthode du Repository, nous pouvons y accéder directement. 

Syntaxe :

$this->createQueryBuilder('alias pour l'entité')-> 
méthodes du Query Builder 

Vous trouverez toute la syntaxe du Query Builder sur la page : https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/query-builder.html#the-querybuilder

Les méthodes de récupération des résultats sont les mêmes que pour le DQL.

Prenons un exemple.

Nous souhaitons afficher une promotion pour le dernier produit insérée dans la base (pour le faire connaître).

Nous allons créer une méthode getLastProduit() dans le fichier Repository/ProduitRepository.php. Cette fois, nous utilisons le Query Builder pour lancer la requête.

Nous nous servirons de la méthode setMaxResults(1), qui renvoie le premier élément retourné par la requête, et nous utiliserons getOneOrNullResult() pour récupérer le résultat.

La méthode getLastProduit() s’écrit donc :

 public function getLastProduit() 
    { 
 
        $lastProduit=$this->createQueryBuilder('p') 
        ->orderBy('p.id', 'DESC') 
        ->setMaxResults(1) 
        ->getQuery() 
        ->getOneOrNullResult(); 
 
        return $lastProduit; 
 
    } 

Utilisons ensuite cette méthode dans ListeProduitsController...

L’exécution des requêtes SQL

Il est possible de faire beaucoup de choses avec le DQL ou le Query Builder. Ces méthodes ne s’arrêtent pas à la consultation de données, nous pouvons aussi nous en servir pour insérer, modifier ou supprimer des enregistrements.

Cependant, pour ceux qui ont de grosses requêtes à effectuer, qui seraient difficiles à implémenter avec ces méthodes, Doctrine donne la possibilité d’exécuter des requêtes SQL directement.

Il suffit de récupérer la connexion à la base de données à partir de l’Entity Manager, de préparer la requête et de l’exécuter. La récupération des données se fait avec la méthode fetchAllAssociative().

Par exemple, nous pouvons remplacer le DQL de la méthode orderingProduit() par une requête SQL équivalente :

    public function orderingProduit() 
    { 
        $conn = $this->getEntityManager()->getConnection(); 
 
        $sql="SELECT * FROM produit ORDER BY id DESC"; 
        $stmt = $conn->prepare($sql); 
        $resultSet =$stmt->executeQuery(); ...

L’écriture d’une requête SQL et l’obtention des objets mappés

Nous pouvons associer à une requête SQL un objet de type ResultSetMappingpour mapper le tableau de valeurs dans un objet.

Les valeurs de la requête sont traduites en propriétés de l’objet.

Il suffit d’instancier un objet $rsm :

$rsm = new ResultSetMappingBuilder($this->getEntityManager()); 

puis de lui associer une classe :

    $rsm->addEntityResult(MyClass::class, "m"); 

Il faut mapper le nom de chaque colonne en base de données sur les propriétés de nos entités :

foreach ($this->getClassMetadata()->fieldMappings as $monObjet) { 
        $rsm->addFieldResult("m", $monObjet["columnName"], 
$monObjet["fieldName"]); 
    } 

Enfin, nous exécutons notre requête SQL en transmettant l’objet $rsm :

$stmt = $this->getEntityManager()->createNativeQuery($sql, $rsm); 

Les relations entre entités

Il est possible, avec DQL ou Query Builder, de faire des jointures entre différentes tables.

En effet, il arrive souvent que toutes les informations ne soient pas dans une seule table, mais réparties dans plusieurs tables (il se peut par exemple qu’il y ait une jointure entre des produits et leurs marques).

Les tables utilisent des champs (on appelle cela des clés étrangères) ou des tables intermédiaires pour réaliser leurs jointures.

Nous allons voir ici comment nous pouvons faire des jointures directement dans les entités, sans passer par le DQL ou le Query Builder.

Il y a quatre sortes de jointures.

OneToOne

Un enregistrement de la table principale (qu’on appelle table propriétaire) ne peut être lié qu’à un seul enregistrement de la table secondaire (qu’on appelle table inverse) et, réciproquement, un enregistrement de la table inverse ne peut être lié qu’à un enregistrement de la table propriétaire.

Exemple

Imaginons une table Référence qui contient le numéro de référence de chaque produit. La relation entre les deux tables est de type OneToOne. C’est une relation d’unicité.

OneToMany

Un enregistrement de la table propriétaire peut être lié à plusieurs enregistrements de la table inverse, mais un enregistrement...

Les relations OneToOne

Prenons l’exemple d’une table Reference liée à notre table produit.

Nous allons créer la table Reference qui contiendra un seul champ : le numéro.

php bin/console make:entity 

Il faut répondre aux questions comme ci-dessous :

Class name of the entity to create or update (e.g. FierceElephant): 
> Reference 
Add the ability to broadcast entity updates using Symfony UX Turbo? 
(yes/no) [no]:  
created: src/Entity/Reference.php 
created: src/Repository/ReferenceRepository.php 
 
Entity generated! Now let's add some fields! 
You can always add more fields later manually or by re-running this 
command. 
New property name (press <return> to stop adding fields): 
> numero 
Field type (enter ? to see all types) [string]: 
> integer 
Can this field be null in the database (nullable) (yes/no) [no]: 
> no 
Add another property? Enter the property name (or press <return> to stop 
adding fields): 
> 
  Success! 

Créons la migration :

php bin/console make:migration 

et mettons à jour la base :

php bin/console doctrine:migrations:migrate 

La table Reference est maintenant créée. Nous allons créer une jointure OneToOne entre l’entité Produit et l’entité Reference.

Tout se passe dans l’entité propriétaire, qui est ici l’entité Produit.

La relation OneToOne va se faire en ajoutant une nouvelle propriété qui fera la jointure. Appelons cette propriété $reference.

Les annotations vont indiquer à Doctrine que cette propriété est une propriété de jointure OneToOne.

Dans l’entité Produit, il suffit d’ajouter :

#[ORM\OneToOne(targetEntity:Reference::class,cascade:["persist"])] 
private $reference = null; 

targetEntity indique le nom de l’entité inverse Reference.

cascade :["persist"] indique qu’il ne sera pas nécessaire de persister les objets de l’entité Reference qui seront joints à l’entité principale (nous y reviendrons).

La propriété que nous avons écrite à la main n’a pas d’accesseurs (getReference() et setReference($reference)) puisque nous l’avons créée manuellement.

Nous pouvons les générer automatiquement avec la commande suivante sur le terminal :

 php bin/console make:entity --regenerate App 

Cette commande met à jour toutes les entités présentes dans le dossier src.

Il faut maintenant générer les migrations pour mettre à jour la base de données :

php bin/console make:migration 
php bin/console doctrine:migrations:migrate 

Si vous allez sur localhost/phpmyadmin, vous verrez que votre table produit a une nouvelle colonne : reference_id, qui est un index de la table.

La jointure est mise en place. Il ne reste plus qu’à créer des jointures. Nous allons les créer dans une nouvelle Fixture.

Créez le fichier JoinReferenceFixtures.php dans le dossier src/DataFixtures et copiez-collez le contenu de ProduitFixtures.php.

Nous allons récupérer tous les enregistrements de l’entité...

Les relations ManyToMany

Nous allons gérer le cas où nous pouvons avoir autant de relations que nous souhaitons entre la table propriétaire et la table inverse et réciproquement.

Cette fois, une colonne de jointure, comme précédemment, ne suffit pas. Pour ce type de relation, il faut créer une table de jointure intermédiaire entre les deux tables.

Rassurez-vous, Doctrine s’occupe de tout.

Nous allons créer une nouvelle entité Distributeur pour faire notre jointure.

Distributeur contiendra juste une propriété $nom.

php bin/console make:entity 

Il faut répondre aux questions :

Class name of the entity to create or update (e.g. DeliciousPizza): 
> Distributeur 
Add the ability to broadcast entity updates using Symfony UX Turbo? 
(yes/no) [no]: 
> 

New property name : nom 
type : string 
length : 255 

Il faut créer la migration :

php bin/console make:migration 

puis migrer :

php bin/console doctrine:migrations:migrate 

La relation ManyToMany entre l’entité Produit et l’entité Distributeur se fait comme pour la relation OneToOne, grâce aux annotations.

Dans l’entité Produit, ajoutez cette propriété :

#[ORM\ManyToMany(targetEntity:Distributeur::class, cascade:["persist"])] 
private $distributeurs = null; 

Nous avons volontairement ajouté un « s » à la propriété $distributeurs. En effet, celle-ci est censée recevoir plusieurs entités de la classe Distributeur, puisque c’est une relation multiple. Cette propriété sera donc de type ArrayCollection.

Il faut générer les accesseurs :

 php bin/console make:entity --regenerate App 

Si vous jetez un œil aux accesseurs créés dans l’entité Produit, vous remarquerez que ce ne sont pas les mêmes que pour la relation OnetoOne.

Vous trouverez une méthode addDistributeur, qui permettra d’ajouter un distributeur, et une méthode removeDistributeur pour en supprimer.

Il faut maintenant générer les migrations pour mettre à jour la base de données :

php bin/console make:migration 
php bin/console doctrine:migrations:migrate 

En se rendant sur localhost/phpmyadmin, nous constatons que deux tables ont été créées : une table distributeur et une table produit_distributeur, table intermédiaire qui va gérer les jointures multiples.

Nous allons créer la fixture qui va remplir la table distributeur et faire des jointures multiples avec la table produit.

Créez le fichier JoinDistributeurFixtures.php et copiez-collez le contenu de JoinReferenceFixtures.php.

Modifiez la fixture : nous allons créer les entités dans distributeur et ensuite faire les jointures.

Voici à quoi ressemble le fichier JoinDistributeurFixtures.php :

<?php 
 
namespace App\DataFixtures; 
 
use App\Entity\Distributeur; 
use App\Entity\Produit; 
use Doctrine\Bundle\FixturesBundle\Fixture; 
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface; 
use Doctrine\Persistence\ObjectManager; 
 
class JoinDistributeurFixtures extends Fixture implements 
FixtureGroupInterface 
{ ...

Les relations bidirectionnelles

Nous avons toujours défini dans nos jointures une table prioritaire et une table inverse, ce qui signifie qu’il n’y a pas de réciprocité.

Dans la jointure, nous partons toujours d’une table (la table prioritaire) pour aller chercher des informations dans la table inverse.

Que se passe-t-il si nous souhaitons faire l’inverse ?

Exemple : avec la jointure ManyToMany précédente, on souhaite créer une vue qui liste les marques des distributeurs et qui donne, pour chacun d’eux, les noms des produits qu’ils distribuent.

Impossible à faire en l’état actuel, car nous n’avons pas de propriété $produit dans l’entité Distributeur. Si on ajoute cette propriété et que nous lui associons une jointure ManyToMany, il y a peu de chance pour que la table de liaison intermédiaire soit la même (nous aurions une deuxième table de liaison : distributeur_produit, ce qui n’est pas très pratique à gérer).

Nous allons donc mettre en place une relation bidirectionnelle.

La première étape est d’ajouter la propriété $produit de jointure dans la table inverse, c’est-à-dire la table distributeur.

Nous lui associons une jointure ManyToMany, mais en prenant le soin de préciser qu’il s’agit d’une relation inverse avec la propriété mappedBy.

Attention, cette option prend pour valeur le nom de la propriété qui sert de jointure dans la table propriétaire. Pour notre exemple, le nom de cette propriété est, si on regarde l’entité Produit : $distributeurs.

Voici donc ce qu’il faut ajouter dans l’entité Distributeur :

#[ORM\ManyToMany(targetEntity:Produit::class, 
mappedBy:'distributeurs')] 
private $produits = null; 

Ce n’est pas tout : il faut avertir l’entité Produit qu’il y a une relation bidirectionnelle. 

Il suffit pour cela d’ajouter l’option inversedBy qui, comme pour mappedBy, va avoir pour valeur le nom de la propriété qui fait la jointure dans l’entité inverse autrement dit pour notre exemple :...

Les relations bidirectionnelles avec attributs

Imaginons que que nous souhaitions avoir le nombre de produits générés pour chaque produit par marque.

Cela signifie qu’à chaque jointure entre l’entité Produit et l’entité Distributeur, il faudrait renseigner un paramètre nbProduit.

Mais où stocker cette information ?

Dans l’entité Produit, ce n’est pas possible, car cette information dépend de chaque marque. De même pour l’entité Distributeur, l’information dépend du produit.

Il faudrait pouvoir l’insérer dans la table de liaison produit_distributeur, mais nous n’avons pas accès à cette table. C’est Symfony qui la gère pour nous.

La seule solution est de créer et de gérer soi-même la table intermédiaire pour pouvoir y ajouter la colonne nbProduit. Mais il ne faut plus passer par une jointure ManyToMany.

Il faudra gérer une relation OneToMany entre l’entité produit et l’entité intermédiaire que nous créerons et une relation ManyToOne entre l’entité intermédiaire et la table distributeur.

Le Lazy Loading

Il est très important d’avoir présent à l’esprit cette notion quand on développe avec les jointures.

Symfony utilise un procédé qui s’appelle le Lazy Loading pour réaliser ses jointures. Il essaie d’optimiser les requêtes. En synthèse, il ne charge que ce dont il a besoin, au moment où il en a besoin.

Ainsi, lorsqu’il exécute la commande :

$listeProduits=$produitsRepository->findAll(); 

la requête générée ne fait pas une réelle jointure. Elle utilise un objet Proxy pour simuler la jointure. Tant que des informations de la table inverse ne sont pas demandées, la requête ne fait pas de jointure.

Pour plus de précisions sur les objets Proxy, consultez le lien : https://fr.wikipedia.org/wiki/Proxy

Voyons un exemple.

Créons une action eager() qui va appeler une vue pour afficher les noms des produits :

#[Route("/eager",name: "eager")] 
  public function eager(EntityManagerInterface $entityManager) 
    { 
    $produitsRepository=$entityManager->getRepository(Produit::class); 
    $listeProduits=$produitsRepository->findAll(); 
        return $this->render('liste_produits/eager.html.twig', [ 
            'listeproduits' => $listeProduits, ...

Le Reverse Engineering

Il se peut que vous entamiez Symfony avec une base de données déjà existante. C’est souvent le cas en entreprise. Si votre base contient beaucoup de tables, il sera assez fastidieux de créer à la main toutes les entités correspondantes dans votre application.

Symfony vous donne la possibilité de créer toutes les entités automatiquement à partir de la base de données.

Sur le terminal, lancez la commande :

php bin/console doctrine:mapping:import "App\Entity" annotation 
--path=src/Entity/Reverse 

Dans le dossier src/Entity, vous trouverez un dossier Reverse qui contiendra toutes les entités générées.

Si vous ouvrez l’entité Produit, vous verrez que les jointures ont été détectées. Il y a bien un ManyToMany sur la propriété $distributeur.

Par contre, Symfony a fait une jointure ManyToOne (par défaut) sur la propriété $reference. En effet, il n’y a rien qui différencie un ManyToOne d’un OneToOne dans la structure des tables.

À vous de faire les corrections manuellement.

Il ne vous reste plus qu’à générer les accesseurs avec la commande :

php bin/console make:entity --regenerate App 

Attention : supprimez le dossier Reverse et son contenu pour la suite. Si vous ne le faites pas, vous aurez...