Development

Documentation/fr_FR/jobeet/doctrine/7

You must first sign up to be able to contribute.

Cette page fait partie de la traduction en français de la documentation de Symfony. Il s'agit d'une version traduite qui peut comporter des erreurs. La seule version officielle est la version en anglais.

  • Traduction : Sébastien B
  • Date de traduction : 14 Mars 2009
  • Date de dernière modification :

Jour 7: Ajout de la page catégorie

Hier, vous avez pu améliorer vos connaissances dans plusieurs domaines : requêtes avec Doctrine, fixtures, routage, débuggage et la configuration personnalisée. Et nous avions fini la leçon en vous laissant un petit défi.

Si vous avez travaillé sur la mise en place de la page catégorie, ce tutorial vous permettra de mieux compremdre.

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

La route des catégories

Pour commençer, 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 relationnel. Il est également possible d'utiliser n'importe quelle autre valeur si un accesseur est définit dans la classe de l'objet. Etant 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/doctrine/JobeetCategory.class.php
    public function getSlug()
    {
      return Jobeet::slugify($this->getName());
    }

Lien vers la catégorie

A présent, éditez le template indexSuccess.php du module job et ajoutez le lien vers la page de la 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>

Le lien est affiché seulement si la catégorie possède plus de 10 jobs. Pour faire fonctionner cette contrainte, nous allons ajouter la méthode countActiveJobs() au modèle JobeetCategory :

    // lib/model/doctrine/JobeetCategory.class.php
    public function countActiveJobs()
    {
      $q = Doctrine_Query::create()
        ->from('JobeetJob j')
        ->where('j.category_id = ?', $this->getId());

      return Doctrine::getTable('JobeetJob')->countActiveJobs($q);
    }

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

    // lib/model/doctrine/JobeetJobTable.class.php
    class JobeetJobTable extends Doctrine_Table
    {
      public function retrieveActiveJob(Doctrine_Query $q)
      {
        return $this->addActiveJobsQuery($q)->fetchOne();
      }

      public function getActiveJobs(Doctrine_Query $q = null)
      {
        return $this->addActiveJobsQuery($q)->execute();
      }

      public function countActiveJobs(Doctrine_Query $q = null)
      {
        return $this->addActiveJobsQuery($q)->count();
      }

      public function addActiveJobsQuery(Doctrine_Query $q = null)
      {
        if (is_null($q))
        {
          $q = Doctrine_Query::create()
            ->from('JobeetJob j');
        }

        $alias = $q->getRootAlias();

        $q->andWhere($alias . '.expires_at > ?', date('Y-m-d h:i:s', time()))
          ->addOrderBy($alias . '.expires_at DESC');

        return $q;
      }
    }

Comme vous pouvez le constater, nous avons refactorisé tout le code de JobeetJobTable afin d'utiliser la nouvelle méthode addActiveJobsQuery() 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 execute() puis de compter le nombre de résultats dans la méthode countActiveJobs(), nous avons utilisé la méthode count() 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.

Job Category Module Creation

Il est temps de créer le module category :

    $ php symfony generate:module frontend category

Si vous avez créer le module avant de lire ce tutoriel, vous avez sûrement utilisé la commande propel:generate-module. C'est correct mais 90% du code généré nous est inutile. La commande generate:module nous permet de créer un module vierge.

TIP

Pourquoi ne pas simplement ajouter une action category au module job ? Nous pourrions le faire mais le sujet principal de la page catégorie est une catégorie. Il paraît donc normal de créer une module category à part entière.

