Hey salut, bienvenue dans cette série de tutoriel sur Symfony. Dans les précédentes partie nous somme allés jusqu'à créer les relations entre nos entités. Dans cette partie nous allons voir comment utiliser le gestionnaire d'entité de Symfony pour ajouter, modifier ou supprimer des objets en base de données et nous allons aussi utiliser les repositories pour pouvoir lire nos enregistrements. Alors t'es prêt? On commence tout de suite.
Mais avant, il faut cloner le dépôt du projet pour pouvoir pratiquer avec moi:
$ git clone https://github.com/kaherecode/symfony-ticketing-app.git
$ cd symfony-ticketing-app
$ git checkout entities
$ composer install
Et voilà, c'est partit.
EntityManager
Le gestionnaire d'entités (EntityManager
) est un objet Doctrine qui va nous permettre de Gérer nos entités, nous allons l'utiliser pour ajouter, modifier ou supprimer des objets en base de données. Pour récupérer l'EntityManager dans un contrôleur:
- Si le contrôleur hérite de la classe
Symfony\Bundle\FrameworkBundle\Controller\AbstractController
, on peut faire$this→getDoctrine()→getManager()
- Si le contrôleur n'hérite pas de la classe
Symfony\Bundle\FrameworkBundle\Controller\AbstractController
, il faut ajouter un argument à la fonction comme cecicreateProduct(EntityManagerInterface $entityManager)
Le deuxième point est valable même si le contrôleur hérite de la classe Symfony\Bundle\FrameworkBundle\Controller\AbstractController
.
Ajouter des objets
Nous allons créer et ajouter un objet Event
en base de données, pour cela nous allons définir une nouvelle route /events/create
et nous allons la mettre dans la classe EventController
que nous avons déjà créer.
<?php
// src/Controller/EventController.php
namespace App\Controller;
use App\Entity\Event;
use App\Entity\Tag;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class EventController extends AbstractController
{
/**
* @Route("/events/create", name="create_event")
*/
public function createEvent(): Response
{
// on crée un événement, ces données pourraient venir d'un formulaire
$event = new Event();
$event->setPicture('https://images.pexels.com/photos/251225/pexels-photo-251225.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260');
$event->setTitle('À la découverte du développement web');
$event->setAddress('Sacré Coeur 3 VDN, Dakar');
$event->setDescription('Lorem ipsum dolor sit amet consectetur
adipisicing, elit. Libero tenetur beatae repellendus possimus magni
quae! Impedit soluta sit iusto amet unde repudiandae fugit
perspiciatis, deleniti quod placeat.');
// la date de l'événement c'est dans 14 jours à 10h30
$event->setEventDate((new DateTime('+14 days'))->setTime(10, 30));
$event->setIsPublished(true); // on publie l'événement
$event->setPublishedAt(new DateTimeImmutable());
// on crée un deuxième événement qui ne sera pas publié pour l'instant
$event2 = new Event();
// on renseigne seulement le titre qui est obligatoire
$event2->setTitle('Événement à venir, pas encore publique');
// on ajoute quelques tags à l'événement
$webTag = new Tag();
$webTag->setLabel('web');
$event->addTag($webTag);
$codeTag = new Tag();
$codeTag->setLabel('code');
$event->addTag($codeTag);
/* on récupère le gestionnaire d'entités qui va nous permettre
d'enregistrer l'événement */
$entityManager = $this->getDoctrine()->getManager();
/* on confie l'objet $event au gestionnaire d'entités,
l'objet n'est pas encore enregistrer en base de données */
$entityManager->persist($event);
// on confie aussi l'objet $event2 au gestionnaire d'entités
$entityManager->persist($event2);
/* on exécute maintenant les 2 requêtes qui vont ajouter
les objets $event et $event2 en base de données */
$entityManager->flush();
return new Response(
"Les événements {$event->getTitle()} et {$event2->getTitle()}
ont bien été enregistrés."
);
}
// ...
}
On crée deux objets $event
et $event2
que nous allons ajouter en base de données par la suite, l'objet $event
est disponible publiquement et l'objet $event2
pas encore. La partie création des objets est plutôt basique. Laisse moi t'expliquer un peu comment nous utilisons l'EntityManager:
- Sur la ligne 54, nous récupérons le gestionnaire d'entités avec
$this→getDoctrine()→getManager()
. Dans la dernière section je te disais que tu peux faire comme cela ou passer le gestionnaire d'entités en paramètre à la fonction comme cecicreateProduct(EntityManagerInterface $entityManager)
- Sur la ligne 58, nous prenons l'objet
$event
et nous le confions au gestionnaire d'entités. À partir de là, le gestionnaire d'entités connaît l'existence de l'objet$event
, il va donc l'ajouter à la liste d'objets à gérer. La fonctionpersist($event)
n'exécute aucune requête encore - Sur la ligne 61, nous faisons la même chose pour l'objet
$event2
. Maintenant le gestionnaire d'entités gère deux objets$event
et$event2
. Si tu as d'autres objets, il faudra faire la même chose pour que le gestionnaire d'entités connaissent leur existences - Sur la ligne 65, la fonction
flush()
va exécuter les requêtes SQL pour chaque objet qui se trouve dans la liste des objets à gérer du gestionnaire d'entités. Dans notre cas, les objets$event
et$event2
sont ajoutés en base de données, et nous, on n'a écrit aucune requête SQL
Avant d'aller tester l'enregistrement de nos objets, nous allons juste un peu modifier l'entité Event
pour mentionner les valeurs par défaut createdAt
et isPublished
:
<?php
// src/Entity/Event.php
namespace App\Entity;
use App\Repository\EventRepository;
use DateTime;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=EventRepository::class)
*/
class Event
{
// ...
/**
+ * @ORM\Column(type="boolean", options={"default": false})
*/
private $isPublished;
// ...
public function __construct()
{
$this->tags = new ArrayCollection();
+ $this->createdAt = new DateTimeImmutable();
+ $this->isPublished = false;
}
// ...
}
J'en ai aussi profité pour définir le champ isPublished
comme false
par défaut en base de données. Il faut donc exécuter les migrations pour le prendre en compte:
$ symfony console make:migration
$ symfony console doctrine:migrations:migrate
Tu peux maintenant naviguer sur la page https://localhost:8000/events/create pour enregistrer les données:
Oouuppsss! On a une erreur. L'erreur dit qu'il y a des entités qui n'ont pas été persisté. Nous avons juste persister les objets $event
et $event2
, mais l'objet $event
contient deux autres objets $codeTag
et $webTag
dont nous n'avons pas persister, le gestionnaire d'entités ne sait donc pas quoi faire de ces objets, il ne peut pas les enregistrer parce qu'on ne lui a pas dit de le faire, mais il a besoin des identifiants de ces objets pour faire la relation. Nous pouvons soit persister les deux objets $codeTag
et $webTag
et nous ferons la même chose à chaque fois, même si l'événement à 10 tags, ou nous pouvons demander à Doctrine de gérer cela automatiquement, il devra persister les objets et les ajouter en base de données s'ils n'existent pas. Pour cela, nous allons modifier l'entité Event
au niveau de la relation entre Event
et Tag
:
<?php
// src/Controller/Event/php
namespace App\Entity;
use App\Repository\EventRepository;
use DateTime;
use DateTimeImmutable;
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",
+ * cascade={"persist"}
+ * )
*/
private $tags;
// ...
}
Nous rajoutons cascade={"persist"}
dans l'annotation, quand tu retournes maintenant sur la page https://localhost:8000/events/create:
Pour vraiment être sûr que l'enregistrement a passer, exécute la commande:
$ symfony console doctrine:query:sql 'select title, event_date, created_at from event'
Et là biimmmm!!!
On a bien les deux événements enregistrer. Dans les prochaines sections, voyons comment récupérer des données depuis la base de données et les afficher sur notre page d'accueil.
Lire des objets depuis la BDD
Pour lire des objets depuis la base de données, nous allons utiliser ce qu'on appelle un repository
. Un repository c'est une classe PHP qui va nous permettre de récupérer les objets d'un entité depuis la base données. Lorsque nous avons généré l'entité Event
par exemple, deux classes ont été créés: Event
et EventRepository
. C'est le cas pour toutes les entités.
Regardons ensemble le contenu du fichier EventRepository.php
dans le dossier src/Repository/
:
<?php
namespace App\Repository;
use App\Entity\Event;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Event|null find($id, $lockMode = null, $lockVersion = null)
* @method Event|null findOneBy(array $criteria, array $orderBy = null)
* @method Event[] findAll()
* @method Event[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class EventRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Event::class);
}
// /**
// * @return Event[] Returns an array of Event objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('e')
->andWhere('e.exampleField = :val')
->setParameter('val', $value)
->orderBy('e.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Event
{
return $this->createQueryBuilder('e')
->andWhere('e.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
La classe est plutôt basique, elle hérite d'une classe ServiceEntityRepository
qui vient de Symfony. Dans ce fichier nous allons définir toutes les méthodes qui vont nous permettre de récupérer des objets événements depuis la base de données. Le fichier contient déjà deux fonctions qui sont commentés pour nous montrer un exemple, nous reviendrons sur ceux-là plus tard.
Si tu regardes sur la Docblock avant la définition de la classe, tu vas voir quatres méthodes find
, findOneBy
, findAll
et findBy
. Ces méthodes sont prédéfinis par Symfony et sont disponibles pour tous les repositories, elles sont définies dans une classe EntityRepository
qui se trouve dans le namespace Doctrine\ORM
. Généralement ces méthodes suffisent largement, mais il peut y avoir des cas spéciales ou il faut toi même créer tes propres méthodes, nous parlerons de cela plus tard, pour l'instant, utilisons les méthodes de Symfony pour récupérer la liste des événements et les afficher sur la page d'accueil.
Le contrôleur de la page d'accueil se trouve dans la classe CoreController
et c'est la méthode homepage()
. Tu te rappelles de ce qu'on disait lorsque nous avons parler de l'architecture MVC? Le contrôleur va chercher les modèles nécessaires puis les envoyer à la vue qui va construire la page HTML et ensuite le contrôleur frontal retourne cette page. Eh bien, nous avons notre contrôleur homepage()
, la vue core/index.html.twig
, il nous manque le modèle qui est la liste des événements à afficher sur la page d'accueil. Voici le contrôleur homepage()
modifier:
<?php
// src/Controller/CoreController.php
namespace App\Controller;
use App\Repository\EventRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(EventRepository $eventRepository): Response
{
$events = $eventRepository->findAll();
return $this->render('core/index.html.twig', ['events' => $events]);
}
// ...
}
Plutôt basique, on injecte la classe EventRepository
dans la fonction homepage()
puis nous récupérons tous les événements de la base de données avec $eventRepository->findAll()
, cette fonction retourne un tableau d'objets événements que nous envoyons ensuite à la vue, nous pouvons maintenant nous rendre dans la vue parcourir ce tableau et afficher les événements.
Attention: un autre moyen de récupérer le repository de l'entité dans un contrôleur, c'est de faire:
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
$eventRepository = $this->getDoctrine()->getRepository(Event::class);
$events = $eventRepository->findAll();
return $this->render('core/index.html.twig', ['events' => $events]);
}
Personnellement, j'ai une préférence pour l'injection du repository en tant que paramètre du contrôleur.
Maintenant la vue:
{# templates/core/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Accueil |
{{ parent() }}
{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col">
<h2 class="text-center">Événements à venir</h2>
</div>
</div>
<div class="row">
<div class="d-flex flex-row flex-wrap">
{% for event in events %}
<div class="card m-2" style="width: 16rem;">
<img class="card-img-top" src="{{ event.picture }}" alt="{{ event.title }}">
<div class="card-body">
<h5 class="card-title">{{ event.title }}</h5>
<p class="card-text text-muted">
Le
{{ event.eventDate|date('d/m/Y') }}
<br>
{{ event.address }}
</p>
<a href="{{ path('show_event', {'id': event.id}) }}" class="btn btn-primary">Réserver</a>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
Tout ce qui change c'est de la ligne 19 à 33, nous utilisons la boucle for de Twig pour parcourir le tableau events
qui a été envoyé par le contrôleur puis à chaque tour, nous avons l'objet courant dans un objet event
(sans le s
) et nous l'affichons. Pour afficher un attribut nous utilisons le point: {{ event.id }}, {{ event.picture }}, et pour afficher la date, il faut la formatter, nous avons utiliser le filtre date ici, nous allons le modifier dans un instant. Pour l'instant, allons voir ce que nous avons sur la page d'accueil https://localhost:8000:
Nous avons les 2 événements, le premier s'affiche bien, sans problème. Le deuxième événement ne devrait pas être là, on est d'accord? Il n'est pas encore publique. En même temps, dans le contrôleur nous avons utilisé findAll()
pour aller chercher tout les événement de la base de données, mais ça c'était avant, maintenant voilà ce que le client il veut:
Sur la page d'accueil, je veux juste afficher les derniers 12 événements publiés, afficher du plus proche au plus loin, l'événement de demain va s'afficher avant celui qui va se passer dans une semaine.
Bon je pense que le client il a vraiment été clair maintenant, qu'est-ce qu'on fait donc? On crée notre propre méthode? Pas maintenant, nous allons utiliser une autre méthode fournit par Symfony, on va donc modifier la fonction homepage()
comme ceci:
/**
* @Route("/", name="homepage")
*/
public function homepage(EventRepository $eventRepository): Response
{
$events = $eventRepository->findBy(
['isPublished' => true],
['eventDate' => 'ASC'],
12,
0
);
return $this->render('core/index.html.twig', ['events' => $events]);
}
Nous utilisons la méthode findBy()
qui prend quatres paramètres:
- le premier tableau est celui de la condition, c'est la clause
WHERE
d'une requête SQL, dans notre cas nous voulons juste les événements qui ont le champisPublished
qui est égal àtrue
- le second tableau c'est le
ORDER BY
d'une requête SQL, nous souhaitons que les événements soient organisés par date de passage du plus petit (proche) au plus grand (loin) - le troisième paramètre défini le nombre d'éléments à récupérer, c'est la clause
LIMIT
d'une requête SQL, nous voulons juste 12 événements - le dernier paramètre défini l'indice à partir duquel on souhaite lire, c'est la clause
OFFSET
d'une requête SQL, nous voulons les 12 derniers événements, donc on commence à partir de 0
Si tu retournes maintenant sur la page d'accueil:
Bon on ne sait pas si ça a vraiment marcher ou pas, vu que nous avons juste un événement. Pour pallier à cela, nous allons ajouter au moins quatres événements publiques. On peut soit lancer MySQL et ajouter nous même ces données ou utiliser des fixtures.
Définir des fixtures
Les fixtures sont des données de tests que nous ajoutons à notre base de données. Pour pouvoir les définir, il nous faut installer un nouveau composant orm-fixtures
, avec composer
cela va donner:
$ symfony composer require orm-fixtures --dev
On aura besoin de cette dépendance que quand on est en développement.
Une fois l'installation terminer, un nouveau fichier AppFixtures.php
sera créé dans le dossier src/DataFixtures/
, voici le contenu du fichier:
<?php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
// $product = new Product();
// $manager->persist($product);
$manager->flush();
}
}
Nous allons définir les objets que nous voulons ajouter dans la base de données au niveau de la fonction load()
, et la dernière ligne de la fonction $manager->flush()
va enregistrer tous les objets que nous aurons persister.
<?php
namespace App\DataFixtures;
use App\Entity\Event;
use App\Entity\Tag;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
$webTag = new Tag();
$webTag->setLabel('web');
$codeTag = new Tag();
$codeTag->setLabel('code');
$apiTag = new Tag();
$apiTag->setLabel('api');
$designTag = new Tag();
$designTag->setLabel('api');
$event1 = new Event();
$event1->setPicture('https://images.pexels.com/photos/251225/pexels-photo-251225.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260');
$event1->setTitle('À la découverte du développement web');
$event1->setAddress('Sacré Coeur 3 VDN, Dakar');
$event1->setDescription('Lorem ipsum dolor sit amet consectetur
adipisicing, elit. Libero tenetur beatae repellendus possimus magni
quae! Impedit soluta sit iusto amet unde repudiandae fugit
perspiciatis, deleniti quod placeat.');
// la date de l'événement c'est dans 14 jours à 10h30
$event1->setEventDate((new \DateTime('+14 days'))->setTime(10, 30));
$event1->setIsPublished(true);
$event1->setPublishedAt(new \DateTimeImmutable());
$event1->addTag($webTag);
$event1->addTag($codeTag);
$manager->persist($event1);
$event2 = new Event();
$event2->setTitle('API REST - Best Practices');
$event2->setPicture('https://images.pexels.com/photos/3861943/pexels-photo-3861943.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260');
$event2->setAddress('Impact Hub Dakar');
$event2->setDescription('Lorem ipsum dolor sit amet consectetur
adipisicing, elit. Libero tenetur beatae repellendus possimus magni
quae! Impedit soluta sit iusto amet unde repudiandae fugit
perspiciatis, deleniti quod placeat.');
// la date de l'événement c'est dans 10 jours à 10h00
$event2->setEventDate((new \DateTime('+10 days'))->setTime(10, 0));
$event2->setIsPublished(true);
$event2->setPublishedAt(new \DateTimeImmutable());
$event2->addTag($webTag);
$event2->addTag($codeTag);
$event2->addTag($apiTag);
$manager->persist($event2);
$event3 = new Event();
$event3->setTitle('Introduction au UX/UI Design');
$event3->setPicture('https://images.pexels.com/photos/196644/pexels-photo-196644.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260');
$event3->setAddress('Toogueda - Lambanyi, Conakry');
$event3->setDescription('Lorem ipsum dolor sit amet consectetur
adipisicing, elit. Libero tenetur beatae repellendus possimus magni
quae! Impedit soluta sit iusto amet unde repudiandae fugit
perspiciatis, deleniti quod placeat.');
// la date de l'événement c'est dans 14 jours à 16h00
$event3->setEventDate((new \DateTime('+14 days'))->setTime(16, 0));
$event3->setIsPublished(true);
$event3->setPublishedAt(new \DateTimeImmutable());
$event3->addTag($designTag);
$manager->persist($event3);
$event4 = new Event();
$event4->setTitle('Symfony + API Platform pour vos API REST');
$event4->setPicture('https://images.pexels.com/photos/270348/pexels-photo-270348.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260');
$event4->setAddress('Jokkolabs Dakar');
$event4->setDescription('Lorem ipsum dolor sit amet consectetur
adipisicing, elit. Libero tenetur beatae repellendus possimus magni
quae! Impedit soluta sit iusto amet unde repudiandae fugit
perspiciatis, deleniti quod placeat.');
// la date de l'événement c'est dans 5 jours à 10h00
$event4->setEventDate((new \DateTime('+5 days'))->setTime(10, 0));
$event4->setIsPublished(true);
$event4->setPublishedAt(new \DateTimeImmutable());
$event4->addTag($apiTag);
$event4->addTag($webTag);
$manager->persist($event4);
$event5 = new Event();
$event5->setTitle('Événement à venir, pas encore publique');
$manager->flush();
}
}
Nous ajoutons cinq événements dont quatres sont publics, avec différentes dates et tags.
Nous allons maintenant exécuter les fixtures pour que ces données soient ajouter dans la base de données, pour cela il faut exécuter la commande:
$ symfony console doctrine:fixtures:load
Quand tu exécutes la commande, la console te notifie que la base de données sera purger (totalement vider de son contenu), c'est pour cela qu'il ne faut jamais utiliser cette commande en production. Saisi donc yes
puis Entrer
. Tu peux ouvrir ton gestionnaire de base de données et voir les données qui ont été ajouter, ou tout simplement aller sur la page d'accueil https://localhost:8000/:
On a bien les quatres événements publiques qui s'affichent du plus proche au moins proche, exactement ce que le client il veut.
Bon j'avoue que les boutons en escalier ça fait un peu mal aux yeux, mais on est pas là pour nous battre avec du CSS et quelques pixels et peut être que l'événement sur l'UI Design va nous aider avec cela.
Revenons plutôt sur la façon d'afficher la date, pour l'instant nous avons un affichage basique, mais le client veut que la date s'affiche comme ceci: Le 13 sept. 2021 à 10:30 par exemple. Et en tant que développeur, on fait ce que le client il veut. Twig dispose d'un filtre format_datetime que nous allons utiliser pour cela, pour l'utiliser, il faut d'abord installer le composant twig/intl-extra
:
$ symfony composer require twig/intl-extra
Et aussi modifier l'affichage de la date dans la vue:
{# templates/core/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Accueil |
{{ parent() }}
{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col">
<h2 class="text-center">Événements à venir</h2>
</div>
</div>
<div class="row">
<div class="d-flex flex-row flex-wrap">
{% for event in events %}
<div class="card m-2" style="width: 16rem;">
<img class="card-img-top" src="{{ event.picture }}" alt="{{ event.title }}">
<div class="card-body">
<h5 class="card-title">{{ event.title }}</h5>
<p class="card-text text-muted">
Le
{{ event.eventDate|format_datetime() }}
<br>
{{ event.address }}
</p>
<a href="{{ path('show_event', {'id': event.id}) }}" class="btn btn-primary">Réserver</a>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
Je modifie juste la ligne 26 pour utiliser {{ event.eventDate|format_datetime() }}
et cela donne:
OK, nous avons la date en anglais et même si on aimerait dans le futur avoir un public anglais, pour l'instant le client cible un public français. Sur la documentation, il est dit que nous pouvons définir le local comme ceci:
{{ event.eventDate|format_datetime(locale='fr') }}
Et voilà!!!
Ceci n'est pas la meilleur manière pour traduire les dates, le mieux c'est d'avoir le formatage en fonction de la langue de notre application et nous reviendrons sur cela dans un tutoriel spécifique sur l'internationalisation (i18n
).
Pour l'instant nous avons à peu près ce que nous voulons, il reste à gérer l'heure, nous ne voulons pas afficher les seconds:
{{ event.eventDate|format_datetime('medium', 'short', locale='fr') }}
Et nous avons exactement ce que nous voulons.
Affichage d'un objet
Jusque là, notre page d'accueil marche bien et affiche la liste des prochains événements. Mais quand on clique sur un événement pour l'afficher, nous n'avons pas les bonnes infos de l'événement qui s'affichent, nous allons donc remédier à cela.
La fonction show($id)
dans la classe EventController
est le contrôleur qui se charge d'afficher un événement, pour l'instant la fonction se présente comme ceci:
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show($id): Response
{
return $this->render('event/show.html.twig', ['event_id' => $id]);
}
La fonction prend en paramètre l'id de l'événement à afficher, nous devons donc récupérer l'événement avec l'id $id
et l'envoyer à la vue:
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show($id, EventRepository $eventRepository): Response
{
$event = $eventRepository->find($id);
return $this->render('event/show.html.twig', ['event' => $event]);
}
Nous définissons la classe EventRepository
comme paramètre de la fonction et nous cherchons l'événement avec la méthode find()
du repository. Cette méthode sert à récupérer un objet par son id
seulement. Nous allons ensuite modifier la vue pour afficher les bonnes infos de l'événement:
{# templates/event/show.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}
{{ event.title }}
|
{{ parent() }}
{% endblock %}
{% block content %}
<div class="hero p-4">
<div class="container">
<div class="row">
<div class="col-sm-12 col-lg-6">
<img src="{{ event.picture }}" alt="" class="img-fluid">
</div>
<div class="col-sm-12 col-lg-6 mt-3">
<h2>{{ event.title }}</h2>
<p class="card-text text-muted">
Le
{{ event.eventDate|format_datetime('medium', 'short', locale='fr') }}
<br>
{{ event.address }}
</p>
<p class="lead">
{{ event.description }}
</p>
<a href="#" class="btn btn-primary">Réserver</a>
</div>
</div>
</div>
</div>
{% endblock %}
Et voilà, si tu cliques sur un événement maintenant, tu as bien la page de l'événement avec ces détails.
Mais nous avons un problème que nous n'avons pas gérer, pour l'instant nous partons du principe que l'utilisateur va juste cliquer sur un événement pour l'afficher, qu'est-ce qu'il se passe s'il décide de saisir lui même l'URL et qu'il saisisse un id
qui n'existe pas? Disons https://localhost:8000/events/10000 par exemple pour l'événement qui a l'id 10000, eh bien essaie pour voir:
On a une erreur qui dit qu'il est impossible d'accéder à l'attribut title
d'une variable qui est null
. Il faut savoir que la méthode find()
retourne null
si l'objet n'existe pas et dans notre cas nous prenons cet objet qui est null
puis nous l'envoyons à la vue, ce qui cause cette erreur.
Pour éviter cela, nous allons vérifier que l'objet $event
que nous récupérons de la base de données n'est pas null
, si l'objet est nul, on retourne une erreur 404
pour dire que l'objet n'existe pas, sinon on retourne la page de détail de l'événement:
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show($id, EventRepository $eventRepository): Response
{
$event = $eventRepository->find($id);
if (!$event) {
throw $this->createNotFoundException(
"L'événement avec l'id = {$id} n'existe pas!"
);
}
return $this->render('event/show.html.twig', ['event' => $event]);
}
Si tu retournes sur la page https://localhost:8000/events/10000 tu dois maintenant avoir une page d'erreur avec le statut 404
:
Bon c'est vrai que cette page ressemble beaucoup à celle que nous avions avant, mais si tu regardes bien, le statut de la réponse est 404
cette fois-ci et c'est ce qui compte vraiment pour ce genre d'erreur. Puis quand nous serons en production tu ne verras plus cette page, mais la page d'erreur que tu auras créer toi même.
Mais je trouve la fonction un peu verbeuse, laisse moi te présenter une autre manière de récupérer automatiquement l'événement ou de lever une exception NotFoundException
si l'objet n'existe pas.
La magie du ParamConverter
Le ParamConverter
va nous permettre de rendre la fonction show()
moins verbeuse. Le ParamConverter
va se charger d'aller chercher l'objet en base de données et si l'objet existe, continuer l'exécution du contrôleur, sinon retourner une erreur 404 et arrêter l'exécution. Et son utilisation est tellement simple:
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show(Event $event): Response
{
return $this->render('event/show.html.twig', ['event' => $event]);
}
Nous modifions juste le type du paramètre et nous le définissons comme un objet de l'entité Event
vu que nous voulons chercher dans la table event
, si on voulait chercher dans la table tag
on aurait utiliser l'entité Tag
. Le nom du paramètre ne veut rien dire et n'a donc aucune signification dans ce cas-ci. L'autre point à respecter c'est le nom du paramètre de la requête /events/{id}
donc id
dans notre cas ici, le ParamConverter
va chercher l'objet en utilisant ce champ, ce sera donc un WHERE id = :valeur
, tu peux par exemple modifier le id
pour passer par un autre champ, mais il faudra que ce champ soit un attribut de l'entité, sinon il faudra toi même spécifier la relation. Je te propose d'aller lire la documentation pour mieux comprendre la puissance du ParamConverter
.
Modifier un objet
Maintenant que nous avons quelques objets en base de données, nous allons voir comment les modifier.
Modifier un objet ressemble beaucoup à la création d'un nouvel objet, la seule différence dans le cas de la modification c'est que nous n'allons pas persister l'objet, parce que le gestionnaire d'objets connaît déjà l'existence de l'objet. Je vais te présenter ici juste un code pour te montrer l'exemple, mais nous verrons vraiment tout cela en pratique quand nous parlerons des formulaires. Disons que nous voulons modifier l'objet qui à l'id 1:
<?php // src/Controller/EventController.php
namespace App\Controller;
// ...
class EventController extends AbstractController
{
// ...
/**
* @Route("/events/{id}/update", name="update_event")
*/
public function update(
Event $event,
EntityManagerInterface $entityManager
): Response {
// grâce au ParamConverter, nous avons automatiquement accès à l'objet $event
$event->setTitle("À la découverte du Web 2.0");
$event->setEventDate((new \DateTime('+14 days'))->setTime(15, 30));
$entityManager->flush();
return new Response("L'événement à bien été modifier.");
}
}
Nous modifions le titre et la date de l'événement, puis cette fois-ci nous ne persistons pas l'objet, parce que le gestionnaire d'entités connaît déjà l'existence de cet objet, il est lui même aller le chercher en base de données. Nous appelons directement la méthode flush()
pour porter les modifications en base de données.
Si tu pars maintenant sur l'adresse https://localhost:8000/events/1/update, l'événement avec l'id égal à 1 sera modifier:
Supprimer un objet
Pour supprimer un objet, il faut aussi utiliser le gestionnaire d'entités avec sa méthode remove()
:
<?php // src/Controller/EventController.php
namespace App\Controller;
// ...
class EventController extends AbstractController
{
// ...
/**
* @Route("/events/{id}/delete", name="delete_event")
*/
public function delete(
Event $event,
EntityManagerInterface $entityManager
): Response {
// grâce au ParamConverter, nous avons automatiquement accès à l'objet $event
$entityManager->remove($event); // on utilise la method remove de l'entity manager
$entityManager->flush();
return new Response("L'événement {$event->getId()} à bien été supprimer.");
}
}
Et voilà, c'est aussi simple que cela. Pour supprimer l'événement avec l'id égal à 5, il faut aller à l'adresse https://localhost:8000/events/5/delete.
Voilà, nous avons vu comment créer, lire, modifier et supprimer des objets avec Doctrine. Dans la prochaine section nous allons passer à un niveau un peu plus supérieur, nous allons parler des formulaire et commencer à créer les formulaires pour créer des événements. D'ici là, continue de pratiquer ce que nous avons appris jusque là, si tu as des questions, n'hésite pas à laisser un commentaire ci-dessous ou à te rendre sur discord pour en discuter. À plus tard, prends soin de toi!
Participe à la discussion