Salut! Bienvenue dans cette troisième partie sur comment créer un blog avec Symfony 4. Dans la première partie, nous avons créer et poser les bases pour notre projet, puis nous avons rajouter les vues de nos pages sur la deuxième partie, dans ce tutoriel, nous allons nous attaquer aux entités de notre application, à savoir les articles et catégories. Aller c'est parti.

Doctrine

Symfony utilise Doctrine pour la gestion de la base de données. Doctrine est un ORM (Object Relational Mapping) qui va nous permettre d’écrire et lire dans notre base de données en utilisant que du PHP, pas de requête SQL donc (à moins que ce soit un cas vraiment précis).

Disons par exemple nous avons notre objet $article qui à été créer et nous devons l'enregistrer en base de données, d'habitude on fait une requête INSERT, avec un ORM, nous n'allons pas du tout nous fatiguer avec tout cela,  on fait juste un $orm->save($article) et c'est bon. Plus de SQL dans du PHP.

Vous pouvez lire plus sur la documentation de Dotrine.

Définir nos entités

En utilisant un ORM comme doctrine, comme on l'a dit 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 ce qu'on appelle 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é) est la table dans la base de données et ses attributs vont représenter les champs de la table.

Créer une entité

Nous allons commencer par créer notre entité Article. Un article il est composer de quoi?

  • Une image pour illustrer l'article (picture: string)
  • Un titre (title: string)
  • Un contenu, oui forcement (content: text)
  • Une ou plusieurs catégories (categories: Category[])
  • Une date de publication et de dernière modification (publicationDate: datetime, lastUpdateDate: datetime)
  • Et un booléen pour savoir si l'article est publier ou pas, pour nous permettre d'avoir des brouillons (isPublished: boolean)

Nous avons donc 7 champs pour l’entité article en plus de l'id. Mais une chose avant de continuer, le champ catégories est un tableau de type Category qui est aussi une entité, ce qui veut dont dire que nous devrons créer une entité Category et faire la relation entre Category et Article. Nous verrons cela plus tard, pour l'instant, nous allons créer l’entité Article avec les champs picture, title, content, publicationDate, lastUpdateDate et isPublished. Le champ id sera automatiquement générer.

Comme nous l'avons dit auparavant, une entité est une classe PHP, les entités se trouvent dans le dossier src/Entity. Mais nous n'allons pas foncer et créer nous meme le fichier pour ensuite le remplir, nous sommes trop paresseux pour cela, nous allons donc utiliser la commande:

$ php bin/console make:entity

A exécuter à la racine de votre projet.

Il va donc vous être demander de rentrer le nom de votre classe, entrer Article, puis la console nous dit que les fichiers Article.php et ArticleRepository.php ont été créés. Maintenant il faut renseigner les champs de notre entité:

  • picture: string (255), nullable = yes
  • title: string (255), nullable = no
  • content: text, nullable = yes
  • publicationDate: datetime, nullable = yes
  • lastUpdateDate: datetime, nullable = no
  • isPublished: boolean, nullable = no

Si tout est bon, le message Success s'affiche, nous pouvons donc ouvrir le fichier src/Entity/Article.php

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @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="text", nullable=true)
     */
    private $content;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $publicationDate;

    /**
     * @ORM\Column(type="datetime")
     */
    private $lastUpdateDate;

    /**
     * @ORM\Column(type="boolean")
     */
    private $isPublished;

    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 getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(?string $content): self
    {
        $this->content = $content;

        return $this;
    }

    public function getPublicationDate(): ?\DateTimeInterface
    {
        return $this->publicationDate;
    }

    public function setPublicationDate(?\DateTimeInterface $publicationDate): self
    {
        $this->publicationDate = $publicationDate;

        return $this;
    }

    public function getLastUpdateDate(): ?\DateTimeInterface
    {
        return $this->lastUpdateDate;
    }

    public function setLastUpdateDate(\DateTimeInterface $lastUpdateDate): self
    {
        $this->lastUpdateDate = $lastUpdateDate;

        return $this;
    }

    public function getIsPublished(): ?bool
    {
        return $this->isPublished;
    }

    public function setIsPublished(bool $isPublished): self
    {
        $this->isPublished = $isPublished;

        return $this;
    }
}

Tous les attributs ont été créés en plus de l'attribut $id, la nouveauté pour vous ici c'est peut être les annotations qu'il y a avant chaque attribut, mais il suffit juste de les lires pour comprendre, chaque attribut représente une colonne en base de données et entre les parentheses nous avons le type du champs, sa longueur, ... Vous vous rappelez quand on créait notre table en SQL avec CREATE TABLE et que l'on mentionnait pour chaque champ le nom du champ, son type, est-ce qu'il peut être null, ... beh c'est la meme 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 meme nom que la classe et les champs aussi auront le meme nom que les attributs respectifs, mais vous pouvez modifier tout cela. Veillez lire la documentation.

