• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/13/19

TP : Consolidation de notre code

Log in or subscribe for free to enjoy all this course has to offer!

L'objectif de ce chapitre est de mettre en application tout ce que nous avons vu au cours de cette partie sur Doctrine2. Nous avons déjà créé les entitésAdvert etApplication, mais il nous faut également adapter le contrôleur pour nous en servir. Enfin, nous verrons quelques astuces de développement Symfony au cours du TP.

Surtout, je vous invite à bien réfléchir par vous-mêmes avant de lire les codes que je donne. C'est ce mécanisme de recherche qui va vous faire progresser sur Symfony, il serait dommage de s'en passer !

Bon TP !

Synthèse des entités

Pour être sûr de partir sur les mêmes bases, je vous remets ici le code complet de toutes nos entités. Nous les avons déjà construites au cours de cette partie, mais cela vous permet de vérifier que vous avez le bon code pour chacune d'entre elles.

EntitéAdvert

On a déjà pas mal traité l'entitéAdvert au cours de cette partie. Pour l'instant, on a toujours le nom de l'auteur écrit en dur dans l'entité. Souvenez-vous, pour commencer, on n'a pas d'entitéUser (elle sera créée durant la prochaine partie) on doit donc écrire le nom de l'auteur en dur dans les annonces.

Voici donc la version finale de l'entitéAdvert que vous devriez avoir :

<?php

namespace OC\PlatformBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
// N'oubliez pas ce use :
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Table(name="oc_advert")
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Repository\AdvertRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Advert
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="date", type="datetime")
   */
  private $date;

  /**
   * @ORM\Column(name="title", type="string", length=255)
   */
  private $title;

  /**
   * @ORM\Column(name="author", type="string", length=255)
   */
  private $author;

  /**
   * @ORM\Column(name="content", type="string", length=255)
   */
  private $content;

  /**
   * @ORM\Column(name="published", type="boolean")
   */
  private $published = true;

  /**
   * @ORM\OneToOne(targetEntity="OC\PlatformBundle\Entity\Image", cascade={"persist"})
   */
  private $image;

  /**
   * @ORM\ManyToMany(targetEntity="OC\PlatformBundle\Entity\Category", cascade={"persist"})
   * @ORM\JoinTable(name="oc_advert_category")
   */
  private $categories;

  /**
   * @ORM\OneToMany(targetEntity="OC\PlatformBundle\Entity\Application", mappedBy="advert")
   */
  private $applications; // Notez le « s », une annonce est liée à plusieurs candidatures

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

  /**
   * @ORM\Column(name="nb_applications", type="integer")
   */
  private $nbApplications = 0;

  /**
   * @Gedmo\Slug(fields={"title"})
   * @ORM\Column(name="slug", type="string", length=255, unique=true)
   */
  private $slug;

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

  /**
   * @ORM\PreUpdate
   */
  public function updateDate()
  {
    $this->setUpdatedAt(new \Datetime());
  }

  public function increaseApplication()
  {
    $this->nbApplications++;
  }

  public function decreaseApplication()
  {
    $this->nbApplications--;
  }

  /**
   * @return int
   */
  public function getId()
  {
    return $this->id;
  }

  /**
   * @param \DateTime $date
   */
  public function setDate($date)
  {
    $this->date = $date;
  }

  /**
   * @return \DateTime
   */
  public function getDate()
  {
    return $this->date;
  }

  /**
   * @param string $title
   */
  public function setTitle($title)
  {
    $this->title = $title;
  }

  /**
   * @return string
   */
  public function getTitle()
  {
    return $this->title;
  }

  /**
   * @param string $author
   */
  public function setAuthor($author)
  {
    $this->author = $author;
  }

  /**
   * @return string
   */
  public function getAuthor()
  {
    return $this->author;
  }

  /**
   * @param string $content
   */
  public function setContent($content)
  {
    $this->content = $content;
  }

  /**
   * @return string
   */
  public function getContent()
  {
    return $this->content;
  }

  /**
   * @param bool $published
   */
  public function setPublished($published)
  {
    $this->published = $published;
  }

  /**
   * @return bool
   */
  public function getPublished()
  {
    return $this->published;
  }

  public function setImage(Image $image = null)
  {
    $this->image = $image;
  }

  public function getImage()
  {
    return $this->image;
  }

  /**
   * @param Category $category
   */
  public function addCategory(Category $category)
  {
    $this->categories[] = $category;
  }

  /**
   * @param Category $category
   */
  public function removeCategory(Category $category)
  {
    $this->categories->removeElement($category);
  }

  /**
   * @return ArrayCollection
   */
  public function getCategories()
  {
    return $this->categories;
  }

  /**
   * @param Application $application
   */
  public function addApplication(Application $application)
  {
    $this->applications[] = $application;

    // On lie l'annonce à la candidature
    $application->setAdvert($this);
  }

  /**
   * @param Application $application
   */
  public function removeApplication(Application $application)
  {
    $this->applications->removeElement($application);
  }

  /**
   * @return \Doctrine\Common\Collections\Collection
   */
  public function getApplications()
  {
    return $this->applications;
  }

  /**
   * @param \DateTime $updatedAt
   */
  public function setUpdatedAt(\Datetime $updatedAt = null)
  {
      $this->updatedAt = $updatedAt;
  }

  /**
   * @return \DateTime
   */
  public function getUpdatedAt()
  {
      return $this->updatedAt;
  }

  /**
   * @param integer $nbApplications
   */
  public function setNbApplications($nbApplications)
  {
      $this->nbApplications = $nbApplications;
  }

  /**
   * @return integer
   */
  public function getNbApplications()
  {
      return $this->nbApplications;
  }

  /**
   * @param string $slug
   */
  public function setSlug($slug)
  {
      $this->slug = $slug;
  }

  /**
   * @return string
   */
  public function getSlug()
  {
      return $this->slug;
  }
}

