Apprendre Symfony 6 : Base de Données, Entités et Relations

Doctrine est une librairie PHP conçue pour manipuler ses bases de données et mapper des objets. Elle représente le coeur des modèles dans Symfony.

Icône de calendrier
Intermédiaire
13 chapitres

Doctrine : c’est quoi ?

Doctrine est une librairie PHP conçue pour faciliter la manipulation de ses bases de données et le mapping de ses objets. Il est qualifié d’ORM et de DBAL.

On utilise ce type de librairie dans la plupart des frameworks PHP. Doctrine est utilisée par défaut par le framework Symfony.

Modèles, sans Doctrine

Quand on récupère des informations stockées en base de données en PHP natif, on fait généralement appel à PDO (PHP Data Object), une extension définissant l’interface orientée objet pour accéder à une base de données avec PHP.

La classe PDO constitue une couche d’abstraction qui intervient entre l’application PHP et un Système de Gestion de Base de Données (SGDB) tel que MySQL, PostgreSQL ou MariaDB par exemple.

La couche d’abstraction permet de séparer le traitement de la base de données en elle-même.

Pour exemple, on récupérait des données comme ceci :

// On se connecte à la base de données MySQL
$db = new PDO('mysql:host=localhost;dbname=bdd_demo;charset=utf8', 'root', '');
// Ecriture de la requête SQL qui récupère tous les articles contenus dans la table "articles"
$query = 'SELECT * FROM articles';
// Exécution de la requête SQL
$stmt = $db->query($query);
// Récupération des résultats
$articles = $stmt->fetchAll();

Bien que très pratique, l’extension PDO présente 3 inconvénients :

  • Il faut avoir des compétences en langage SQL
  • Il faut écrire soi-même ses requêtes
  • PDO retourne par défaut des données structurées sous forme de tableaux… et Symfony est un framework orienté objet !

Modèles, avec Doctrine

Doctrine DBAL

La partie DBAL (Database Abstraction Layer) est la couche assurant la manipulation des bases de données. Il s’agit de la couche de plus bas niveau de Doctrine qui permet de communiquer avec les bases de données relationnelles.

Elle est comparable à l’extension PHP PDO, et pourrait être définie comme étant une version plus avancée de cette extension.

Elle offre par exemple des fonctions qui listent les tables, les champs, le détails des structures, etc.

On peut alors facilement appeler une fonction qui va récupérer les 3 derniers articles d’un blog sans écrire la moindre ligne de SQL.

Retenez que cette surcouche est une API (Application Programming Interface) qui vous permet de faciliter la manipulation de vos bases de données en bénéficiant d’un ensemble de classes, méthodes, fonctions, etc.

Doctrine ORM

La partie ORM (Object Relational Mapping) est la couche assurant le mapping des objets. Le mapping consiste à faire le lien entre nos objets et les données stockées en base.

Admettons que DBAL retourne les 3 derniers articles en provenance de la table « articles » et que ces derniers possèdent 3 champs: id, title et content.

Côté Symfony : il sera donc logique de manipuler un objet issu de la classe Article, avec des propriétés correspondantes. On appelle ce type de classe une entité.

Et bien l’ORM permettra très facilement de faire correspondre chaque propriété de votre objet au champ correspondant dans votre table physique.

Ainsi, ORM fait correspondre une table à une entité.

Schéma récapitulatif

Doctrine joue donc le rôle d’intermédiaire entre votre application Symfony et la base de données. Il possède :

  • Une couche DBAL qui facilite la communication avec la base de données.
  • Une couche ORM qui va mapper les données récupérées avec vos objets dans l’application : les entités.

Schéma illustrant le fonctionnement de Doctrine

Base de données

Configuration

Pour que Doctrine soit en mesure d’interagir avec notre base de données, il faut lui indiquer où cette dernière se trouve.

Cette information sur la connexion à la base de données doit être placée dans une variable d’environnement appelée DATABASE_URL située dans le fichier 📄 .env, à la racine de votre projet :

