English spoken conference

Symfony 5: The Fast Track

A new book to learn about developing modern Symfony 5 applications.

Support this project

Caution: You are browsing the legacy symfony 1.x part of this website.

Jour 7 : Jouons avec la page Catégorie

1.4 / Propel
Symfony version
1.2
Language ORM

Hier, vous avez pu améliorer vos connaissances dans plusieurs domaines : requêtes avec Propel, jeux de test, routage, débogage et la configuration personnalisée. Et nous avions fini la leçon en vous laissant un petit défi pour débuter ce chapitre.

Nous espèrons que vous avez travaillé sur la page catégorie de Jobeet car le tutoriel d'aujourd'hui aura alors beaucoup plus de valeur pour vous.

Prêt ? Voici une des implémentations possibles.

La route de la catégorie

Pour commencer, nous devons ajouter une route pour utiliser des URL propres. Ajoutez ce code au début du fichier de configuration :

# apps/frontend/config/routing.yml
category:
  url:      /category/:slug
  class:    sfPropelRoute
  param:    { module: category, action: show }
  options:  { model: JobeetCategory, type: object }

tip

A chaque fois que vous commencez à implémenter une nouvelle fonctionnalité, créer une route pour les URL en premier est une bonne méthode de travail. Et c'est obligatoire si jamais vous supprimez les règles de routage par défaut.

Une route peut avoir comme paramètre n'importe quelle colonne de l'objet associé. Il est également possible d'utiliser n'importe quelle autre valeur si un accesseur est défini dans la classe de l'objet. Étant donné que le paramètre slug ne fait référence à aucune colonne dans la table category, nous devons ajouter un accessseur virtuel dans JobeetCategory pour faire fonctionner la route :

// lib/model/JobeetCategory.php
public function getSlug()
{
  return Jobeet::slugify($this->getName());
}

Le lien de la catégorie

A présent, éditez le Template indexSuccess.php du module job et ajoutez le lien vers la page catégorie :

<!-- some HTML code -->
 
        <h1>
          <?php echo link_to($category, 'category', $category) ?>
        </h1>
 
<!-- some HTML code -->
 
      </table>
 
      <?php if (($count = $category->countActiveJobs() - sfConfig::get('app_max_jobs_on_homepage')) > 0): ?>
        <div class="more_jobs">
          and <?php echo link_to($count, 'category', $category) ?>
          more...
        </div>
      <?php endif; ?>
    </div>
  <?php endforeach; ?>
</div>

Nous ajoutons seulement les liens, s'il y a plus de 10 emplois à afficher pour la catégorie actuelle. Le lien contient le nombre d'emplois non affichés. Pour que ce Template fonctionne, nous devons ajouter la méthod countActiveJobs() pour JobeetCategory :

// lib/model/JobeetCategory.php
public function countActiveJobs()
{
  $criteria = new Criteria();
  $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
 
  return JobeetJobPeer::countActiveJobs($criteria);
}

La méthode countActiveJobs() utilise la méthode countActiveJobs() qui n'existe pas encore dans le modèle JobeetJobPeer. Remplacer le contenu du fichier JobeetJobPeer.php par le code suivant :

// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public function getActiveJobs(Criteria $criteria = null)
  {
    return self::doSelect(self::addActiveJobsCriteria($criteria));
  }
 
  static public function countActiveJobs(Criteria $criteria = null)
  {
    return self::doCount(self::addActiveJobsCriteria($criteria));
  }
 
  static public function addActiveJobsCriteria(Criteria $criteria = null)
  {
    if (is_null($criteria))
    {
      $criteria = new Criteria();
    }
 
    $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
    $criteria->addDescendingOrderByColumn(self::CREATED_AT);
 
    return $criteria;
  }
 
  static public function doSelectActive(Criteria $criteria)
  {
    return self::doSelectOne(self::addActiveJobsCriteria($criteria));
  }
}

Comme vous pouvez le constater, nous avons refactorisé tout le code de JobeetJobPeer afin d'utiliser la nouvelle méthode addActiveJobsCriteria() qui est partagée. Notre code utilise maintenant la philosophie DRY (Don't Repeat Yourself).

tip

La première fois qu'une partie de code est réutilisée, le copier peut être suffisant. Mais si vous en avez besoin pour d'autres utilisations, vous devez refactoriser toutes les méthodes faisant appel à la fonction ou méthode partagée comme nous venons de le faire.

Au lieu d'utiliser doSelect() puis de compter le nombre de résultats dans la méthode countActiveJobs(), nous avons utilisé la méthode doCount() qui est beaucoup plus rapide.

Nous venons de modifier beaucoup de fichiers pour cette simple fonctionnalité. Mais pour chaque ajout de code, nous avons essayé de le mettre dans la bonne couche de l'application et nous avons également essayé de rendre le code réutilisable. Dans la foulée, nous avons refactorisé du code existant. C'est une méthode de travail typique quand vous travaillez sur un projet symfony. Dans la capture d'écran suivante, nous montrons que 5 emplois pour diminuer l'affichage, vous devriez en voir 10 (le paramètre max_jobs_on_homepage):