EntitéImage

L'entitéImageest une entité très simple, qui nous servira par la suite pour l'upload d'images avec Symfony. Sa particularité est qu'elle peut être liée à n'importe quelle autre entité : elle n'est pas du tout exclusive à l'entitéAdvert. Si vous souhaitez ajouter des images ailleurs que dans desAdvert, il n'y aura aucun problème.

Voici son code, que vous devriez déjà avoir :

<?php
// src/OC/PlatformBundle/Entity/Image.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oc_image")
 * @ORM\Entity
 */
class Image
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="url", type="string", length=255)
   */
  private $url;

  /**
   * @ORM\Column(name="alt", type="string", length=255)
   */
  private $alt;
  
  // Getters et setters
}

EntitéApplication

L'entitéApplication contient la relation avec l'entitéAdvert, c'est elle la propriétaire. Voici son code :

<?php
// src/OC/PlatformBundle/Entity/Application.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oc_application")
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Repository\ApplicationRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Application
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="author", type="string", length=255)
   */
  private $author;

  /**
   * @ORM\Column(name="content", type="text")
   */
  private $content;

  /**
   * @ORM\Column(name="date", type="datetime")
   */
  private $date;

  /**
   * @ORM\ManyToOne(targetEntity="OC\PlatformBundle\Entity\Advert", inversedBy="applications")
   * @ORM\JoinColumn(nullable=false)
   */
  private $advert;

  public function __construct()
  {
    $this->date = new \Datetime();
  }

  /**
   * @ORM\PrePersist
   */
  public function increase()
  {
    $this->getAdvert()->increaseApplication();
  }

  /**
   * @ORM\PreRemove
   */
  public function decrease()
  {
    $this->getAdvert()->decreaseApplication();
  }

  /**
   * @return int
   */
  public function getId()
  {
    return $this->id;
  }

  /**
   * @param string $author
   */
  public function setAuthor($author)
  {
    $this->author = $author;
  }

  /**
   * @return string
   */
  public function getAuthor()
  {
    return $this->author;
  }

  /**
   * @param string $content
   */
  public function setContent($content)
  {
    $this->content = $content;
  }

  /**
   * @return string
   */
  public function getContent()
  {
    return $this->content;
  }

  /**
   * @param \Datetime $date
   */
  public function setDate(\Datetime $date)
  {
    $this->date = $date;
  }

  /**
   * @return \Datetime
   */
  public function getDate()
  {
    return $this->date;
  }

  /**
   * @param Advert $advert
   */
  public function setAdvert(Advert $advert)
  {
    $this->advert = $advert;
  }

  /**
   * @return Advert
   */
  public function getAdvert()
  {
    return $this->advert;
  }
}

EntitéCategory

L'entitéCategory ne contient qu'un attributnom(enfin, vous pouvez en rajouter de votre côté bien sûr !). La relation avecAdvert est contenue dans l'entitéAdvert, qui en est la propriétaire. Voici son code, que vous devriez déjà avoir :

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

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Category
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="name", type="string", length=255)
   */
  private $name;

  // Getters et setters
}