.env
copié !
DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8&charset=utf8mb4"
  • app (le 1er) : le nom d’utilisateur pour accéder à votre base de données. En local, avec WAMP ou MAMP, c’est par défaut « root ».
  • !ChangeMe! : le mot de passe pour accéder à votre base de données. En local, avec WAMP, il n’y en a pas par défaut (vous supprimez !ChangeMe! et ne mettez rien à la place). Avec MAMP, c’est par défaut « root ».
  • 127.0.0.1:3306 : l’adresse de votre base de données. En local, c’est par défaut « 127.0.0.1:3306 » ou « localhost:3306 ». 3306 est le port d’écoute de MySQL. Si vous utilisez le serveur MariaDB, il est possible que cela ne fonctionne pas, renseignez alors le port 3307. Si ça ne fonctionne toujours pas, spécifiez la version de votre serveur à la toute fin de cette ligne de code avec ?serverVersion=***. Vous pouvez facilement connaître votre numéro de version en tapant la requête SQL SHOW GLOBAL VARIABLES LIKE '%version%' dans votre interface phpMyAdmin.
  • app (le 2nd) : c’est le nom de votre base de données.

Création

Il est possible de créer sa base de données via un script ou à la main depuis phpMyAdmin… mais Symfony nous propose une ligne de commande tout prête, il serait dommage de s’en priver.

Pour créer la base de données correspondante aux informations spécifiées dans le fichier 📄 .env, taper :

copié !
symfony console doctrine:database:create

Suppression

Si vous souhaitez supprimer votre base de données pour une quelconque raison, taper la commande :

copié !
symfony console doctrine:database:drop

Entités

Qu’est-ce qu’une entité ?

Une entité est une classe PHP qui est mappée avec une table de notre base de données.

Elles sont au centre des modèles. C’est à travers elles que vous allez manipuler tous les éléments propres à votre application (utilisateurs, produits, articles, messages, etc.).

  • Si je dois créer un blog, je vais gérer mes articles via l’entité Article
  • Si je crée un site e-commerce, je vais gérer mes produits via l’entité Product
  • Si je crée un forum je vais gérer mes messages via l’entité Post

Créer une entité

Les entités d’un projet Symfony sont rassemblées dans le dossier 📁 src/Entity.

Si nous possédons un blog, nous créerons d’abord une entité 📄 Article.php, contenant par exemple les propriétés suivantes :

  • id : integer
  • title : string(255)
  • content : text
  • createdAt : datetime

Créer ses entités à la main est laborieux, et comme pour toute tâche laborieuse, il existe une solution : le MakerBundle !

copié !
symfony console make:entity

Cette ligne de commande va vous demander plusieurs informations :

  1. Un nom pour l’entité à générer (avec une majuscule au début). Deux fichiers seront créés : 📄 src/Entity/Nom.php et 📄 src/RepositoryNomRepository.php
  2. Ajout d’une propriété à votre entité. Vous pouvez alors taper à nouveau sur ENTER pour sortir de l’interface ou saisir le nom de votre première propriété (en lowercase, camelCase ou snake_case).
  3. Ajout d’un type pour votre propriété (par défaut string). Vous pouvez consulter la liste de tous les types disponibles en tapant ?. Selon votre choix, la suite est variable (s’il s’agit d’une chaîne de caractères on vous demandera la taille maximale : length…). Ensuite, on vous demandera systématiquement si la propriété en question est facultative ou non (nullable). length, nullable… ça ressemble à nos arguments de l’attribut Column() ça non ?! En fait, le MakerBundle va non seulement vous créer les propriétés de la classe, mais aussi les annotations / attributs PHP associés, à partir des renseignements que vous lui donnez.
Article.php
copié !
<?php

namespace App\Entity;
use App\Repository\ArticleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass:ArticleRepository::class)]
class Article {

