Development

Documentation/fr_FR/jobeet/doctrine/6

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 : 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 Doctrine Query

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 = Doctrine::getTable('JobeetJob')
          ->createQuery('a')
          ->execute();
      }

      // ...
    }

Un job est considéré actif s'il a été posté il y a moins de 30 jours. La méthode ~Doctrine_Query~::execute() crée la requête à exécuter sur la base de données. Dans le code ci-dessus, aucune condition n'est spécifiée, ce qui signifie que tous les enregistrements seront retournés.

Modifions cela pour n'afficher que les jobs actifs :

    public function executeIndex(sfWebRequest $request)
    {
      $q = Doctrine_Query::create()
        ->from('JobeetJob j')
        ->where('j.created_at > ?',
         ➥ date('Y-m-d h:i:s', time() - 86400 * 30));

      $this->jobeet_job_list = $q->execute();
    }

Debugging Doctrine generated SQL

Etant donné que vous n'écrivez pas les requêtes SQL, c'est Doctrine 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 Doctrine;. 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 :

    Dec 04 13:58:33 symfony [info] {sfDoctrineLogger} executeQuery : SELECT
    j.id AS j__id, j.category_id AS j__category_id, j.type AS j__type,
    j.company AS j__company, j.logo AS j__logo, j.url AS j__url,
    j.position AS j__position, j.location AS j__location,
    j.description AS j__description, j.how_to_apply AS j__how_to_apply,
    j.token AS j__token, j.is_public AS j__is_public,
    j.is_activated AS j__is_activated, j.email AS j__email,
    j.expires_at AS j__expires_at, j.created_at AS j__created_at,
    j.updated_at AS j__updated_at FROM jobeet_job j
    WHERE j.created_at > ? (2008-11-08 01:13:35)

Comme vous pouvez le voir, Doctrine a généré une clause WHERE pour la colonne created_at column (WHERE j.created_at > ?).

NOTE

Dans la requête, ? indique que Doctrine utilise les conditions déclarées. La valeur actuelle de ? ( '2008-11-08 01:13:35' 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~](http://en.wikipedia.org/wiki/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 Doctrine soit sérialisé dans la base, vous pouvez surpasser la méthode save() de la classe du modèle :

    // lib/model/doctrine/JobeetJob.class.php
    class JobeetJob extends BaseJobeetJob
    {
      public function save(Doctrine_Connection $conn = null)
      {
        if ($this->isNew() && !$this->getExpiresAt())
        {
          $now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time();
          $this->setExpiresAt(date('Y-m-d h:i:s', $now + 86400 * 30));
        }

        return parent::save($conn);
      }

      // ...
    }

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)
    {
      $q = Doctrine_Query::create()
        ->from('JobeetJob j')
        ->where('j.expires_at > ?', date('Y-m-d h:i:s', time()));

      $this->jobeet_job_list = $q->execute();
    }

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/jobs.yml
    JobeetJob:
      # other jobs

      expired_job:
        JobeetCategory: 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 00:00:00'
        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 Doctrine. 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 Doctrine: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(Doctrine_Connection $conn = null)
    {
      if ($this->isNew() && !$this->getExpiresAt())
      {
        $now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time();
        $this->setExpiresAt(date('Y-m-d h:i:s', $now + 86400 * sfConfig::get('app_active_days')));
      }

      return parent::save($conn);
    }

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 Doctrine_Query 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/doctrine/JobeetJobTable.class.php
    class JobeetJobTable extends Doctrine_Table
    {
      public function getActiveJobs()
      {
        $q = $this->createQuery('j')
          ->where('j.expires_at > ?', date('Y-m-d h:i:s', time()));

        return $q->execute();
      }
    }

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 = Doctrine::getTable('JobeetJob')->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 :

    public function getActiveJobs()
    {
      $q = $this->createQuery('j')
        ->where('j.expires_at > ?', date('Y-m-d h:i:s', time()))
        ->orderBy('j.expires_at DESC');

      return $q->execute();
    }

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

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 JobeetCategoryTable et ajouter la méthode getWithJobs() :

    // lib/model/doctrine/JobeetCategoryTable.class.php
    class JobeetCategoryTable extends Doctrine_Table
    {
      public function getWithJobs()
      {
        $q = $this->createQuery('c')
          ->leftJoin('c.JobeetJobs j')
          ->where('j.expires_at > ?', date('Y-m-d h:i:s', time()));

        return $q->execute();
      }
    }

Modifiez l'action index en conséquence :

    // apps/frontend/modules/job/actions/actions.class.php
    public function executeIndex(sfWebRequest $request)
    {
      $this->categories = Doctrine::getTable('JobeetCategory')->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/doctrine/JobeetCategory.class.php
    public function getActiveJobs()
    {
      $q = Doctrine_Query::create()
        ->from('JobeetJob j')
        ->where('j.category_id = ?', $this->getId());

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

La méthode JobeetCategory::getActiveJobs() utilise la méthode Doctrine::getTable('JobeetJob?')->getActiveJobs()` pour rechercher les jobs actifs de la catégorie donnée.

A l'appel de Doctrine::getTable('JobeetJob')->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 Doctrine_Query qui est la meilleure solution pour encapsuler une condition générique.

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

    // lib/model/doctrine/JobeetJobTable.class.php
    public function getActiveJobs(Doctrine_Query $q = null)
    {
      if (is_null($q))
      {
        $q = Doctrine_Query::create()
          ->from('JobeetJob j');
      }

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

      return $q->execute();
    }

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/doctrine/JobeetCategory.class.php
    public function getActiveJobs($max = 10)
    {
      $q = Doctrine_Query::create()
        ->from('JobeetJob j')
        ->where('j.category_id = ?', $this->getId())
        ->limit($max);

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

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 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 ?>:
        JobeetCategory: 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 Doctrine: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 ?

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

NOTE

La paramètre ~method_for_query~ ne fonctionne pas avant la version 1.2.2.

La méthode retrieveActiveJob reçevra l'objet Doctrine_Query crée par la route :

    // lib/model/doctrine/JobeetJobTable.class.php
    class JobeetJobTable extends Doctrine_Table
    {
      public function retrieveActiveJob(Doctrine_Query $q)
      {
        $q->andWhere('a.expires_at > ?', date('Y-m-d h:i:s', time()));

        return $q->fetchOne();
      }

      // ...
    }

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