EntitésSkill etAdvertSkill

L'entitéSkill ne contient, au même titre que l'entitéCategory, qu'un attributnom, mais vous pouvez bien sûr en rajouter selon vos besoins. Voici son code :

<?php
// src/OC/PlatformBundle/Entity/Skill.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\SkillRepository")
 */
class Skill
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="name", type="string", length=255)
   */
  private $name;
  
  // Getters et setters
}

L'entitéAdvertSkill est l'entité de relation entreAdvert etSkill. Elle contient les attributs$advert et$skill qui permettent de faire la relation, ainsi que d'autres attributs pour caractériser la relation, ici j'ai utilisé un attribut$level. Voici son code, vous pouvez bien entendu rajouter les attributs de relation que vous souhaitez :

<?php
// src/OC/PlatformBundle/Entity/AdvertSkill.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="oc_advert_skill")
 */
class AdvertSkill
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="level", type="string", length=255)
   */
  private $level;

  /**
   * @ORM\ManyToOne(targetEntity="OC\PlatformBundle\Entity\Advert")
   * @ORM\JoinColumn(nullable=false)
   */
  private $advert;

  /**
   * @ORM\ManyToOne(targetEntity="OC\PlatformBundle\Entity\Skill")
   * @ORM\JoinColumn(nullable=false)
   */
  private $skill;

  // ... vous pouvez ajouter d'autres attributs bien sûr

  /**
   * @return integer
   */
  public function getId()
  {
    return $this->id;
  }

  /**
   * @param string $level
   */
  public function setLevel($level)
  {
    $this->level = $level;
  }

  /**
   * @return string
   */
  public function getLevel()
  {
    return $this->level;
  }

  /**
   * @param Advert $advert
   */
  public function setAdvert(Advert $advert)
  {
    $this->advert = $advert;
  }

  /**
   * @return Advert
   */
  public function getAdvert()
  {
    return $this->advert;
  }

  /**
   * @param Skill $skill
   */
  public function setSkill(Skill $skill)
  {
    $this->skill = $skill;
  }

  /**
   * @return Skill
   */
  public function getSkill()
  {
    return $this->skill;
  }
}

Et bien sûr…

Si vous avez ajouté et/ou modifié des entités, n'oubliez pas de mettre à jour votre base de données ! Vérifiez les requêtes avec php bin/console doctrine:schema:update --dump-sql, puis exécutez-les avec --force.

Adaptation du contrôleur

Théorie

Maintenant que l'on a nos entités, on va enfin pouvoir adapter notre contrôleurAdvert pour qu'il récupère et modifie des vraies annonces dans la base de données, et non plus nos annonces statiques définies à la partie précédentes pour nos tests. On l'a déjà fait pour quelques actions, mais il en reste, et c'est notre travail pour ce chapitre.

Pour cela, il y a très peu de modifications à réaliser : voici encore un exemple du code découplé que Symfony nous permet de réaliser ! En effet, il vous suffit de modifier les quelques endroits où on avait écrit une annonce en dur dans le contrôleur. Modifiez ces endroits en utilisant bien le repository de l'entitéAdvert pour récupérer l'annonce, seules les méthodesfindAll()etfind()vont nous servir pour le moment.

Attention, je vous demande également de faire attention au cas où l'annonce demandée n'existe pas. Si on essaie d'aller à la page/platform/advert/4alors que l'annonce d'id 4 n'existe pas, je veux une erreur correctement gérée ! ;) On a déjà vu le déclenchement d'une erreur 404 lorsque le paramètrepagede la page d'accueil n'était pas valide, reprenez ce comportement.

À la fin, le contrôleur ne sera pas entièrement opérationnel, car il nous manque toujours la gestion des formulaires. Mais il sera déjà mieux avancé !

Et bien sûr, n'hésitez pas à nettoyer tous les codes de tests qu'on a pu utiliser lors de cette partie pour jouer avec les entités, maintenant on doit avoir un vrai contrôleur qui ne fait que son rôle.

Pratique

Vous avez fini de réécrire le contrôleur ? Bien, passons à la correction.

L'idée ici était juste de transformer les annonces écrites en dur, en des appels de fonctions pour effectivement récupérer les annonces depuis la base de données.

