Development

Documentation/fr_FR/jobeet/6

You must first sign up to be able to contribute.

Version 7 (modified by sebastien.b, 9 years ago)
--

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 : 12 Mars 2009
  • Date de dernière modification :

Jour 6: Aller plus loin avec le Modèle

Hier était un grand jour. Vous avez appris comment créer des URLs propres et comment utiliser symfony pour automatiser beaucoup de choses.

Aujourd'hui, nous allons améliorer Jobeet en optimisant le code ci et là. Vous en apprendrez plus sur toutes les fonctions que nous avons déjà présenté au cours des jours précédents.

L' objet Propel Criteria

Conditions du Jour 2 :

"Quand un utilisateur arrive sur Jobeet, il doit voir la liste des jobs actifs."

Mais pour l'instant, tous les jobs sont affichés, qu'ils soient actifs ou non :

    // apps/frontend/modules/job/actions/actions.class.php
    class jobActions extends sfActions
    {
      public function executeIndex(sfWebRequest $request)
      {
        $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria());
      }

      // ...
    }

Un job est considéré actif s'il a été posté il y a moins de 30 jours. La méthode doSelect() utilise un objet ~Criteria~ qui décrit la requête à exécuter sur la base de données. Dans le code ci-dessus, aucun argument n'est passé dans Criteria, ce qui signifie que tous les enregistrements seront retournés.

Modifions cela pour n'afficher que les jobs actifs :

    public function executeIndex(sfWebRequest $request)
    {
      $criteria = new Criteria();
      $criteria->add(JobeetJobPeer::CREATED_AT, time() - 86400 * 30, Criteria::GREATER_THAN);

      $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria);
    }

La méthode Criteria::add() ajoute une clause WHERE à la requête SQL générée. Ici, criteria retournera les jobs qui datent de moins de 30 jours. La méthode add() accepte plusieurs types d'opérateurs de comparaison. Voici les plus utilisés :

  • Criteria::EQUAL
  • Criteria::NOT_EQUAL
  • Criteria::GREATER_THAN, Criteria::GREATER_EQUAL
  • Criteria::LESS_THAN, Criteria::LESS_EQUAL
  • Criteria::LIKE, Criteria::NOT_LIKE
  • Criteria::CUSTOM
  • Criteria::IN, Criteria::NOT_IN
  • Criteria::ISNULL, Criteria::ISNOTNULL
  • Criteria::CURRENT_DATE, Criteria::CURRENT_TIME, Criteria::CURRENT_TIMESTAMP

Debugging Propel generated SQL

Etant donné que vous n'écrivez pas les requêtes SQL, c'est Propel qui prend en charge la génération des requêtes propres au moteur de base de donnée choisi le 3ème jour. Mais il est parfois utile de connaître le code SQL généré par Propel;. Par exemple, pour debugger une requête qui ne fonctionne pas. Dans l'environnement de développement, symfony enregistre toutes ces requêtes ( ainsi que beaucoup d'autres informations ) dans le répertoire log/. There is one log file for every combination of an application and an environment. Le fichier qui nous intéresse est frontend_dev.log :

    # log/frontend_dev.log
    Dec 6 15:47:12 symfony [debug] {sfPropelLogger} exec: SET NAMES 'utf8'
    Dec 6 15:47:12 symfony [debug] {sfPropelLogger} prepare: SELECT jobeet_job.ID, jobeet_job.CATEGORY_ID, jobeet_job.TYPE, jobeet_job.COMPANY, jobeet_job.LOGO, jobeet_job.URL, jobeet_job.POSITION, jobeet_job.LOCATION, jobeet_job.DESCRIPTION, jobeet_job.HOW_TO_APPLY, jobeet_job.TOKEN, jobeet_job.IS_PUBLIC, jobeet_job.CREATED_AT, jobeet_job.UPDATED_AT FROM `jobeet_job` WHERE jobeet_job.CREATED_AT>:p1
    Dec 6 15:47:12 symfony [debug] {sfPropelLogger} Binding '2008-11-06 15:47:12' at position :p1 w/ PDO type PDO::PARAM_STR

