• 30 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

Ce cours existe en livre papier.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 04/09/2017

TP : Consolidation de notre code

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 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 Symfony2 au cours du TP.

Surtout, je vous invite à bien essayer de 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 Symfony2, 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
// src/OC/PlatformBundle/Entity/Advert.php

namespace OC\PlatformBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\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, unique=true)
   */
  private $title;

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

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

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

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

  /**
   * @ORM\ManyToMany(targetEntity="OC\PlatformBundle\Entity\Category", cascade={"persist"})
   */
  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(length=128, unique=true)
   */
  private $slug;

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

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

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

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

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

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

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

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

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

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

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

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

  /**
   * @param Image $image
   * @return Advert
   */
  public function setImage(Image $image = null)
  {
    $this->image = $image;
    return $this;
  }

  /**
   * @return Image
   */
  public function getImage()
  {
    return $this->image;
  }

  public function addCategory(Category $category)
  {
    $this->categories[] = $category;
    return $this;
  }

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

  public function getCategories()
  {
    return $this->categories;
  }

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

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

    return $this;
  }

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

    // Et si notre relation était facultative (nullable=true, ce qui n'est pas notre cas ici attention) :
    // $application->setAdvert(null);
  }

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

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

  public function setUpdatedAt(\Datetime $updatedAt)
  {
    $this->updatedAt = $updatedAt;
    return $this;
  }

  public function getUpdatedAt()
  {
    return $this->updatedAt;
  }

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

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

EntitéImage

L'entitéImageest une entité très simple, qui nous servira par la suite pour l'upload d'images avec Symfony2. 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

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\ImageRepository")
 */
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, bien que très simple, 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\Entity(repositoryClass="OC\PlatformBundle\Entity\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();
  }

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

  public function setAuthor($author)
  {
    $this->author = $author;
    return $this;
  }

  public function getAuthor()
  {
    return $this->author;
  }

  public function setContent($content)
  {
    $this->content = $content;
    return $this;
  }

  public function getContent()
  {
    return $this->content;
  }

  public function setDate($date)
  {
    $this->date = $date;

    return $this;
  }

  public function getDate()
  {
    return $this->date;
  }

  public function setAdvert(Advert $advert)
  {
    $this->advert = $advert;

    return $this;
  }

  public function getAdvert()
  {
    return $this->advert;
  }

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

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

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(repositoryClass="OC\PlatformBundle\Entity\AdvertSkillRepository")
 */
class AdvertSkill
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @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;

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

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

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

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

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

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

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

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

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 avecphp app/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éfinis à la partie précédentes pour nos tests.

Pour cela, il y a très peu de modifications à réaliser : voici encore un exemple du code découplé que Symfony2 nous permet de réaliser ! En effet, il vous suffit de modifier les quatre endroits où on avait écrit une annonce en dur dans le contrôleur. Modifiez ces quatre 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 Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

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

    // 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)
  {
    // On récupère l'EntityManager
    $em = $this->getDoctrine()->getManager();

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

    // On vérifie que l'annonce avec cet id existe bien
    if ($advert === null) {
      throw $this->createNotFoundException("L'annonce d'id ".$id." n'existe pas.");
    }

    // On récupère la liste des advertSkill pour l'annonce $advert
    $listAdvertSkills = $em->getRepository('OCPlatformBundle:AdvertSkill')->findByAdvert($advert);

    // Puis modifiez la ligne du render comme ceci, pour prendre en compte les variables :
    return $this->render('OCPlatformBundle:Advert:view.html.twig', array(
      'advert'           => $advert,
      'listAdvertSkills' => $listAdvertSkills,
    ));
  }

  public function addAction(Request $request)
  {
    // La gestion d'un formulaire est particulière, mais l'idée est la suivante :

    if ($request->isMethod('POST')) {
      // Ici, on s'occupera de la création et de la gestion du formulaire

      $request->getSession()->getFlashBag()->add('info', 'Annonce bien enregistrée.');

      // Puis on redirige vers la page de visualisation de cet article
      return $this->redirect($this->generateUrl('oc_platform_view', array('id' => 1)));
    }

    // Si on n'est pas en POST, alors on affiche le formulaire
    return $this->render('OCPlatformBundle:Advert:add.html.twig');
  }

  public function editAction($id)
  {
    // On récupère l'EntityManager
    $em = $this->getDoctrine()->getManager();

    // On récupère l'entité correspondant à l'id $id
    $advert = $em->getRepository('OCPlatformBundle:Advert')->find($id);

    // Si l'annonce n'existe pas, on affiche une erreur 404
    if ($advert == null) {
      throw $this->createNotFoundException("L'annonce d'id ".$id." n'existe pas.");
    }

    // Ici, on s'occupera de la création et de la gestion du formulaire

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

  public function deleteAction($id, Request $request)
  {
    // On récupère l'EntityManager
    $em = $this->getDoctrine()->getManager();

    // On récupère l'entité correspondant à l'id $id
    $advert = $em->getRepository('OCPlatformBundle:Advert')->find($id);

    // Si l'annonce n'existe pas, on affiche une erreur 404
    if ($advert == null) {
      throw $this->createNotFoundException("L'annonce d'id ".$id." n'existe pas.");
    }

    if ($request->isMethod('POST')) {
      // Si la requête est en POST, on deletea l'article

      $request->getSession()->getFlashBag()->add('info', 'Annonce bien supprimée.');

      // Puis on redirige vers l'accueil
      return $this->redirect($this->generateUrl('oc_platform_home'));
    }

    // Si la requête est en GET, on affiche une page de confirmation avant de delete
    return $this->render('OCPlatformBundle:Advert:delete.html.twig', array(
      'advert' => $advert
    ));
  }

  public function menuAction($limit = 3)
  {
    $listAdverts = $this->getDoctrine()
      ->getManager()
      ->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 Doctrine2, 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->getResult() qu'on a l'habitude d'utiliser.

Regarder 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 objet Paginator, 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 Doctrine2 généreux en fonctionnalités, fait tout le travail.

Enfin, il vous restait la vue à adapter. Voici ce que je peux vous proposer, j'ai juste rajouté l'affichage de la liste des pages possibles :

{# 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 Symfony2, 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 !

Exemple de certificat de réussite
Exemple de certificat de réussite