	#[ORM\Id]
	#[ORM\GeneratedValue]
	#[ORM\Column]
	private ?int $id = null;
	
	#[ORM\Column(length:255)]
	private ?string $title = null;
	
	#[ORM\Column(type:Types::TEXT)]
	private ?string $content = null;
	
	#[ORM\Column]
	private ?\DateTimeImmutable $createdAt = null;
	
	// Les getters et setters ...

}
Namespace et import
  • namespace : on définit un namespace pour situer les entités de notre application.
  • use : on importe la classe ArticleRepository (automatiquement créée - détailée par la suite), la classe Doctrine Types dédiée au typage des propriétés, ainsi que la classe Doctrine Mapping, utilisée pour mapper nos classes avec la base de données.
Les attributs PHP

ORM\Entity : spécifie que la classe est une entité. Entre parenthèses, il est possible de préciser des arguments nommés aux attributs. L’attribut Entity en possède la plupart du temps un : repositoryClass, nous permettant d’associer ce fichier à un Repository - nous aborderons ce concept au chapitre suivant.

  • ORM\Id : identifie quel propriété PHP joue le rôle de clé primaire de la table.
  • ORM\GeneratedValue : permet de définir la valeur automatique « AUTO_INCREMENT » pour les identifiants.
  • ORM\Column : permet de mapper une propriété PHP à une colonne de la base de données. Si une propriété n’est pas marquée avec cet attribut, elle sera ignorée. Par défaut le nom de la colonne en base de données correspond au nom de la propriété, mais il est possible d’en spécifier un différent avec l’argument name. L’attribut Column a de nombreux autres paramètres, mais nous en utilisons la plupart du temps 4 :
  1. unique : précise que la valeur de cette colonne doit être unique (par exemple un e-mail ou un pseudo utilisateur).
  2. nullable : permet de définir une propriété comme étant facultative (#[ORM\Column(nullable: true)]). Toutes les propriétés sont par défaut obligatoires.
  3. length : permet de définir la longueur maximale d’une chaîne de caractère.
  4. type : permet de définir le type de la propriété si ce dernier ne peut pas être déduit du typage PHP traditionnel (?string $content par exemple). Doctrine fait automatiquement le lien entre le type PHP et le type en base de données (MySQL par exemple).
Getters et setters

Les « getters » et « setters » sont les méthodes qui vont nous permettre respectivement de définir et d’accéder à nos propriétés.

Modifier une entité

Votre entité doit évoluer ? Vous souhaitez lui ajouter une propriété ? Deux choix s’offrent alors à vous :

  1. Aller modifier le code à la main. C’est utile lorsqu’on souhaite faire des changements spécifiques dans son entité (suppression de propriétés, modification des attributs PHP…).
  2. Utiliser le MakerBundle. C’est utile pour l’ajout classique de nouvelles propriétés. Cela nous amène à taper une nouvelle fois la ligne de commande make:entity, en spécifiant en argument le nom de l’entité existante.
copié !
symfony console make:entity NomEntite

Supprimer une entité

Pour supprimer une entité, on supprime :

  1. Le fichier de l’entité en question : 📄 src/Entity/Nom.php
  2. Son repository associé : 📄 src/Repository/NomRepository.php

Relations entre entités

Généralités

Direction

Nous avons précédemment créé une entité isolée : elle n’a pas de lien avec d’autres entités de notre base de données. Mais en réalité, nous avons souvent besoin de mettre en place une structure plus complexe. On va alors créer des relations entre nos entités afin qu’elles puissent interagir entre elles.

Voici quelques exemples de relations entre entités dans le cas d’un site internet :

  • Sur Instagram / Facebook, une publication est postée par un utilisateur. Post.php et User.php
  • Sur Netflix, un film / une série est rattaché(e) à une catégorie. Movie.php / Serie.php et Category.php
  • Sur Blablacar, un trajet est réservé par un utilisateur. Road.php et User.php
  • Sur Amazon, un utilisateur peut enregistrer une ou plusieurs adresses de livraison. User.php et Adress.php
  • Sur laConsole, une leçon est rattachée à une formation. Lesson.php et Course.php

Ces relations vont représenter dans notre application ce qui est caractérisé dans nos tables par des clés étrangères et des tables d’associations.

Créer une relation

Pour créer une relation il faut créer une propriété relationnelle lors de la création / mise à jour d’une entité avec la commande :

copié !
symfony console make:entity

Au moment de choisir le type de la propriété, saisissez simplement relation.

Vous n’avez plus qu’à répondre aux quelques questions de l’invite de commande et puis toutes vos relations seront générées par magie ! 🧙‍♂️

Multiplicité

On distingue 4 grands types de relations possibles, variant selon le nombre de liens entre entités : on parle de multiplicité.

  • One-To-One (1-1) : deux entités A et B sont liées de manière unique.
  • Many-To-One (n-1) : plusieurs entités A peuvent être liées à une unique entité B.
  • One-To-Many (1-n) : une entité A peut être liée à plusieurs entités B.
  • Many-To-Many (n-n) : plusieurs entités A peuvent être liées à plusieurs entités B.

Outre le nombre de liens existant entre 2 entités, une notion de direction de la relation entre en jeu. On distingue deux types de directions :

Unidirectionnelle

Cela signifie que l’entité A a accès à une entité B et non l’inverse. A est l’entité propriétaire de la relation, c’est celle qui « possède » l’autre.

Bidirectionnelle

Cela signifie qu’une entité A a accès à une entité B et inversement. Dans le cas d’une relation bidirectionnelle, on définit en plus de l’entité propriétaire une entité inverse, c’est celle qui « est possédée » par l’autre.

  • Dans le cas des relations 1-1 : l’entité correspondant à la table qui contient la clé étrangère est propriétaire.
  • Dans le cas des relations 1-n et n-1 : l’entité du côté n est toujours propriétaire et l’entité du côté 1 est toujours inverse.
  • Dans le cas des relations n-n : on peut choisir le côté propriétaire et inverse comme on le souhaite.

En base de données, une relation One-To-One, Many-To-One et One-To-Many est assurée par la présence d’une clé étrangère. Au sein d’une entité, elle le sera par une propriété relationnelle.

En base de données, une relation Many-To-Many est assurée par la présence d’une table de relation. Au sein d’une entité, elle le sera par une propriété relationnelle.

Bien que la multiplicité ainsi que la direction d’une relation soient automatiquement définies dans le code des entités en fonction des réponses apportées à la commande make:entity, je vous détaille ci-dessous les modifications structurelles apportées à vos entités en fonction du type de la relation.

Relation 1-1

La relation One-To-One consiste à associer un objet avec un autre de manière unique.

Prenons l’exemple d’un article de blog auquel on souhaite rattacher une image de couverture. Un article va pouvoir être lié à une image de couverture.

Relation unidirectionnelle

Ici, on aura plutôt tendance à récupérer l’image à partir d’un article que l’inverse, notre relation est bien unidirectionnelle et l’entité propriétaire est Article.

Entité propriétaire

Article.php
copié !
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article {

	// Propriétés ...

	#[ORM\OneToOne(targetEntity: Image::class)]
	private ?Image $image = null;

	// Getters et setters ...

	public function getImage(): ?Image { ... }
	public function setImage(Image $image): self { ... }

}
  1. $image est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).
  2. ORM\OneToOne est un attribut, spécifiant la multiplicité 1-1 de la relation. L’argument targetEntity précise que l’entité liée est Image.
  3. Getter et setter de la propriété.