Page d'accueil

Création du module de la catégorie d'un emploi

Il est temps de créer le module category :

$ php symfony generate:module frontend category

Si vous avez créé le module, vous avez sûrement utilisé propel:generate-module. C'est très bien, mais comme nous n'aurons pas besoin de 90% du code généré, j'ai utilisé le generate:module qui crée un module vide.

tip

Pourquoi ne pas ajouter une action category pour le module job ? Nous pourrions, mais comme le sujet principal de la page catégorie est une catégorie, il parait plus naturel de créer un module de category dédiée.

Lorsque vous accédez à la page catégorie, la route category devra trouver la catégorie associée avec la variable slug. Mais étant donné que slug n'est pas stocké dans la base de donnée et parce qu'il est impossible d'en déduire la catégorie, nous n'avons aucun moyen de trouver la catégorie pour le moment.

Mise à jour de la base de donnée

Nous devons ajouter la colonne slug à la table category :

# config/schema.yml
propel:
  jobeet_category:
    id:           ~
    name:         { type: varchar(255), required: true }
    slug:         { type: varchar(255), required: true, index: unique }

A présent, slug est une colonne réelle et vous pouvez donc supprimer la méthode getSlug() du modèle JobeetCategory.

A chaque modification du nom de la catégorie, la valeur de slug doit être mise à jour. Ajoutons la méthode setName() :

// lib/model/JobeetCategory.php
public function setName($name)
{
  parent::setName($name);
 
  $this->setSlug(Jobeet::slugify($name));
}

Utilisez la tâche propel:build --all --and-load pour mettre à jour les tables de la base de donnée et recharger les données avec vos jeux de test :

$ php symfony propel:build --all --and-load --no-confirmation

Nous pouvons maintenant créer la méthode executeShow(). Remplacez le contenu du fichier des actions de category avec le code suivant :

// apps/frontend/modules/category/actions/actions.class.php
class categoryActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->category = $this->getRoute()->getObject();
  }
}

note

Puisque nous venons de supprimer la méthode executeIndex(), vous pouvez aussi supprimer le Template indexSuccess.php qui a été généré automatiquement (apps/frontend/modules/category/templates/indexSuccess.php).

Pour la dernière étape, il reste à créer le template showSuccess.php :

// apps/frontend/modules/category/templates/showSuccess.php
<?php use_stylesheet('jobs.css') ?>
 
<?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?>
 
<div class="category">
  <div class="feed">
    <a href="">Feed</a>
  </div>
  <h1><?php echo $category ?></h1>
</div>
 
<table class="jobs">
  <?php foreach ($category->getActiveJobs() as $i => $job): ?>
    <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>">
      <td class="location">
        <?php echo $job->getLocation() ?>
      </td>
      <td class="position">
        <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>
      </td>
      <td class="company">
        <?php echo $job->getCompany() ?>
      </td>
    </tr>
  <?php endforeach; ?>
</table>

Partials

Notez que nous avons copié et collé la balise <table> afin de créer une liste d'emploi depuis le Template job indexSuccess.php. C'est mauvais. Il est temps d'apprendre un nouveau tour. Lorsque vous avez besoin de réutiliser une partie d'un Template, vous devez créer un partial. Un partial est un extrait du code du Template qui peut être partagé entre plusieurs Templates. Un partial est juste un autre Template qui commence par un caractère de soulignement (_).

Créez le fichier _list.php :

// apps/frontend/modules/job/templates/_list.php
<table class="jobs">
  <?php foreach ($jobs as $i => $job): ?>
    <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>">
      <td class="location">
        <?php echo $job->getLocation() ?>
      </td>
      <td class="position">
        <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>
      </td>
      <td class="company">
        <?php echo $job->getCompany() ?>
      </td>
    </tr>
  <?php endforeach; ?>
</table>

Vous pouvez inclure un partial en utilisant le helper include_partial() :

<?php include_partial('job/list', array('jobs' => $jobs)) ?>