A l'accès de 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 parcequ'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 :`

La colonne slug peut être gérée par le comportement Sluggable de Doctrine. Il suffit de l'activer dans le modèle JobeetCategory.

    # config/doctrine/schema.yml
    JobeetCategory:
      actAs:
        Timestampable: ~
        Sluggable:
          fields: [name]
      columns:
        name:
          type: string(255)
          notnull:  true

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

NOTE

Le paramètre de la colonne slug est automatiquement pris en compte quand vous sauvegardez un enregistrement. La valeur de slug est construite à partir du champ name et est passée en objet.

Utilisez la commande propel:build-all-load pour mettre à jour les tables de la base de donnée et recharger les données grâce aux fixtures :

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

Nous pouvons maintenant créer la méthode executeShow(). Remplaçez le code du fichier des actions du module catégorie par :

    // 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 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 rest à 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 fait un copié/collé du bloc <table> du template indexSuccess.php du module job qui crée la liste des jobs. C'est une mauvaise façon de faire. Il est temps d'apprendre une nouvelle astuce. Qaund vous avez besoin de réutiliser une portion de code d'une template,vous pouvez créer un **partial**. Un partial est une partie de code d'un template qui peut être partagé avec d'autres templates. Un partial, est simplement un autre template dont le nom commença par un underscore (_).

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 source , un slash /, et le nom du partial sans le underscore _. 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

Conditions du Jour 2 :

"La page doit afficher une liste de 20 jobs avec une pagination."

Pour paginer une liste d'objets Doctrine, symfony fournit une class dédiée : `sfPropelPager`. Dans le fichier d'actions du module 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->setQuery($this->category->getActiveJobsQuery());
      $this->pager->setPage($request->getParameter('page', 1));
      $this->pager->init();
    }

TIP

La méthode sfRequest::getParameter() utilise 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 sfDoctrinePager::setQuery() prend un objet Doctrine_Query à utiliser pour récupérer les items de la base de données.

Et la méthode getActiveJobsQuery() :

    // lib/model/doctrine/JobeetCategory.class.php
    public function getActiveJobsQuery()
    {
      $q = Doctrine_Query::create()
        ->from('JobeetJob j')
        ->where('j.category_id = ?', $this->getId());

      return Doctrine::getTable('JobeetJob')->addActiveJobsQuery($q);
    }

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

    // lib/model/doctrine/JobeetCategory.class.php
    public function getActiveJobs($max = 10)
    {
      $q = $this->getActiveJobsQuery()
        ->limit($max);

      return $q->execute();
    }

    public function countActiveJobs()
    {
      return $this->getActiveJobsQuery()->count();
    }

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="/images/first.png" alt="First page" />
        </a>

        <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>">
          <img src="/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="/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="/images/last.png" alt="Last page" title="Last page" />
        </a>
      </div>
    <?php endif; ?>

    <div class="pagination_desc">
      <strong><?php echo $pager->getNbResults() ?></strong> jobs in this category

      <?php if ($pager->haveToPaginate()): ?>
        - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong>
      <?php endif; ?>
    </div>

La majeure partie de ce code utilise des liens vers les différentes pages. Voici la liste des méthodes de sfPropelPager employées dans ce template :

  * `getResults()`: Returns an array of Doctrine objects for the current page
  * `getNbResults()`: Returns the total number of results
  * `haveToPaginate()`: Returns `true` if there is more than one page
  * `getLinks()`: Returns a list of page links to display
  * `getPage()`: Returns the current page number
  * `getPreviousPage()`: Returns the previous page number
  * `getNextPage()`: Returns the next page number
  * `getLastPage()`: Returns the last page number

A demain

Si vous avez travaillé sur votre propre inétgration des catégories hier et que vous avez le sentiment de ne rien avoir appris aujourd'hui, alors vous commençez à comprendre la philosophie de symfony. Le processus pour ajouter une nouvelle fonctionnalité à un site symfony est toujours le même : - penser les URLs - créer actions nécessaires - mettre à jour le modèle - et écrire quelques templates. Si vous appliquez ces bonnes méthodes de développement, vous deviendrez très vite un expert de symfony.

Demain nous entamerons une nouvelle semaine de travail sur Jobeet. Pour fêter ça, nous aborderons une nouveauté : les tests.

Attachments