Avec notre modèle unidirectionnel, lorsque nous avons un article, nous pouvons trouver l’image qui lui est associée avec $article->getImage().

Relation bidirectionnelle

Admettons que nous souhaitons également pouvoir accéder à nos articles depuis nos images côté back-office (peut être utile pour les administrateurs). Chacune des 2 entités doit alors pouvoir faire référence à l’autre.

Le côté propriétaire d’une association One-To-One bidirectionnelle est l’entité avec la table contenant la clé étrangère. Ce côté peut être défini comme vous le souhaitez, peu importe.

Entité propriétaire

Article.php
copié !
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article {

	// Propriétés ...

	#[ORM\OneToOne(targetEntity: Image::class, inversedBy: 'article')]
	private ?Image $image = null;

	// Getters et setters ...

	public function getImage(): ?Image { ... }
	public function setImage(Image $image): self { ... }

}
  1. $image est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).
  2. ORM\OneToOne est un attribut, spécifiant la multiplicité 1-1 de la relation. L’argument targetEntity précise que l’entité liée est Image. L’argument inversedBy référence la propriété $article qui porte la relation côté entité inverse.
  3. Getter et setter de la propriété.

Entité inverse

Image.php
copié !
namespace App\Entity;

use App\Repository\ImageRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ImageRepository::class)]
class Image {