Comme vous pouvez le voir, Propel a généré une clause WHERE pour la colonne created_at column (WHERE jobeet_job.CREATED_AT > :p1).

NOTE

Dans la requête, :p1 indique que Propel utilise les conditions déclarées. La valeur actuelle de :p1 ( '2008-11-06 15:47:12' dans l'exemple ci-dessus ) est passée pendant l'exécution de la requête. L'utilisation de conditions déclarées reduit considérablement le risque d'attaques du type SQL injection

Le travail est facilité mais devoir basculer entre le navigateur, l'IDE et le fichier log à chaque fois que l'on veut tester une modification est assez contraignant. Heureusement, symfony possède une barre d'outil de débuggage. Toutes les informations nécessaire sont disponibles dans votre navigateur :

Object ~Serialization~

Jusqu'à présent, notre code fonctionne mais il est loin d'être parfait et ne prend pas en charge les contraintes évoqués le 2ème jour :

"Un utilisateur peut activer à nouveau ou augmenter la validité de l'offre d'emploi pour une période de 30 jours supplémentaires..."

Le code actuel se base sur la valeur de la colonne created_at qui stocke la date de création ce qui ne nous permet pas de satisfaire la condition ci-dessus.

Mais si vous vous rappelez le schéma de la base décrit le 3ème jour, nous avons aussi défini une colonne expires_at. Pour l'instant cette valeur est vide car nous ne l'avons pas renseignée dans le fichier du répertoire fixture. Cette valeur peut être automatiquement renseignée à plus 30 jours à partir de la date courante lors de la création d'un job.

Quand vous devez créer une action automatique avant que l'objet Propel soit sérialisé dans la base, vous pouvez surpasser la méthode save() de la classe du modèle :

    // lib/model/JobeetJob.php
    class JobeetJob extends BaseJobeetJob
    {
      public function save(PropelPDO $con = null)
      {
        if ($this->isNew() && !$this->getExpiresAt())
        {
          $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time();
          $this->setExpiresAt($now + 86400 * 30);
        }

        return parent::save($con);
      }

      // ...
    }

La méthode ~isNew()~ renvoie true quand l'objet n'est pas encore sérialisé dans la base et false dans la cas contraire.

A présent, modifions l'action pour récupérer les jobs actifs en utilisant la colonne expires_at au lieu de la colonne created_at :

    public function executeIndex(sfWebRequest $request)
    {
      $criteria = new Criteria();
      $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN);

      $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria);
    }

La requête sélectionnera seulement les jobs possédant une date expires_at.

Aller plus loin avec Fixtures

Si vous actualisez la page d'accueil de Jobeet, vous ne constaterez aucun changement. Modifions le fichier fixture pour ajouter un job qui a expiré :

    # data/fixtures/020_jobs.yml
    JobeetJob:
      # other jobs

      expired_job:
        category_id:  programming
        company:      Sensio Labs
        position:     Web Developer
        location:     Paris, France
        description:  |
          Lorem ipsum dolor sit amet, consectetur
          adipisicing elit.
        how_to_apply: Send your resume to lorem.ipsum [at] dolor.sit
        is_public:    true
        is_activated: true
        expires_at:   2005-12-01
        token:        job_expired
        email:        job@example.com

NOTE

Faites bien attention quand vous faîtes un copier/coller du code. il faut conserver l'indentation. Il doit y avoir deux espaces devant expired_job.

Comme vous pouvez le constater, il est possible de définir une valeur pour la colonne created_at même si elle est automatiquement remplie par Propel. La valeur définie sera utilisé à la place de la valeur automatique. Rechargez les fixtures et actualisez la page d'accueil pour vérifier que l'ancien job n'apparaisse pas :

    $ php symfony propel:data-load