Actuellement nous avons juste une classe PHP, il faut maintenant créer la table dans la base de données. Mais avant nous allons d'abord configurer l’accès à notre base de données.

Pour cela nous allons ouvrir le fichier .env qui se trouve à la racine du projet, sur la ligne 27 nous avons:

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name

Nous allons naturellement utiliser MySQL, mais il y a plusieurs integrations possible SQLite, PostgreSQL, ...

Ce que nous allons faire, c'est de créer un fichier .env.local à la racine de notre projet et coller cette ligne dans ce fichier. Alors pourquoi utiliser un nouveau fichier? Pour la simple raison que le fichier .env n'est pas ignorer par git, ce qui veut donc dire que si vous publier votre code sur Github par exemple, tout le monde aura accès à votre mot de passe, et il y aura toujours un bad guy pour mal l'utiliser. Par contre le fichier .env.local est lui ignorer par git, il ne quittera jamais donc votre ordinateur.

Nous allons donc mentionner le nom d'utilisateur de notre base de données (db_user), son mot de passe (db_password) et le nom de la base de données (db_name) dans le fichier .env.local, dans mon cas j'aurais:

#.env.local
DATABASE_URL=mysql://orion:pass@127.0.0.1:3306/symfony_blog

Vous pouvez lire plus sur les fichiers .env sur la documentation de Symfony.

Nous allons ensuite créer la base de données avec:

$ php bin/console doctrine:database:create
Created database `symfony_blog` for connection named default

Voilà!

Je peux maintenant créer la table article dans cette nouvelle base de données. Pour cela je vais vous presenter une commande (tout se passe en ligne de commande ici) avec deux options. La première

$ php bin/console doctrine:schema:update --dump-sql

Cette commande va nous afficher la requête SQL à exécuter s'il y a lieu d’être, et comme vous pouvez le voir, nous avons bien une requête CREATE TABLE, mais nous nous allons pas écrire du SQL, on rappelle la meme commande avec l'option --force pour dire execute cette requête SQL dans la base de données

$ php bin/console doctrine:schema:update --force

Et qu'avons nous? 1 requête a été exécuter avec un joli message OK. Si vous voulez vous pouvez ouvrir MySQL et vous verrez que c'est pour de vrai, d'ailleurs je vous invite à le faire, comme ça vous pourrez un peu comprendre la relation entre les annotations dans la classe PHP et la base de données.

Vous vous souvenez de la deuxième entité dont nous avons besoin? Un article a un ou plusieurs categories, il nous faut donc l’entité Category qui sera composer d'un champ label plus l'id. Le label est une chaîne de caractères qui ne peut pas être null. Je vous laisse donc créer l’entité Category. Voici ma classe Category.php

<?php
// src/Entity/Category.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\CategoryRepository")
 */
class Category
{
    /**
     * @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;
    }
}

Puis nous allons mettre à jour la structure de la base de données

$ php bin/console doctrine:schema:update --dump-sql

Pour voir la requête SQL à exécuter

$ php bin/console doctrine:schema:update --force

Pour exécuter la requête. La table category est maintenant créer.

Les relations entre nos entités

Rappelez-vous, un article a un ou plusieurs catégories et une catégories peut se retrouver dans plusieurs articles, on a donc une relation plusieurs a plusieurs entre ces deux entités.

Vous pouvez lire sur les relations entre entités sur la documentation officielle.

Nous avons dans notre cas une relation ManyToMany entre Article et Category, il y a plusieurs Category (Many) lier à (To) un ou plusieurs Article (Many). L'entité propriétaire est celui que vous voulez dans ce cas, personnellement je préfère bien que l’entité propriétaire soit Article. Nous allons donc rajouter la relation dans la classe src/Entity/Article.php

<?php
// src/Entity/Article.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    // ..

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Category")
     */
    private $categories;

    public function getId(): ?int
    {
        return $this->id;
    }

    // ...
}

Nous ajoutons la relation avec l'annotation puis nous définissons un attribut $categories au pluriel, il ne faut pas le manquer, l'article à plusieurs Category (catégories). Il faut maintenant créer les Getter et Setter pour cet attribut, encore une ligne de commande

$ php bin/console make:entity --regenerate

Il va vous être demander quelle classe vous voulez régénérer, par défaut le namespace App\Entity est sélectionner, appuyez juste sur Entrer, les classes qui ont été modifier seront mises à jour.