Le premier argument du helper include_partial() est le nom du partial (nom du module, un slash /, et le nom du partial sans le caractère de soulignement (_). Le second argument est un tableau contenant les variables à passer dans le partial.

note

Pourquoi ne pas utiliser la méthode include() intégrée à PHP plutôt que le helper include_partial() ? La principale différence est le support du cache intégré au helper include_partial().

Remplaçez le bloc de code HTML <table> dans les deux templates par un appel include_partial() :

// in apps/frontend/modules/job/templates/indexSuccess.php
<?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?>
 
// in apps/frontend/modules/category/templates/showSuccess.php
<?php include_partial('job/list', array('jobs' => $category->getActiveJobs())) ?>

Pagination de la liste

Conditions du Jour 2 :

"La liste est paginée avec 20 emplois par page."

Pour paginer une liste d'objets Propel, symfony fournit une classe dédiée : sfPropelPager. Dans l'action category, au lieu de passer les objets job au template showSuccess, nous allons passer un paginateur :

// apps/frontend/modules/category/actions/actions.class.php
public function executeShow(sfWebRequest $request)
{
  $this->category = $this->getRoute()->getObject();
 
  $this->pager = new sfPropelPager(
    'JobeetJob',
    sfConfig::get('app_max_jobs_on_category')
  );
  $this->pager->setCriteria($this->category->getActiveJobsCriteria());
  $this->pager->setPage($request->getParameter('page', 1));
  $this->pager->init();
}

tip

La méthode sfRequest::getParameter() prend une valeur par défaut pour le second argument. Dans l'action ci-dessus, si le paramètre requis page n'existe pas, alors getParameter() retournera 1.

Le constructeur sfPropelPager utilise la classe du modèle et le nombre maximum d'items à afficher par page. Ajouter la dernière ligne au fichier de configuration :

# apps/frontend/config/app.yml
all:
  active_days:          30
  max_jobs_on_homepage: 10
  max_jobs_on_category: 20

La méthode sfPropelPager::setCriteria() prend un objet Criteria à utiliser lors de la sélection des items de la base de données.

Ajoutez la méthode getActiveJobsCriteria() :

// lib/model/JobeetCategory.php
public function getActiveJobsCriteria()
{
  $criteria = new Criteria();
  $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
 
  return JobeetJobPeer::addActiveJobsCriteria($criteria);
}

Maintenant que nous avons défini la méthode getActiveJobsCriteria(), nous pouvons refactoriser les autres méthodes du modèle JobeetCategory qui l'utilise :

// lib/model/JobeetCategory.php
public function getActiveJobs($max = 10)
{
  $criteria = $this->getActiveJobsCriteria();
  $criteria->setLimit($max);
 
  return JobeetJobPeer::doSelect($criteria);
}
 
public function countActiveJobs()
{
  $criteria = $this->getActiveJobsCriteria();
 
  return JobeetJobPeer::doCount($criteria);
}

Et pour finir, mettons le Template à jour :

<!-- apps/frontend/modules/category/templates/showSuccess.php -->
<?php use_stylesheet('jobs.css') ?>
 
<?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?>
 
<div class="category">
  <div class="feed">
    <a href="">Feed</a>
  </div>
  <h1><?php echo $category ?></h1>
</div>
 
<?php include_partial('job/list', array('jobs' => $pager->getResults())) ?>
 
<?php if ($pager->haveToPaginate()): ?>
  <div class="pagination">
    <a href="<?php echo url_for('category', $category) ?>?page=1">
      <img src="/legacy/images/first.png" alt="First page" title="First page" />
    </a>
 
    <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>">
      <img src="/legacy/images/previous.png" alt="Previous page" title="Previous page" />
    </a>
 
    <?php foreach ($pager->getLinks() as $page): ?>
      <?php if ($page == $pager->getPage()): ?>
        <?php echo $page ?>
      <?php else: ?>
        <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $page ?>"><?php echo $page ?></a>
      <?php endif; ?>
    <?php endforeach; ?>
 
    <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getNextPage() ?>">
      <img src="/legacy/images/next.png" alt="Next page" title="Next page" />
    </a>
 
    <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getLastPage() ?>">
      <img src="/legacy/images/last.png" alt="Last page" title="Last page" />
    </a>
  </div>
<?php endif; ?>
 
<div class="pagination_desc">
  <strong><?php echo count($pager) ?></strong> jobs in this category
 
  <?php if ($pager->haveToPaginate()): ?>
    - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong>
  <?php endif; ?>
</div>

L'essentiel de ce code traite des liens vers d'autres pages. Voici la liste des méthodes sfPropelPager utilisées dans ce Template :

  • getResults(): Retourne un tableau d'objets Propel de la page actuelle
  • getNbResults(): Retourne le nombre total de résultats
  • haveToPaginate(): Retourne true s'il y a plus d'une page
  • getLinks(): Retourne une liste de liens vers des pages à afficher
  • getPage(): Retourne le numéro de la page actuelle
  • getPreviousPage(): Retourne le numéro de la page précédente
  • getNextPage(): Retourne le numéro de la page suivante
  • getLastPage(): Retourne le numéro de la dernière page

Comme sfPropelPager implémente également les interfaces Iterator et Countable, vous pouvez utiliser la fonction count() pour obtenir le nombre de résultats au lieu de la méthode getNbResults().

Pagination

À demain

Si vous avez travaillé sur votre propre implémentation d'hier et le sentiment que vous n'avez pas appris beaucoup aujourd'hui, cela signifie que vous êtes habituer à la philosophie de symfony. Le processus pour ajouter une nouvelle fonctionnalité à un site web symfony est toujours le même : réfléchir sur les URL, créer certaines actions, mettre à jour le modèle et écrire quelques Templates. Et, si vous pouvez appliquer certaines bonnes pratiques de développement en plus, vous deviendrez un maître symfony très rapidement.

Demain sera le début d'une nouvelle semaine pour Jobeet. Pour fêter ça, nous allons parler d'un tout nouveau sujet : les tests.