Hey salut, bienvenue dans cette série de tutoriel sur Symfony! Dans la dernière partie, nous avons parlé des vues et nous avons mis en place quelques vues de notre application. Dans ce tutoriel, nous allons nous attaquer aux entités de notre application, nous allons créer nos tables en base de données et définir la relation entre nos entités.
Avant de commencer, il faut avoir le code source pour pouvoir pratiquer avec moi. Le code est disponible sur Github et tu peux donc le cloner:
$ git clone https://github.com/kaherecode/symfony-ticketing-app.git
$ cd symfony-ticketing-app
$ git checkout views
$ composer install
Le git checkout views
va te permettre de te placer au même niveau que moi au début de cet article.
Doctrine
Symfony utilise Doctrine pour la gestion des entités. Doctrine est un ORM (Object Relational Mapping) qui va nous permettre d’écrire et lire dans notre base de données en utilisant que des objets en PHP, pas de requête SQL donc ou très rarement.
Disons par exemple nous avons notre objet $event
qui à été créer et nous devons l'enregistrer en base de données, avec SQL on fait une requête INSERT
, avec un ORM, on fait juste un $orm->save($event)
et c'est bon. Plus de SQL dans du PHP.
Tu peux lire plus sur la documentation de Doctrine.
Les entités
En utilisant un ORM comme doctrine, la base de données elle est abstraite pour nous, on n'y pense même pas, les tables dans la base de données sont représentés par des entités. Les entités sont des classes PHP comme on le connaît déjà avec des attributs et méthodes. Donc la classe (l'entité) représente la table dans la base de données et ses attributs vont représenter les champs de la table. Pour spécifier cela avec Symfony (Doctrine), il faut utiliser des annotations (ou des attributs depuis PHP 8), il faut donc commencer par créer une classe PHP.
Créer un entité
Alors tu préfères la méthode simple ou la méthode simple? Moi mon choix il est fait. Nous allons utiliser une commande pour générer nos entités, et c'est la commande:
$ symfony console make:entity
Nous allons commencer par créer un entité qui va représenter les événements de notre application, nous allons l'appeler Event
cet entité et il sera composé de:
- une image pour illustrer l'événement (
picture: string
,nullable: yes
) - un titre (
title: string
,nullable: no
) - un endroit ou l'événement va se dérouler (
address: string
,nullable: yes
) - une date à laquelle l'événement va se dérouler (
eventDate: datetime
,nullable: yes
), disclaimer - j'ai vraiment galérer pour trouver un nom pour cet attribut - une description (
description: text
,nullable: yes
) - un booléen pour savoir si l'événement est publié ou pas (
isPublished: boolean
,nullable: no
) - une date de création, de modification et de publication (
createAt: datetime_immutable
-nullable: no
,updatedAt: datetime
-nullable: yes
etpublishedAt: datetime_immutable
-nullable: yes
)
Voilà les attributs dont nous avons besoin pour l'instant, nous allons le compléter au fur et à mesure. On défini les champs comme address
et eventDate
comme étant null
par défaut, c'est pour permettre la création d'événements brouillons, il faudra ensuite faire un contrôle pour éviter de publier un événement sans renseigner ces champs.
Nous pouvons maintenant créer l'entité Event
avec la commande:
$ symfony console make:entity Event
Il faut ensuite renseigner les champs de notre entité, la console te demande d'abord le nom du champ (attribut de la classe PHP), c'est exactement comme le nom d'une variable, pas d'espace ni de caractères accentués, tu valides le nom avec Entrer
, puis tu choisis le type de ton attribut, par défaut le type string
est proposé, si ton attribut n'est pas de type string
, tu peux alors saisir le type de ton choix, text
ou datetime
par exemple ou saisir ?
pour voir les types disponibles, enfin il faut dire si cet attribut (le champ en base de données) peut être null
, là aussi par défaut le choix est no
(le champ ne peut pas être null
), si c'est pas le cas, tu saisi yes
et tu valides avec Entrer
, puis c'est le tour à un nouvel attribut. Voilà un peu à quoi ressemble la saisi pour l'attribut eventDate
par exemple:
Une fois que tu as renseigné tous les attributs, il faut valider avec Entrer
et là tu auras un message comme ici:
Et un nouveau fichier Event.php
est créer dans le dossier src/Entity/
dont voici le contenu:
<?php // src/Entity/Event.php
namespace App\Entity;
use App\Repository\EventRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=EventRepository::class)
*/
class Event
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $picture;
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $address;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $eventDate;
/**
* @ORM\Column(type="text", nullable=true)
*/
private $description;
/**
* @ORM\Column(type="boolean")
*/
private $isPublished;
/**
* @ORM\Column(type="datetime_immutable")
*/
private $createdAt;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $updatedAt;
/**
* @ORM\Column(type="datetime_immutable", nullable=true)
*/
private $publishedAt;
public function getId(): ?int
{
return $this->id;
}
public function getPicture(): ?string
{
return $this->picture;
}
public function setPicture(?string $picture): self
{
$this->picture = $picture;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getAddress(): ?string
{
return $this->address;
}
public function setAddress(?string $address): self
{
$this->address = $address;
return $this;
}
public function getEventDate(): ?\DateTimeInterface
{
return $this->eventDate;
}
public function setEventDate(?\DateTimeInterface $eventDate): self
{
$this->eventDate = $eventDate;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getIsPublished(): ?bool
{
return $this->isPublished;
}
public function setIsPublished(bool $isPublished): self
{
$this->isPublished = $isPublished;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getPublishedAt(): ?\DateTimeImmutable
{
return $this->publishedAt;
}
public function setPublishedAt(?\DateTimeImmutable $publishedAt): self
{
$this->publishedAt = $publishedAt;
return $this;
}
}
Tous les attributs ont été créés en plus de l'attribut $id
, et si tu remarques bien, cette classe a quelque chose en plus, c'est les annotations sur chaque attribut et aussi sur la classe. Ces annotations définissent la classe Event
comme étant un entité et il suffit juste de les lires pour comprendre, chaque attribut avec l'annotation @ORM\Column()
représente une colonne en base de données et entre les parenthèses nous avons le type du champ, sa longueur, s'il est nulle ou pas, … Tu te rappelles quand on créait notre table en SQL avec CREATE TABLE
et que l'on mentionne pour chaque champ le nom du champ, son type, est-ce qu'il peut être nul, … beh c'est la même chose ici, sauf qu'on n’écrit pas du SQL mais du PHP.
Par défaut le nom de la table en base de données aura le même nom que la classe et les champs aussi auront le même nom que les attributs respectifs, mais tu peux modifier tout cela. Tu peux lire la documentation pour en savoir plus.
Actuellement nous avons juste une classe PHP, il faut maintenant créer la table dans la base de données. Pour cela, il faut d'abord créer les migrations avec la commande (tout se passe en ligne de commande ici):
$ symfony console make:migration
Nous avons ensuite un message comme sur l'image:
Sur le message, la console nous demande de regarder le fichier migrations/Version20210823231900.php
, le nom du fichier est forcément différent chez toi, parce qu'il contient la date du jour et aussi l'heure de création du fichier et si tu lis ce tutoriel aujourd'hui, c'est parce que je l'avais écrit, au passé (dis moi dans les commentaires ci-dessous à quelle heure j'ai générer ce fichier), mais ce fichier sera toujours dans le dossier migrations
qui se trouve à la racine de ton projet. Dans mon cas, voici le contenu du fichier:
<?php
// migrations/Version20210823231900.php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210823231900 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE event (id INT AUTO_INCREMENT NOT NULL, picture VARCHAR(255) DEFAULT NULL, title VARCHAR(255) NOT NULL, address VARCHAR(255) DEFAULT NULL, event_date DATETIME DEFAULT NULL, description LONGTEXT DEFAULT NULL, is_published TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL, published_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE event');
}
}
Nous avons une classe avec le même nom que le fichier qui hérite de la classe Doctrine\Migrations\AbstractMigration
, tout va bien, mais cette classe contient deux fonctions qui nous intéresse, la fonction up()
et la fonction down()
, et toutes les deux fonctions font appel à une fonction addSql()
qui prend en paramètre une chaîne SQL. La fonction up()
pour créer une table event
et la fonction down()
pour supprimer la table event
, nous n'avons pas eu à écrire ce code SQL, il a été généré automatiquement en fonction des annotations que nous avons dans la classe Event.php
, mais tu peux le modifier comme l'indique le commentaire, pour notre cas, nous n'avons pas besoin de le modifier.
La commande make:migration
crée juste le code SQL à exécuter, il faut maintenant exécuter ce code pour créer la table en base de données, et là aussi devine nous allons utiliser quoi? Une commande bien sûr et c'est la commande:
$ symfony console doctrine:migrations:migrate
Et cette fois-ci aussi la console nous demande si on est vraiment sûr de ce qu'on fait? Réponds yes
puis Entrer
et elle nous indique les fichiers exécutés durant la migration et quelques infos sur le nombre de requêtes SQL:
Et si tu regardes ta base de données maintenant (utilise phpMyAdmin, DBeaver, …), tu dois avoir la table event
:
Et une autre table en plus, elle fait quoi ici elle? Regardons son contenu:
Elle contient juste des informations sur nos migrations.
Et la structure de la table event
:
Exactement comme sur les annotations.
Il nous faut ensuite un entité Tag
, nous allons dire qu'un événement à un ou plusieurs tags. Les tags vont nous permettre de catégoriser les événements et surtout pour moi, me permettre de te parler des relations entre nos entités. Il nous faut donc l’entité Tag
qui sera composé d'un champ label
puis l'id qui va être généré. Le label est une chaîne de caractères qui ne peut pas être null
. Je te laisse donc créer l’entité Tag
. Voici ma classe Tag.php
<?php
// src/Entity/Tag.php
namespace App\Entity;
use App\Repository\TagRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=TagRepository::class)
*/
class Tag
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $label;
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
}
Si tu as des soucis sur la génération de cette classe, essaie un peu de regarder comment nous avons fait avec l'entité event
, c'est la même chose. Si tu n'y arrives toujours pas, laisse un commentaire ci-dessous.
Et il faut ensuite créer les migrations:
$ symfony console make:migration
Un autre deuxième fichier est créé comme précédemment qui contient le code SQL pour créer la table tag
et la supprimer:
<?php
// migrations/Version20210823233019.php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210823233019 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE tag (id INT AUTO_INCREMENT NOT NULL, label VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE tag');
}
}
Il faut ensuite appliquer les migrations avec la commande:
$ symfony console doctrine:migrations:migrate
Et si on regarde en base de données, on a bien la nouvelle table tag
:
Et le contenu de la table doctrine_migration_versions
a changé:
La relation entre nos entités
Maintenant que nous avons nos entités, il faut faire la relation entre eux. Un événement comme nous l'avons dit, va être lier à un ou plusieurs tags et un tag peut se retrouver dans plusieurs événements, on a donc une relation plusieurs à plusieurs entre ces deux entités.
Il y a plusieurs types de relations qui peuvent exister entre des entités en Doctrine:
OneToOne
: c'est le type de relation le plus basique. Il permet de lier un entité à un seul autre entité. Prenons l'exemple de notre entitéEvent
et disons qu'au lieu d'avoir un attributpicture
pourEvent
, nous allons avoir un entitéPicture
et chaque objet$picture
sera lié à un et un seul objet$event
, c'est aussi valable pour un objet$event
qui sera lié à un et un seul objet$picture
ManyToOne
: ce type de relation défini un objet comme pouvant avoir référence à plusieurs autres objets. Restons toujours sur l'exemple avecEvent
etPicture
, mais cette fois-ci, un objet$event
peut avoir plusieurs images (Many), une galerie par exemple, dans l'autre sens un objet$picture
ne peut être lié qu'à un et un seul objet$event
(One)OneToMany
: cette relation est juste l'inverse de la relationManyToOne
, il sera pratique de l'utiliser quand tu voudras faire une relation bi-directionnelleManyToMany
: à ce niveau tu as déjà une idée de ce qu'est ce type de relation. Elle permet juste de dire que deux entités peuvent avoir plusieurs références entre eux. C'est le cas de nos entitésEvent
etTag
, un objet$event
a plusieurs objets$tags
et un objet$tag
a plusieurs objets$events
Nous n'aurons pas le temps de pouvoir tous les découvrir dans ce tutoriel. Je te recommande donc de lire la documentation sur les relations entre entités sur la documentation officielle.
Nous avons dans notre cas une relation ManyToMany
entre Event
et Tag
, il y a plusieurs Tag
(Many) lier à (To) un ou plusieurs Event
(Many). L'entité propriétaire est celui que tu veux dans ce cas, personnellement je préfère bien que l’entité propriétaire soit Event
. Nous allons donc rajouter la relation dans la classe Event.php
et pour cela: Une commande et c'est la même commande make:entity
:
$ symfony console make:entity Event
Quand j'entre la commande, la console me dit que l'entité existe déjà, nous allons donc le modifier. Et là, commence une vraie discussion entre la console et moi:
- Console: Quel nom vas-tu donner à ce nouvel attribut?
- Moi:
tags
, au pluriel parce qu'un événement peut en avoir plusieurs - Console: D'accord, quel est le type de
tags
(au pluriel)? - Moi:
ManyToMany
, le nom de la relation au fait - Console: Ah! Il veut créer une relation
ManyToMay
. A quelle classe cet attribut est relié? - Moi:
Tag
, la classeTag
- Console: Super, j'ai trouver la classe
Tag
, maintenant dis moi, est-ce que tu aimerais que je te rajoute dans la classeTag
un attribut qui te permettrait d'accéder aux objetsEvent
d'un tag avec$tag->getEvents()
par exemple pour avoir les événement du tagcoding
? Tu as le choix entreyes
etno
et je te proposeyes
par défaut - Moi: 🤔 (je reflechis un peu), oui j'aimerais bien, je réponds donc
yes
- Console: cool, un attribut sera ajouté à la classe
Tag
qui sera lié à la classeEvent
, mais comment est-ce que tu aimerais appeler cet attribut dans la classeTag
, saisi ton choix, mais je te proposeevents
, le nom de la classeEvent
au pluriel, mais tu fais ce que tu veux - Moi: OK je valide le nom
events
, tu ne fais que de bonnes propositions depuis que je t'ai connu, t'es extraordinaire (je parle toujours à la console) - Console: OK c'est fini, je t'ai modifier les fichiers
Event.php
etTag.php
pour faire exactement ce que tu m'as demandé, je reste disponible au besoin - Moi: Merci beaucoup Console, à tout de suite!
Super, maintenant allons regarder ces modifications, d'abord la classe Event.php
:
<?php
namespace App\Entity;
use App\Repository\EventRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=EventRepository::class)
*/
class Event
{
// …
/**
* @ORM\ManyToMany(targetEntity=Tag::class, inversedBy="events")
*/
private $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
// …
/**
* @return Collection|Tag[]
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
}
return $this;
}
public function removeTag(Tag $tag): self
{
$this->tags->removeElement($tag);
return $this;
}
}
Nous avons bien l'attribut $tags
avec l'annotation @ORM\ManyToMany
, puis il y a targetEntity
qui équivaut à la classe Tag
et un autre paramètre inversedBy
qui est égal à events
. Un constructeur a aussi été ajouter pour initialiser l'attribut $tags
en un ArrayCollection()
. Ensuite il y a trois méthodes qui ont été rajouter, getTags()
qui retourne la liste des tags d'un événement, addTag(Tag $tag)
qui ajoute un tag à l'événement, removeTag(Tag $tag)
pour retirer un tag de l'événement.
Puis la classe Tag.php
:
<?php
namespace App\Entity;
use App\Repository\TagRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=TagRepository::class)
*/
class Tag
{
// …
/**
* @ORM\ManyToMany(targetEntity=Event::class, mappedBy="tags")
*/
private $events;
public function __construct()
{
$this->events = new ArrayCollection();
}
// …
/**
* @return Collection|Event[]
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->addTag($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->removeElement($event)) {
$event->removeTag($this);
}
return $this;
}
}
C'est pratiquement la même chose que dans la classe Event
, ici par contre nous avons mappedBy="tags"
. Tu ne vois pas ce que c'est? Attends je t'explique.
Tu te rappelles du inversedBy
dans la classe Event
et dont la valeur est égale à events
? Eh bien le events
de l'annotation sur l'attribut $tags
dans la classe Event
vient de l'attribut $events
dans la classe Tag
, et l'attribut $events
dans la classe Tag
se défini comme étant lié (mappedBy
) à l'attribut $tags
dans la classe Event
😧😨. Ça peut être un peu flou, mais tout ça pour dire que nous avons une relation bidirectionnelle entre Event
et Tag
, c'est à dire que nous pouvons de la classe Event
accéder à la classe Tag
et inversement.
Il faut ensuite créer les migrations et les appliquer en base de données avec:
$ symfony console make:migration
$ symfony console doctrine:migrations:migrate
Cette fois-ci, une table event_tag
a été créée:
Cette table contient une référence à la table event
et tag
:
Et nous n'avons écrit aucune ligne de code SQL, je crois que je comprends pourquoi je suis vraiment si nul en SQL.
Voilà, nous avons maintenant nos entités, dans la prochaine partie, nous allons parler un peu plus de doctrine, du gestionnaire des entités pour voir comment ajouter ou supprimer nos entités en base de données et aussi des repositories pour pouvoir lire nos entités.
Si tu as des questions, n'hésite pas, fonce dans les commentaires ci-dessous et je ferais mon possible pour te répondre le plus tôt possible ou retrouve moi sur le serveur discord de Kaherecode. A très bientôt.