	// Propriétés ...

	#[ORM\OneToOne(targetEntity: Article::class, inversedBy: 'image')]
	private ?Article $article = null;

	// Getters et setters ...

	public function getArticle(): ?Article { ... }
	public function setArticle(Article $article): self { ... }

}
  1. $article est la propriété relationnelle faisant le lien avec l’entité propriétaire.
  2. ORM\OneToOne est un attribut, spécifiant la multiplicité 1-1 de la relation. L’argument targetEntity précise que l’entité liée est Article. L’argument mappedBy référence la propriété $image qui porte la relation côté entité propriétaire.
  3. Getter et setter de la propriété.

Relations 1-n et n-1

  • La relation One-To-Many consiste à associer un objet avec plusieurs autres.
  • La relation Many-To-One consiste à associer plusieurs objets avec un autre.

Prenons l’exemple d’un article de blog auquel on souhaite rattacher une catégorie. Plusieurs articles vont pouvoir être liés à une unique catégorie. Ici, nous avons intérêt à pouvoir manipuler nos catégories dans une table indépendante pour plusieurs raisons :

  • Renommer une catégorie : cela se fera une unique fois dans la table categories et non dans chaque colonne de la table articles.
  • Afficher les catégories sur une page : cela permettra de les récupérer simplement en requêtant la table categories.
  • Hiérarchie : mettre en place un système de hiérarchie entre vos catégories (par exemple : musique > rock, pop, folk…) n’est possible qu’avec une table dédiée.
Relation unidirectionnelle (n-1)

Côté Article, la relation est Many-To-One car « plusieurs articles peuvent être liés à une seule catégorie ».

Si la relation est unidirectionnelle, à partir des articles, je pourrai récupérer leur catégorie, mais pas l’inverse.

Entité propriétaire

Article.php
copié !
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article {

	// Propriétés ...

	#[ORM\ManyToOne(targetEntity: Category::class)]
	private ?Category $category = null;

	// Getters et setters ...

	public function getCategory(): ?Category { ... }
	public function setCategory(Category $category): self { ... }

}
  1. $category est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).
  2. ORM\ManyToOne est un attribut, spécifiant la multiplicité n-1 de la relation. L’argument targetEntity précise que l’entité liée est Category.
  3. Getter et setter de la propriété.

Avec notre modèle unidirectionnel, lorsque nous avons un article, nous pouvons trouver la catégorie qui lui est associée avec $article->getCategory().

Relation unidirectionnelle (1-n)

Côté Category, la relation est One-To-Many car « une catégorie peut être associée à plusieurs articles ».

Si la relation est unidirectionnelle, à partir d’une catégorie, je pourrai récupérer ses articles, mais pas l’inverse.

Entité propriétaire

Category.php
copié !
namespace App\Entity;

