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 Doctrine.
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 publié 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 donc 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 même 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 = yestitle
: string (255), nullable = nocontent
: text, nullable = yespublicationDate
: datetime, nullable = yeslastUpdateDate
: datetime, nullable = noisPublished
: 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 parenthèses 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 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 vous pouvez modifier tout cela. Veuillez 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 intégrations 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 présenter 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 même commande avec l'option --force
pour dire exécute 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écuté 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 à un ou plusieurs catégories, il nous faut donc l'entité Category
qui sera composée 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é modifiés 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.
Maintenant 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 référence à 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 bi-directionnelles
Avec notre code actuel, tout marche bien, on peut faire $article->getCategories()
pour récupérer toutes les catégories d'un article, mais qu'est ce qui se passe dans le cas où l'on veut aussi récupérer tous 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 catégories 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 même 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 tôt possible. A bientôt.
Participe à la discussion