Si on ouvre la classe Article.php, on voit bien le changement

<?php
// src/Entity/Article.php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    // ...

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Category")
     */
    private $categories;

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

    // ...

    /**
     * @return Collection|Category[]
     */
    public function getCategories(): Collection
    {
        return $this->categories;
    }

    public function addCategory(Category $category): self
    {
        if (!$this->categories->contains($category)) {
            $this->categories[] = $category;
        }

        return $this;
    }

    public function removeCategory(Category $category): self
    {
        if ($this->categories->contains($category)) {
            $this->categories->removeElement($category);
        }

        return $this;
    }
}

Un constructeur a été rajouter pour initialiser l'attribut $categories en un ArrayCollection(), un ArrayCollection() est une collection qui contient un tableau PHP, pour obtenir un tableau PHP on fera $categories->toArray(), je vous invite à lire sa documentation. Ensuite il y a trois méthodes qui ont été rajouter, getCategories() qui retourne la liste des catégories d'un article, addCategory(Category $category) qui rajoute une catégorie à l'article, removeCategory(Category $category) pour retirer une catégorie de l'article.

Now il faut mettre à jour la base de données, nous allons utiliser la ligne de commande comme d'habitude

$ php bin/console doctrine:schema:update --dump-sql

puis 

$ php bin/console doctrine:schema:update --force

Que passa? Une table article_category a été créée, cette table contient une reference à la table article et category, vous pouvez vérifier, nous on a écrit aucune ligne de code SQL, je crois que je comprends pourquoi je suis si nulle en SQL.

Les relations bidirectionnelles

Avec notre code actuel, tout marche bien, on peut faire $article->getCategories() pour récupérer toutes les categories d'un article, mais qu'est ce qui se passe dans le cas où l'on veut aussi récupérer tout les articles d'une catégorie? On peut passer par le repository et écrire une requête DQL (Doctrine Query Language), mais flemme. Nous allons faire de sorte que la relation entre Article et Category soit dans les deux sens, c'est à dire que l'on puisse récupérer les categories d'un article (on l'a déjà) mais aussi les articles d'une catégorie, nous allons donc rendre la relation bidirectionnelle.

Dans l’entité inverse, nous allons rajouter

<?php
// src/Entity/Category.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\CategoryRepository")
 */
class Category
{
    // ...

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Article", mappedBy="categories")
     */
    private $articles;

    public function getId(): ?int
    {
        return $this->id;
    }

    // ...
}

Pratiquement la meme chose que dans l’entité propriétaire Article, sauf que là nous rajoutons une autre information mappedBy="categories" qui correspond à l'attribut dans l’entité propriétaire (Article: private $categories) qui pointe vers l’entité inverse (Category). Il faut maintenant modifier l’entité propriétaire Article aussi pour lui dire qu'il y a maintenant une relation bidirectionnelle

<?php
// src/Entity/Article.php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    // ...

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Category", inversedBy="articles")
     */
    private $categories;

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

    // ...
}

Et voilà, on rajoute juste inversedBy="articles" qui est aussi le nom de l'attribut dans l’entité inverse Category.

Il faut maintenant générer les Getter et Setter pour Category

$ php bin/console make:entity --regenerate

Et la classe src/Entity/Category.php

<?php
// src/Entity/Category.php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\CategoryRepository")
 */
class Category
{
    // ...

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Article", mappedBy="categories")
     */
    private $articles;

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

    // ...

    /**
     * @return Collection|Article[]
     */
    public function getArticles(): Collection
    {
        return $this->articles;
    }

    public function addArticle(Article $article): self
    {
        if (!$this->articles->contains($article)) {
            $this->articles[] = $article;
            $article->addCategory($this);
        }

        return $this;
    }

    public function removeArticle(Article $article): self
    {
        if ($this->articles->contains($article)) {
            $this->articles->removeElement($article);
            $article->removeCategory($this);
        }

        return $this;
    }
}

Ici pas la peine de mettre à jour la base de données, vu que rien a changer. Vous pouvez essayer voir.

Voilà, nous avons maintenant nos entités, cet article est déjà long, nous allons nous limiter ici et dans la prochaine partie, nous verrons les formulaires et comment ajouter, modifier, lire et supprimer nos entités en base de données.

Vous pouvez trouver le code source de cette partie sur le dépôt officiel.

Si vous avez des questions, n'hésiter pas, foncer dans les commentaires ci-dessus et je ferais mon possible pour vous répondre le plus tot possible. A bientôt.


Partager cet article

alioukahere

Mamadou Aliou Diallo

@alioukahere

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