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.
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.
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 :
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 port3307
. 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 SQLSHOW 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 :
symfony console doctrine:database:create
Suppression
Si vous souhaitez supprimer votre base de données pour une quelconque raison, taper la commande :
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
: integertitle
: string(255)content
: textcreatedAt
: datetime
Créer ses entités à la main est laborieux, et comme pour toute tâche laborieuse, il existe une solution : le MakerBundle !
symfony console make:entity
Cette ligne de commande va vous demander plusieurs informations :
- 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
- 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). - 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’attributColumn()
ç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.
<?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 classeArticleRepository
(automatiquement créée - détailée par la suite), la classe DoctrineTypes
dédiée au typage des propriétés, ainsi que la classe DoctrineMapping
, 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’argumentname
. L’attributColumn
a de nombreux autres paramètres, mais nous en utilisons la plupart du temps 4 :
unique
: précise que la valeur de cette colonne doit être unique (par exemple un e-mail ou un pseudo utilisateur).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.length
: permet de définir la longueur maximale d’une chaîne de caractère.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 :
- 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…).
- 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.
symfony console make:entity NomEntite
Supprimer une entité
Pour supprimer une entité, on supprime :
- Le fichier de l’entité en question :
📄 src/Entity/Nom.php
- 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
etUser.php
- Sur Netflix, un film / une série est rattaché(e) à une catégorie.
Movie.php
/Serie.php
etCategory.php
- Sur Blablacar, un trajet est réservé par un utilisateur.
Road.php
etUser.php
- Sur Amazon, un utilisateur peut enregistrer une ou plusieurs adresses de livraison.
User.php
etAdress.php
- Sur laConsole, une leçon est rattachée à une formation.
Lesson.php
etCourse.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 :
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
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 { ... }
}
$image
est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).ORM\OneToOne
est un attribut, spécifiant la multiplicité1-1
de la relation. L’argumenttargetEntity
précise que l’entité liée estImage
.- 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
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 { ... }
}
$image
est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).ORM\OneToOne
est un attribut, spécifiant la multiplicité1-1
de la relation. L’argumenttargetEntity
précise que l’entité liée estImage
. L’argumentinversedBy
référence la propriété$article
qui porte la relation côté entité inverse.- Getter et setter de la propriété.
Entité inverse
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 { ... }
}
$article
est la propriété relationnelle faisant le lien avec l’entité propriétaire.ORM\OneToOne
est un attribut, spécifiant la multiplicité1-1
de la relation. L’argumenttargetEntity
précise que l’entité liée estArticle
. L’argumentmappedBy
référence la propriété$image
qui porte la relation côté entité propriétaire.- 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 tablearticles
. - 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
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 { ... }
}
$category
est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).ORM\ManyToOne
est un attribut, spécifiant la multiplicitén-1
de la relation. L’argumenttargetEntity
précise que l’entité liée estCategory
.- 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
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 {}
}
$articles
est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).ORM\OneToMany
est un attribut, spécifiant la multiplicité1-n
de la relation. L’argumenttargetEntity
précise que l’entité liée estArticle
.- Le getter et les méthodes d’ajout
addArticle()
et de suppressionremoveArticle()
remplacent le setter car$articles
est voué à contenir un tableau d’objets, appelé Collection. - Import des classes de Collection de Doctrine avec nos
use
. - 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
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 { ... }
}
$category
est la propriété relationnelle faisant le lien avec l’autre entité (équivalent de clé étrangère).ORM\ManyToOne
est un attribut, spécifiant la multiplicitén-1
de la relation. L’argumenttargetEntity
précise que l’entité liée estCategory
. L’argumentinversedBy
référence la propriété$articles
qui porte la relation côté entité inverse.- Getter et setter de la propriété.
Entité inverse
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 {}
}
$articles
est la propriété relationnelle faisant le lien avec l’entité propriétaire.ORM\OneToMany
est un attribut, spécifiant la multiplicité1-n
de la relation. L’argumenttargetEntity
précise que l’entité liée estArticle
. L’argumentmappedBy
référence la propriété$category
qui porte la relation côté entité propriétaire.- Le getter et les méthodes d’ajout
addArticle()
et de suppressionremoveArticle()
remplacent le setter car$articles
est voué à contenir un tableau d’objets, appelé Collection. - Import des classes de Collection de Doctrine avec nos
use
. - 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
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 {}
}
$categories
est la propriété relationnelle faisant le lien avec l’autre entité.ORM\ManyToMany
est un attribut, spécifiant la multiplicitén-n
de la relation. L’argumenttargetEntity
précise que l’entité liée estCategory
.- Le getter et les méthodes d’ajout
addCategory()
et de suppressionremoveCategory()
remplacent le setter car$categories
est voué à contenir un tableau d’objets, appelé Collection. - Import des classes de Collection de Doctrine avec nos
use
. - 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
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 {}
}
$categories
est la propriété relationnelle faisant le lien avec l’autre entité.ORM\ManyToMany
est un attribut, spécifiant la multiplicitén-n
de la relation. L’argumenttargetEntity
précise que l’entité liée estCategory
. L’argumentinversedBy
référence la propriété$articles
qui porte la relation côté entité inverse.- Le getter et les méthodes d’ajout
addCategory()
et de suppressionremoveCategory()
remplacent le setter car$categories
est voué à contenir un tableau d’objets, appelé Collection. - Import des classes de Collection de Doctrine avec nos
use
. - Ajout d’un constructeur pour initialiser notre propriété
$categories
comme une collection vide.
Entité inverse
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 {}
}
$articles
est la propriété relationnelle faisant le lien avec l’entité propriétaire.ORM\ManyToMany
est un attribut, spécifiant la multiplicitén-n
de la relation. L’argumenttargetEntity
précise que l’entité liée estArticle
. L’argumentmappedBy
référence la propriété$categories
qui porte la relation côté entité propriétaire.- Le getter et les méthodes d’ajout
addArticle()
et de suppressionremoveArticle()
remplacent le setter car$articles
est voué à contenir un tableau d’objets, appelé Collection. - Import des classes de Collection de Doctrine avec nos
use
. - 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.
#[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.
Valeur | Description |
---|---|
persist | Si l’entité A est sauvegardée, faire de même avec l’entité B associée. |
remove | Si 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
.
Statut | Description |
---|---|
nullable: true | Une entité A peut être liée à une entité B. Facultatif car il s’agit du comportement par défaut. |
nullable: false | Une 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 :
#[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.
#[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.
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’ungit 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éthodeup()
.
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.
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 :
symfony console doctrine:migrations:execute 'DoctrineMigrations\Version20200619074820' --up
- Numéro de version : ici
DoctrineMigrations\Version20200619074820
(ne pas oublierDoctrineMigrations\
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.