use App\Repository\CategoyRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category {

	// Propriétés ...

	#[ORM\OneToMany(targetEntity: Article::class)]
	private Collection $articles;

	public function __construct() {
		$this->articles = new ArrayCollection();
	}

	// Getters et setters ...

	public function getArticles(): Collection { ... }
	public function addArticle(Article $article): self {}
	public function removeArticle(Article $article): self {}

}
  1. $articles est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).
  2. ORM\OneToMany est un attribut, spécifiant la multiplicité 1-n de la relation. L’argument targetEntity précise que l’entité liée est Article.
  3. Le getter et les méthodes d’ajout addArticle() et de suppression removeArticle() remplacent le setter car $articles est voué à contenir un tableau d’objets, appelé Collection.
  4. Import des classes de Collection de Doctrine avec nos use.
  5. Ajout d’un constructeur pour initialiser notre propriété $articles comme une collection vide.

Avec notre modèle unidirectionnel, lorsque nous avons une catégorie, nous pouvons trouver les articles qui lui sont associés avec $category->getArticles().

Relation bidirectionnelle (n-1 et 1-n)

Généralement, sur un blog, nous avons besoin de pouvoir accéder depuis un article à sa catégorie mais également l’inverse pour lister sur une page les articles publiés dans une catégorie. Chacune des 2 entités doit alors pouvoir faire référence à l’autre.

Si une relation Many-To-One ou One-To-Many est bidirectionnelle alors on aura :

  • une relation Many-To-One d’un côté,
  • une relation One-To-Many de l’autre.

Le côté propriétaire d’une association Many-To-One ou One-To-Many bidirectionnelle est l’entité du côté « many », celui contenant la clé étrangère.

Entité propriétaire

Article.php
copié !
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article {

	// Propriétés ...

	#[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'articles')]
	private ?Category $category = null;

	// Getters et setters ...

	public function getCategory(): ?Category { ... }
	public function setCategory(Category $category): self { ... }

}
  1. $category est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).
  2. ORM\ManyToOne est un attribut, spécifiant la multiplicité n-1 de la relation. L’argument targetEntity précise que l’entité liée est Category. L’argument inversedBy référence la propriété $articles qui porte la relation côté entité inverse.
  3. Getter et setter de la propriété.

Entité inverse

Category.php
copié !
namespace App\Entity;

use App\Repository\CategoyRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category {

	// Propriétés ...

	#[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'category')]
	private Collection $articles;

	public function __construct() {
		$this->articles = new ArrayCollection();
	}

	// Getters et setters ...

	public function getArticles(): Collection { ... }
	public function addArticle(Article $article): self {}
	public function removeArticle(Article $article): self {}

}
  1. $articles est la propriété relationnelle faisant le lien avec l’entité propriétaire.
  2. ORM\OneToMany est un attribut, spécifiant la multiplicité 1-n de la relation. L’argument targetEntity précise que l’entité liée est Article. L’argument mappedBy référence la propriété $category qui porte la relation côté entité propriétaire.
  3. Le getter et les méthodes d’ajout addArticle() et de suppression removeArticle() remplacent le setter car $articles est voué à contenir un tableau d’objets, appelé Collection.
  4. Import des classes de Collection de Doctrine avec nos use.
  5. Ajout d’un constructeur pour initialiser notre propriété $articles comme une collection vide.

Relation n-n

La relation Many-To-Many consiste à associer plusieurs objets avec plusieurs autres.

Conservons notre exemple de catégories d’articles de blog, mais imaginons maintenant qu’un article peut avoir plus d’une catégorie. Plusieurs articles vont pouvoir être liés à plusieurs catégories.

Relation unidirectionnelle

Considérons que nous aurons plutôt tendance à récupérer les catégories à partir d’un article que l’inverse, notre relation est bien unidirectionnelle et l’entité propriétaire est Article.

Entité propriétaire