Vous pouvez aussi exécuter la requête suivante pour être sûr que la colonne expires_at soit automatiquement renseignée en fonction de la valeur de la colonne created_at grâce à la méthode save() :

    SELECT `position`, `created_at`, `expires_at` FROM `jobeet_job`;

Configuration Personnalisée

Dans la méthode JobeetJob::save(), nous avons figé le nombre de jours qui détermine l'expiration d'un job. Il serait préférable que la valeur de 30 jours soit paramétrable. symfony utilise le fichier configuration interne ~app.yml~ qui permet de définir des paramètres spécifiques à l'application. Ce fichier YAML peut contenir n'importe quel paramètre nécessaire :

    # apps/frontend/config/app.yml
    all:
      active_days: 30

Dans l'application, ces paramètres sont disponibles à travers la classe globale ~sfConfig~ :

    sfConfig::get('app_active_days')

Les paramètres utilisent le préfixe app_ car la classe sfConfig fournit également des accès aux paramètres symfony que nous verrons plus tard.

Mettez le code à jour pour prendre en compte ce nouveau paramètre :

    public function save(PropelPDO $con = null)
    {
      if ($this->isNew() && !$this->getExpiresAt())
      {
        $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time();
        $this->setExpiresAt($now + 86400 * sfConfig::get('app_active_days'));
      }

      return parent::save($con);
    }

Le fichier de configuration app.yml est un bon moyen de centraliser les paramètres globaux de votre application.

Pour finir, si vous avez besoin de définir des paramètres étendus, il suffit de créer un nouveau fichier app.yml dans le répertoire config à la racine de votre projet symfony.

Refactoring

Bien que notre code fonctionne correctement, il n'est pas encore parfait. Etes-vous capable de repérer le problème ?

Le code Criteria n'appartient pas à l'action ( couche Controlleur ), mais à la couche Modèle. Dans le modèle ~MVC~, le Modèle définit toute la ~logique métier~, et le Controlleur récupère les données du Modèle. Etant donné que le code renvoie une collection de jobs, déplaçons le dans la classe JobeetJobPeer et créons la méthode getActiveJobs() :

    // lib/model/JobeetJobPeer.php
    class JobeetJobPeer extends BaseJobeetJobPeer
    {
      static public function getActiveJobs()
      {
        $criteria = new Criteria();
        $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
        
        return self::doSelect($criteria);
      }
    }

Il est maintenant possible de récupérer les jobs actifs grâce à cette méthode dans les actions :

    public function executeIndex(sfWebRequest $request)
    {
      $this->jobeet_job_list = JobeetJobPeer::getActiveJobs();
    }

La refactorisation présente plusieurs avantages :

  • Le code pour récupérer les jobs actifs se trouve dans la Modèle, là ou est sa place
  • Le code du Controlleur est plus lisible
  • La méthode getActiveJobs() est réutilisable ( dans une autre action par exemple )
  • Le code du modèle peut être testé indépendemment

Récupérons les jobs grâce à la colonne expires_at :

    static public function getActiveJobs()
    {
      $criteria = new Criteria();
      $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
      $criteria->addDescendingOrderByColumn(self::EXPIRES_AT);

      return self::doSelect($criteria);
    }

La méthode addDescendingOrderByColumn() ajoute une clauser ORDER BY à la requête. Il existe aussi la méthode addAscendingOrderByColumn().

Catégories en page d'accueil

Conditions du 2ème jour :

"Les jobs sont affichés par catégorie et par leur date de publication ( les nouveaux jobs en tête de liste."

Jusqu'à présent, nous n'avons pas pris en compte la catégorie associée aux jobs. Afin d'afficher les jobs par catégorie, nous allons d'abord récupérer toutes les catégories associées à au moins un job.

Editer la classe JobeetCategoryPeer et ajouter la méthode getWithJobs() :

    // lib/model/JobeetCategoryPeer.php
    class JobeetCategoryPeer extends BaseJobeetCategoryPeer
    {
      static public function getWithJobs()
      {
        $criteria = new Criteria();
        $criteria->addJoin(self::ID, JobeetJobPeer::CATEGORY_ID);
        $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN);
        $criteria->setDistinct();

        return self::doSelect($criteria);
      }
    }

