Kaherecode

Créer une application web avec Symfony - Doctrine

Mamadou Aliou Diallo
@alioukahere 16 oct. 2021
0

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:

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:

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:

https://imgur.com/eUQyI7S.jpg

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:

https://imgur.com/gytOUS0.jpg

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!!!

https://imgur.com/FYWnEu0.jpg

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:

https://imgur.com/SGt0RBh.jpg

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:

Si tu retournes maintenant sur la page d'accueil:

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

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

https://imgur.com/Xlwxx3P.jpg

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/:

https://imgur.com/Asp7Jkx.jpg

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:

https://imgur.com/qwy2lTE.jpg

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à!!!

https://imgur.com/hkQrRTV.jpg

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') }}

https://imgur.com/z6ZOxGW.jpg

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:

https://imgur.com/5onIKy8.jpg

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:

https://imgur.com/u4if3vc.jpg

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:

https://imgur.com/nBGv7lh.jpg

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!


Partage ce tutoriel


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

Discussion

Tu dois être connecté pour participer à la discussion. Me connecter.