Article.php
copié !
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article {

	// Propriétés ...

	#[ORM\ManyToMany(targetEntity: Category::class)]
	private Collection $categories;

	public function __construct() {
		$this->categories = new ArrayCollection();
	}

	// Getters et setters ...

	public function getCategories(): Collection { ... }
	public function addCategory(Category $category): self {}
	public function removeCategory(Category $category): self {}

}
  1. $categories est la propriété relationnelle faisant le lien avec l’autre entité.
  2. ORM\ManyToMany est un attribut, spécifiant la multiplicité n-n de la relation. L’argument targetEntity précise que l’entité liée est Category.
  3. Le getter et les méthodes d’ajout addCategory() et de suppression removeCategory() remplacent le setter car $categories est voué à contenir un tableau d’objets, appelé Collection.
  4. Import des classes de Collection de Doctrine avec nos use.
  5. Ajout d’un constructeur pour initialiser notre propriété $categories comme une collection vide.

Avec notre modèle unidirectionnel, lorsque nous avons un article, nous pouvons trouver les catégories qui lui sont associées avec $article->getCategories().

Relation bidirectionnelle

Admettons que nous souhaitons également pouvoir accéder à nos articles depuis nos catégories. Chacune des 2 entités doit pouvoir faire référence à l’autre.

Le côté propriétaire d’une association Many-To-Many bidirectionnelle est celui que nous souhaitons.

Entité propriétaire

Article.php
copié !
namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORMEntity(repositoryClass: ArticleRepository::class)]
class Article {

	// Propriétés ...

	#[ORM\ManyToMany(targetEntity: Category::class, inversedBy: 'articles')]
	private Collection $categories;

	public function __construct() {
		$this->categories = new ArrayCollection();
	}

	// Getters et setters ...

	public function getCategories(): Collection { ... }
	public function addCategory(Category $category): self {}
	public function removeCategory(Category $category): self {}

}
  1. $categories est la propriété relationnelle faisant le lien avec l’autre entité.
  2. ORM\ManyToMany est un attribut, spécifiant la multiplicité n-n de la relation. L’argument targetEntity précise que l’entité liée est Category. L’argument inversedBy référence la propriété $articles qui porte la relation côté entité inverse.
  3. Le getter et les méthodes d’ajout addCategory() et de suppression removeCategory() remplacent le setter car $categories est voué à contenir un tableau d’objets, appelé Collection.
  4. Import des classes de Collection de Doctrine avec nos use.
  5. Ajout d’un constructeur pour initialiser notre propriété $categories comme une collection vide.

Entité inverse

Category.php
copié !
namespace App\Entity;

use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category {

	// Propriétés ...

	#[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'categories')]
	private Collection $articles;

	public function __construct() {
		$this->articles = new ArrayCollection();
	}

	// Getters et setters ...

	public function getArticles(): Collection { ... }
	public function addArticle(Article $article): self {}
	public function removeArticle(Article $article): self {}

}
  1. $articles est la propriété relationnelle faisant le lien avec l’entité propriétaire.
  2. ORM\ManyToMany est un attribut, spécifiant la multiplicité n-n de la relation. L’argument targetEntity précise que l’entité liée est Article. L’argument mappedBy référence la propriété $categories qui porte la relation côté entité propriétaire.
  3. Le getter et les méthodes d’ajout addArticle() et de suppression removeArticle() remplacent le setter car $articles est voué à contenir un tableau d’objets, appelé Collection.
  4. Import des classes de Collection de Doctrine avec nos use.
  5. Ajout d’un constructeur pour initialiser notre propriété $articles comme une collection vide.

Aller plus loin

Opérations en cascade

Nous serons souvent amenés à enregistrer ou encore à supprimer des données en base de données.

L’argument cascade spécifie que les actions devront s’effectuer en cascade. Autrement dit, les opérations de persistance (enregistrement en base de données) et de suppression sur une entité A seront répercutées sur l’entité B.

Article.php
copié !
#[ORM\OneToOne(targetEntity: Image::class, cascade: ['persist', 'remove'])]
private Image $image;

J’indique ici que si un article est enregistré (persist) ou supprimé (remove), il faudra automatiquement enregistrer ou supprimer l’image associée.