La méthode Criteria::addJoin() ajoute une clause ~JOIN~ à la requête. Par défaut, la condition join est ajoutée à la clause WHERE. Vous pouvez également modifier l'opérateur join en ajoutant un troisième argument (Criteria::LEFT_JOIN, Criteria::RIGHT_JOIN, et Criteria::INNER_JOIN).

Modifiez l'action index en conséquence :

    // apps/frontend/modules/job/actions/actions.class.php
    public function executeIndex(sfWebRequest $request)
    {
      $this->categories = JobeetCategoryPeer::getWithJobs();
    }

Dans le template, nous devons rechercher les jobs actifs dans chaque catégorie et les afficher.

    // apps/frontend/modules/job/indexSuccess.php
    <?php use_stylesheet('jobs.css') ?>

    <div id="jobs">
      <?php foreach ($categories as $category): ?>
        <div class="category_<?php echo Jobeet::slugify($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>
        </div>
      <?php endforeach; ?>
    </div>

NOTE

Pour afficher le nom d'une catégorie, nous utilisons echo $category dans le template. Ca vous paraît bizarre ? $category n'est pas un objet, comment peut-on afficher le nom de la catégorie avec un echo. La réponse se trouve au jour 3 quand nous avons défini la méthode magique __toString() pour toutes les classes du modèle.

Nous devons ajouter la méthode getActiveJobs() à la classe JobeetCategory qui retoune les jobs actifs pour l'objet catégorie :

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

      return JobeetJobPeer::getActiveJobs($criteria);
    }

Dans l'appel add(), nous avons omis le troisième argument car la valeur par défaut est Criteria::EQUAL.

La méthode JobeetCategory::getActiveJobs() utilise la méthode JobeetJobPeer::getActiveJobs() pour rechercher les jobs actifs de la catégorie donnée.

A l'appel de JobeetJobPeer::getActiveJobs(), nous voulons restreindre la condition autrement qu'en fournissant uniquement une catégorie. Au lieu de passer l'objet catégorie, nous avons décidé de passer un objet Criteria qui est la meilleure solution pour encapsuler une condition générique.

Pour ce faire, il faut fusionner cet argument Criteria avec les criteria de la méthodegetActiveJobs(). Criteria étant un objet, ce sera simple :

    // lib/model/JobeetJobPeer.php
    static public function getActiveJobs(Criteria $criteria = null)
    {
      if (is_null($criteria))
      {
        $criteria = new Criteria();
      }

      $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN);
      $criteria->addDescendingOrderByColumn(self::EXPIRES_AT);

      return self::doSelect($criteria);
    }

Limiter les résultats

Il reste encore une condition à implémenter pour la liste des jobs en page d'accueil :

"Chaque catégorie doit afficher les 10 premiers jobs et un lien doit permettre d'afficher tous les jobs d'une catégorie choisie."

C'est assez simple de l'ajouter à la méthode getActiveJobs() :

    // lib/model/JobeetCategory.php
    public function getActiveJobs($max = 10)
    {
      $criteria = new Criteria();
      $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
      $criteria->setLimit($max);

      return JobeetJobPeer::getActiveJobs($criteria);
    }

La clause ~LIMIT~ est figée dans le code du Modèle, mais il est préférable de pouvoir configurer cette valeur. Modifiez le template pour utiliser le nombre maximum de jobs configuré dans app.yml :

    <!-- apps/frontend/modules/job/indexSuccess.php -->
    <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $i => $job): ?>

et ajoutez le nouveau paramètre dans app.yml:

    all:
      active_days:          30
      max_jobs_on_homepage: 10

Fixtures dynamiques