Il y a deux cas :

  • Des fois, on ne voulait récupérer qu'une seule annonce, c'est le cas des pages de modification ou de suppression d'une annonce par exemple. Dans ce cas, c'est la méthode find du repository Advert qu'il fallait utiliser ;

  • Des fois, on voulait récupérer toute une liste d'annonces, c'est le cas des pages d'accueil et du menu par exemple. Dans ce cas, c'est la méthode findAll qu'il fallait utiliser.

Voici le code que j'obtiens :

<?php

// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use OC\PlatformBundle\Entity\Advert;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class AdvertController extends Controller
{
  public function indexAction($page)
  {
    if ($page < 1) {
      throw new NotFoundHttpException('Page "'.$page.'" inexistante.');
    }

    // Pour récupérer la liste de toutes les annonces : on utilise findAll()
    $listAdverts = $this->getDoctrine()
      ->getManager()
      ->getRepository('OCPlatformBundle:Advert')
      ->findAll()
    ;

    // L'appel de la vue ne change pas
    return $this->render('OCPlatformBundle:Advert:index.html.twig', array(
      'listAdverts' => $listAdverts,
    ));
  }

  public function viewAction($id)
  {
    $em = $this->getDoctrine()->getManager();

    // Pour récupérer une seule annonce, on utilise la méthode find($id)
    $advert = $em->getRepository('OCPlatformBundle:Advert')->find($id);

    // $advert est donc une instance de OC\PlatformBundle\Entity\Advert
    // ou null si l'id $id n'existe pas, d'où ce if :
    if (null === $advert) {
      throw new NotFoundHttpException("L'annonce d'id ".$id." n'existe pas.");
    }

    // Récupération de la liste des candidatures de l'annonce
    $listApplications = $em
      ->getRepository('OCPlatformBundle:Application')
      ->findBy(array('advert' => $advert))
    ;

    // Récupération des AdvertSkill de l'annonce
    $listAdvertSkills = $em
      ->getRepository('OCPlatformBundle:AdvertSkill')
      ->findBy(array('advert' => $advert))
    ;

    return $this->render('OCPlatformBundle:Advert:view.html.twig', array(
      'advert'           => $advert,
      'listApplications' => $listApplications,
      'listAdvertSkills' => $listAdvertSkills,
    ));
  }

  public function addAction(Request $request)
  {
    $em = $this->getDoctrine()->getManager();

    // On ne sait toujours pas gérer le formulaire, patience cela vient dans la prochaine partie !

    if ($request->isMethod('POST')) {
      $request->getSession()->getFlashBag()->add('notice', 'Annonce bien enregistrée.');

      return $this->redirectToRoute('oc_platform_view', array('id' => $advert->getId()));
    }

    return $this->render('OCPlatformBundle:Advert:add.html.twig');
  }

  public function editAction($id, Request $request)
  {
    $em = $this->getDoctrine()->getManager();

    $advert = $em->getRepository('OCPlatformBundle:Advert')->find($id);

    if (null === $advert) {
      throw new NotFoundHttpException("L'annonce d'id ".$id." n'existe pas.");
    }

    // Ici encore, il faudra mettre la gestion du formulaire

    if ($request->isMethod('POST')) {
      $request->getSession()->getFlashBag()->add('notice', 'Annonce bien modifiée.');

      return $this->redirectToRoute('oc_platform_view', array('id' => $advert->getId()));
    }

    return $this->render('OCPlatformBundle:Advert:edit.html.twig', array(
      'advert' => $advert
    ));
  }

  public function deleteAction($id)
  {
    $em = $this->getDoctrine()->getManager();

    $advert = $em->getRepository('OCPlatformBundle:Advert')->find($id);

    if (null === $advert) {
      throw new NotFoundHttpException("L'annonce d'id ".$id." n'existe pas.");
    }

    // On boucle sur les catégories de l'annonce pour les supprimer
    foreach ($advert->getCategories() as $category) {
      $advert->removeCategory($category);
    }

    $em->flush();
    
    return $this->render('OCPlatformBundle:Advert:delete.html.twig');
  }

  public function menuAction($limit)
  {
    $em = $this->getDoctrine()->getManager();

    $listAdverts = $em->getRepository('OCPlatformBundle:Advert')->findBy(
      array(),                 // Pas de critère
      array('date' => 'desc'), // On trie par date décroissante
      $limit,                  // On sélectionne $limit annonces
      0                        // À partir du premier
    );

    return $this->render('OCPlatformBundle:Advert:menu.html.twig', array(
      'listAdverts' => $listAdverts
    ));
  }
}

Utilisation des jointures