ValeurDescription
persistSi l’entité A est sauvegardée, faire de même avec l’entité B associée.
removeSi l’entité A est supprimée, faire de même avec l’entité B associée.

On place l’argument nommé cascade du côté de l’entité qui doit subir l’opération en cascade.

  • Sur Instagram, si je supprime un utilisateur, je supprime ses publications.
  • Sur Amazon, si je supprime un produit, je supprime les évaluations associées.
  • Sur StackOverflow, si je supprime une question, je supprime les réponses associées.

Relation facultative / obligatoire

Si par défaut, une propriété est défini comme obligatoire (ORM\JoinColumn(nullable: false) implicite), ce n’est pas le cas d’une relation qui sera par défaut facultative. C’est-à-dire qu’elle possède de manière implicite l’attribut PHP ORM\JoinColumn(nullable: true).

Néanmoins, il est possible de la rendre obligatoire en définissant cet attribut à false.

StatutDescription
nullable: trueUne entité A peut être liée à une entité B. Facultatif car il s’agit du comportement par défaut.
nullable: falseUne entité A est obligatoirement liée à une entité B.

Imaginons qu’un article peut recevoir plusieurs commentaires. Un article n’a pas obligatoirement reçu de commentaires. Dans l’entité Comment, je ne précise rien car par défaut ORM\JoinColumn(nullable: true) sera sous-entendu :

Comment.php
copié !
#[ORM\ManyToOne(targetEntity: Article::class)]
private ?Article $article = null;

En revanche, si la relation est obligatoire, par exemple sur une application bancaire, un compte bancaire sera obligatoirement possédé par un client, on précisera dans l’entité Account, que la relation avec un User est obligatoire.

Account.php
copié !
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $owner = null;

Migrations

Les migrations sont les fichiers responsables de la mise à jour de la structure de votre base de données. On parle de schéma, de structure de base de données.

Elles permettent de versionner les modifications effectuées sur la base de données afin d’en faciliter et d’en sécuriser le déploiement.

Concrètement, une migration est une classe PHP qui va contenir le code SQL chargé de mettre à jour notre schéma de données.

Générer une migration

Lorsque nous avons créé nos premières entités dans notre application, nous allons pouvoir générer une première migration qui aura pour but de créer les tables et relations correspondantes en base de données.

copié !
symfony console make:migration

Cette ligne de commande générera un fichier 📄 migrations/Version[numero_de_version].php.

Vous constaterez que des fichiers de migration sont des classes qui possèdent 3 méthodes principales :

  • getDescription() : permet de décrire les modifications apportées à travers notre migration (c’est un peu comme le message d’un git commit). Par défaut, cette méthode retourne une chaîne de caractères vide, mais si vous le souhaitez vous pouvez rédiger une description vous-même.
  • up() : cette fonction contient les instructions SQL qui vont mettre à jour la structure de la base de données.
  • down() : cette fonction contient les instructions SQL qui vont annuler les potentielles modifications apportées par la méthode up().

Appliquer une migration

Une fois la migration générée, il faut l’appliquer pour voir des changements du côté de notre base de données.

copié !
symfony console doctrine:migrations:migrate

La structure de votre base de données est dorénavant identique à celle de vos entités.

doctrine:migrations:migrate exécute en réalité la dernière migration générée mais il est possible d’exécuter / annuler une migration spécifique avec la commande :

copié !
symfony console doctrine:migrations:execute 'DoctrineMigrations\Version20200619074820' --up
  • Numéro de version : ici DoctrineMigrations\Version20200619074820 (ne pas oublier DoctrineMigrations\ au début).
  • Méthode à exécuter : --up indique qu’on exécute la migration et --down indique qu’on l’annule.

Les commandes symfony console make:migration et symfony console doctrine:migrations:migrate constituent une routine que vous devrez systématiquement effectuer après avoir mis à jour vos entités.