A moins de passer la valeur max_jobs_on_homepage à une, vous ne verrez aucune différence. Nous devons ajouter des jobs ans le fichier ~fixture~s. Evidemment, vous pouvez faire 20, 30, ... copier/coller des jobs existants mais il y a une meilleure solution. La duplication n'est pas une bonne méthode, même pour les fichiers fixture.

symfony à la rescousse ! Dans symfony, les fichiers ~YAML~ peuvent contenir du code PHP qui sera évalué juste avant l'analyse du fichier. Editez le fichier fixture 020_jobs.yml et ajoutez le code suivant à la fin :

    JobeetJob:
    # Starts at the beginning of the line (no whitespace before)
    <?php for ($i = 100; $i <= 130; $i++): ?>
      job_<?php echo $i ?>:
        category_id:  programming
        company:      Company <?php echo $i."\n" ?>
        position:     Web Developer
        location:     Paris, France
        description:  Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        how_to_apply: |
          Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sit
        is_public:    true
        is_activated: true
        token:        job_<?php echo $i."\n" ?>
        email:        job@example.com

    <?php endfor; ?>

Attention ! L'analyseur de YAML n'aime pas les erreurs de formatage. Gardez bien à l'esprit les conseils suivants si vous ajoutez du code PHP dans un fichier YAML :

  • <?php ?> doit toujours commençer une ligne ou être intégré dans une valeur.
  • Si <?php ?> fint une ligne, vous devez indiquer clairement une nouvelle ligne ("\n").

Rechargez les données fixtures avec propel:data-load et vérifiez que seulement 10 jobs soient affichés en page d'accueil pour la catégorie Programming. Dans la capture d'écran suivante, nous avons modifié le nombre maximum de jobs sur 5 pour avoir une image de taille raisonnable :

Sécuriser la page Job

Même si vous connaissez l'URL d'un job qui a expiré, il ne doit plus être possible d'y accéder. Essayez l'URL d'un job expiré ( remplaçer l'id par l'id correspondant dans la base de donnée - `SELECT id, token FROM jobeet_job WHERE expires_at < NOW()` ) :

    /frontend_dev.php/job/sensio-labs/paris-france/ID/web-developer-expired

Au lieu d'afficher le job, nous devons rediriger l'utilisateur vers une erreur 404. Mais comment faire alors que le job est recherché automatiquement par la route ?

Par défaut,~sfPropelRoute~ utilise la méthode standard doSelectOne() pour rechercher l'objet. Mais il est possible de le modifier en indiquant une ~method_for_criteria~ dans la configuration de la route :

    # apps/frontend/config/routing.yml
    job_show_user:
      url:     /job/:company_slug/:location_slug/:id/:position_slug
      class:   sfPropelRoute
      options:
        model: JobeetJob
        type:  object
        method_for_criteria: doSelectActive
      param:   { module: job, action: show }
      requirements:
        id: \d+
        sf_method: [GET]

La méthode doSelectActive() reçevra l'objet Criteria crée par la route :

    // lib/model/JobeetJobPeer.php
    class JobeetJobPeer extends BaseJobeetJobPeer
    {
      static public function doSelectActive(Criteria $criteria)
      {
        $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN);

        return self::doSelectOne($criteria);
      }

      // ...
    }

Maintenant, si vous essayez d'accéder à un job qui a expiré, vous serez redirigé vers une page d'erreur 404.

Lien vers la page des Catégories

A présent, nous allons ajouter un lien vers la catégorie et créer la page de cette catégorie.

Une minute. L'heure n'est pas encore écoulée et nous n'avons pas beaucoup travaillé. En fait, vous avez tout le temps nécessaire pour mettre en pratique tout ce que nous avons déjà appris et implémenter cette fonction par vous-même. Vous pourrez vérifier votre travail demain.

A demain

Travaillez votre projet Jobeet. N'hésitez pas à abuser de la documentation en ligne API documentation et de toutes les ressources documentation disponibles sur le site pour vous aider. On se retrouve demain pour découvrir l'implémentation des catégories.

Bonne chance !

Attachments