Actuellement sur la page d'accueil, avec l'actionindexAction(), on ne récupère que les annonces en elles-mêmes. Comme on en a parlé dans les précédents chapitres, cela veut dire que dans la boucle pour afficher les annonces, on ne peut pas utiliser les informations sur les relations (dans notre cas, les attributs$image,$categorieset$applications). Enfin, on peut bien entendu les utiliser via$advert->getImage(), etc., mais dans ce cas, une requête sera générée pour aller récupérée l'image… à chaque itération de la boucle sur les annonces dans la vue !

Ce comportement est bien sûr à proscrire, car le nombre de requêtes SQL va monter en flèche et ce n'est pas bon du tout pour les performances. Il faut donc modifier la requête initiale qui récupère les annonces, pour y rajouter des jointures qui vont récupérer en une seule requête les annonces ainsi que leurs entités jointes.

Tout d'abord, on va créer une méthodegetAdverts()dans le repository de l'entitéAdvert, une version toute simple qui ne fait que récupérer les entités triées par date :

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;

class AdvertRepository extends EntityRepository
{
  public function getAdverts()
  {
    $query = $this->createQueryBuilder('a')
      ->orderBy('a.date', 'DESC')
      ->getQuery()
    ;

    return $query->getResult();
  }
}

Adaptons ensuite le contrôleur pour utiliser cette nouvelle méthode :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

// Dans indexAction, on utilise maintenant getAdverts et non plus findAll :
$listAdverts = $this->getDoctrine()
  ->getManager()
  ->getRepository('OCPlatformBundle:Advert')
  ->getAdverts()
;

Maintenant, il nous faut mettre en place les jointures dans la méthodegetAdverts(), afin de charger toutes les informations sur les annonces et éviter les dizaines de requêtes supplémentaires.

Dans notre exemple, nous allons afficher les données de l'entitéimageet des entitéscategory liées à chaque annonce. Il nous faut donc rajouter les jointures sur ces deux entités. On a déjà vu comment faire ces jointures, essayez donc de reproduire le même comportement dans notre méthode getAdvert de votre côté avant de lire la correction suivante :

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;

class AdvertRepository extends EntityRepository
{
  public function getAdverts()
  {
    $query = $this->createQueryBuilder('a')
      // Jointure sur l'attribut image
      ->leftJoin('a.image', 'i')
      ->addSelect('i')
      // Jointure sur l'attribut categories
      ->leftJoin('a.categories', 'c')
      ->addSelect('c')
      ->orderBy('a.date', 'DESC')
      ->getQuery()
    ;

    return $query->getResult();
  }
}

Comme vous pouvez le voir, les jointures se font simplement en utilisant les attributs existants de l'entité racine, ici l'entitéAdvert. On rajoute donc juste lesleftJoin(), ainsi que lesaddSelect() afin que Doctrine n'oublie pas de sélectionner les données qu'il joint. C'est tout ! Vous pouvez maintenant utiliser un$advert->getImage() dans la boucle de la vueindex.html.twig sans déclencher de nouvelle requête.

Pagination des annonces sur la page d'accueil

Paginer manuellement les résultats d'une requête n'est pas trop compliqué, il faut juste faire un peu de mathématiques à l'aide des variables suivantes :

  • Nombre total d'annonces ;

  • Nombre d'annonces à afficher par page ;

  • La page courante.

Cependant, c'est un comportement assez classique et en bon développeurs que nous sommes, trouvons une méthode plus simple et déjà prête ! Il existe en effet un paginateur intégré dans Doctrine, qui permet de faire tout cela très simplement.

Le paginateur est un objet, auquel on passe notre requête Doctrine, qui va faire tout le nécessaire pour récupérer comme il se faut nos annonces et leurs entités liées. Il vient se substituer au$query->getResults() qu'on a l'habitude d'utiliser.

Regardez comment l'intégrer dans notre méthodegetAdverts(). Faite attention, j'ai ajouté deux arguments à la méthode, car on a besoin de la page actuelle ainsi que du nombre d'annonces par page pour savoir quelles annonces récupérer exactement.

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;

