Kaherecode

Créer une application web avec Symfony - Les entités

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:

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:

https://imgur.com/hhhSPYT.jpg

Une fois que tu as renseigné tous les attributs, il faut valider avec Entrer et là tu auras un message comme ici:

https://imgur.com/d6NpDEH.jpg

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:

https://imgur.com/HVAWZ6w.jpg

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:

https://imgur.com/Hxza5ud.jpg

Et si tu regardes ta base de données maintenant (utilise phpMyAdmin, DBeaver, …), tu dois avoir la table event:

https://imgur.com/oGL44YS.jpg

Et une autre table en plus, elle fait quoi ici elle? Regardons son contenu:

https://imgur.com/Wj21mGj.jpg

Elle contient juste des informations sur nos migrations.

Et la structure de la table event:

https://imgur.com/tMi2oLh.jpg

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:

https://imgur.com/1hixJiw.jpg

Et le contenu de la table doctrine_migration_versions a changé:

https://imgur.com/sHwzJK8.jpg

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:

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

https://imgur.com/cZCzve4.jpg

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:

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:

https://imgur.com/McNmjeq.jpg

Cette table contient une référence à la table event et tag:

https://imgur.com/czpOGkZ.jpg

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.


Merci à

Mamadou Aliou Diallo

Mamadou Aliou Diallo

@alioukahere

Développeur web fullstack avec une passion pour l’entrepreneuriat et les nouvelles technologies. Fondateur de Kaherecode.

Continue de lire