class AdvertRepository extends EntityRepository
{
  public function getAdverts($page, $nbPerPage)
  {
    $query = $this->createQueryBuilder('a')
      ->leftJoin('a.image', 'i')
      ->addSelect('i')
      ->leftJoin('a.categories', 'c')
      ->addSelect('c')
      ->orderBy('a.date', 'DESC')
      ->getQuery()
    ;

    $query
      // On définit l'annonce à partir de laquelle commencer la liste
      ->setFirstResult(($page-1) * $nbPerPage)
      // Ainsi que le nombre d'annonce à afficher sur une page
      ->setMaxResults($nbPerPage)
    ;

    // Enfin, on retourne l'objet Paginator correspondant à la requête construite
    // (n'oubliez pas le use correspondant en début de fichier)
    return new Paginator($query, true);
  }
}

Ce que retourne cet objetPaginator, c'est une liste de$nbPerPage annonces, qui s'utilise comme n'importe quel tableau habituel (vous pouvez faire une boucle dessus notamment).

La seule petite subtilité c'est que si vous faites un count dessus, vous n'obtenez pas $nbPerPage, mais le nombre total d'annonces présentes en base de données. Cette information est indispensable pour calculer le nombre total de pages.

Vous avez maintenant toutes les informations nécessaire pour adapter le contrôleur. L'idée est de correctement utiliser notre méthode getAdverts, et de retourner une erreur 404 si la page demandée n'existe pas. Vous devez également modifier la vue pour afficher une liste de toutes les pages disponible, veillez bien à lui donner toutes les informations nécessaires depuis le contrôleur.

 

Il existe bien entendu différentes manières de le faire, mais voici le code du contrôleur que je vous propose :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class AdvertController extends Controller
{
  public function indexAction($page)
  {
    if ($page < 1) {
      throw $this->createNotFoundException("La page ".$page." n'existe pas.");
    }

    // Ici je fixe le nombre d'annonces par page à 3
    // Mais bien sûr il faudrait utiliser un paramètre, et y accéder via $this->container->getParameter('nb_per_page')
    $nbPerPage = 3;

    // On récupère notre objet Paginator
    $listAdverts = $this->getDoctrine()
      ->getManager()
      ->getRepository('OCPlatformBundle:Advert')
      ->getAdverts($page, $nbPerPage)
    ;

    // On calcule le nombre total de pages grâce au count($listAdverts) qui retourne le nombre total d'annonces
    $nbPages = ceil(count($listAdverts) / $nbPerPage);

    // Si la page n'existe pas, on retourne une 404
    if ($page > $nbPages) {
      throw $this->createNotFoundException("La page ".$page." n'existe pas.");
    }

    // On donne toutes les informations nécessaires à la vue
    return $this->render('OCPlatformBundle:Advert:index.html.twig', array(
      'listAdverts' => $listAdverts,
      'nbPages'     => $nbPages,
      'page'        => $page,
    ));
  }
}

C'est tout ! En effet, rappelez-vous l'architecture MVC, toute la logique de récupération des données est dans la couche Modèle : ici notre repository. Notre contrôleur est donc réduit à son strict minimum ; la couche Modèle, grâce à un Doctrine généreux en fonctionnalités, fait tout le travail.

Enfin, il vous restait l'affichage de la liste des pages possible à ajouter, voici ce que je peux vous proposer :

{# src/OC/PlatformBundle/Resources/views/Advert/index.html.twig #}

{% extends "OCPlatformBundle::layout.html.twig" %}

{# ... #}

<ul class="pagination">
  {# On utilise la fonction range(a, b) qui crée un tableau de valeurs entre a et b #}
  {% for p in range(1, nbPages) %}
    <li{% if p == page %} class="active"{% endif %}>
      <a href="{{ path('oc_platform_home', {'page': p}) }}">{{ p }}</a>
    </li>
  {% endfor %}
</ul>

Ce qui donne le résultat visible à la figure suivante.

Nos annonces et la pagination s'affichent
Nos annonces et la pagination s'affichent

Pour conclure

Et voilà, le premier TP du cours s'achève ici. J'espère que vous avez pu exploiter toutes les connaissances que vous avez pu acquérir jusqu'ici, et qu'il vous a aidé à vous sentir plus à l'aise.

La prochaine partie du cours va vous emmener plus loin avec Symfony, pour connaître tous les détails qui vous permettront de créer votre site Internet de toutes pièces. À bientôt !

En résumé

  • Vous savez maintenant construire vos entités.

  • Vous savez maintenant développer un contrôleur abouti.

  • Vous savez maintenant faire des jointures et plus encore dans vos repositories.

  • Bravo !

  • Le code du cours tel qu'il doit être à ce stade est disponible sur la branche iteration-13 du dépot Github.

Example of certificate of achievement
Example of certificate of achievement