Development

Documentation/fr_FR/askeet/trunk/D4 (diff)

You must first sign up to be able to contribute.

Changes between Version 8 and Version 9 of Documentation/fr_FR/askeet/trunk/D4

Show
Ignore:
Author:
forresst (IP: 82.255.75.185)
Timestamp:
03/01/08 19:08:23 (9 years ago)
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Documentation/fr_FR/askeet/trunk/D4

    v8 v9  
    11{{{ 
    22#!html 
    3 <div style="border: solid 2px #f80;padding:10px;margin:5px;background-color: #fdb"> 
     3<div style="border: solid 3px #ff8;padding:10px;margin:5px;background-color: #ffd"> 
    44}}} 
    5 Cette partie de la documentation est en cours de traduction. Cela signifie qu'elle est traduite de manière soit incomplète, soit inexacte. En attendant que cette traduction soit terminée, vous pouvez consulter la [http://www.symfony-project.org/askeet/1_0/4 version en anglais] pour des informations plus fiables. 
     5Cette partie de la documentation d'Askeet est déjà traduite. 
     6[http://trac.symfony-project.com/wiki/Documentation/fr_FR/askeet/4 Symfony - Calendrier de l'Avent, 4ème jour : Refactorisation]. 
    67{{{ 
    78#!html 
    89</div> 
    910}}} 
    10  
    11 {{{ 
    12 #!WikiMarkdown 
    13 Calendrier de l'Avent, 4ème jour : Refactorisation 
    14 ================================================== 
    15  
    16 Précédemment dans Symfony  
    17 ------------------------- 
    18  
    19 Au cours du [troisième jour](3.txt), toutes les couches de l'architecture MVC ont été vues et modifiées pour obtenir la liste des questions correctement affichée sur la page d'accueil. L'application commence à être plus agréable mais manque toujours de contenu. 
    20  
    21 Les objectifs du quatrième jour sont d'afficher la liste des réponses concernant une question, de donner une belle URL à la page de détail d'une question,  d'ajouter une classe personnalisée, et de migrer des bouts de code vers un meilleur endroit. Ceci devrait vous aider à comprendre les concepts de template, de modèle, de politique de routage, et de refactorisation. Vous pouvez penser qu'il est trop tôt pour réécrire du code qui est vieux de seulement quelques jours, mais nous allons voir ce que vous en penserez à la fin de ce tutoriel. 
    22  
    23 Pour lire ce tutoriel, vous devriez être familiarisé avec les concepts liés à [l'implémentation du MVC dans symfony] (http://www.symfony-project.com/book/1_0/02-Exploring-Symfony-s-Code). Ça pourrait aussi vous aider si vous aviez une idée de ce qu'est la [Méthode agile][1]. 
    24  
    25 Afficher les réponses d'une question 
    26 ------------------------------------ 
    27  
    28 Premièrement, continuons l'adaptation des template générés par le CRUD `Question` lors du [deuxième jour](2.txt). 
    29  
    30 L'action `question/show` est dédiée à l'affichage des détails d'une question, à condition que vous lui passiez un `id`. Pour le tester, appelez juste (vous devez changer le `2` par le bon `id` de la question de votre table): 
    31  
    32     http://askeet/frontend_dev.php/question/show/id/2 
    33  
    34 [question detail](/images/askeet/question_day4.gif) 
    35  
    36 Vous avez surement déjà vu la page `show` si vous avez déjà manipulé l'application. C'est ici que nous allons ajouter les réponses à une question. 
    37  
    38 ### Un coup d'oeil rapide à l'action 
    39  
    40 Premièrement, jetons un coup d'oeil à l'action `show`, située dans le fichier `askeet/apps/frontend/modules/question/actions/actions.class.php`: 
    41  
    42     [php] 
    43     public function executeShow() 
    44      { 
    45        $this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id')); 
    46        $this->forward404Unless($this->question); 
    47      } 
    48  
    49 Si vous connaissez Propel, vous reconnaitrez ici une simple requête sur la table `Question`. Elle a pour but de récupérer l'unique enregistrement ayant comme clé primaire la valeur du paramètre `id` de la requête. Dans l'exemple donné dans l'URL ci-dessus, le paramètre `id` a la valeur `1`, donc la méthode `->retrieveByPk()` de la classe `QuestionPeer` va retourner l'objet de la classe `Question` avec `1` comme clé primaire. Si vous ne connaissez pas Propel, revenez après avoir lu [un peu de documentation][2] sur leur site web. 
    50  
    51 Le résultat de cette requête est passé au template `showSuccess.php` grâce à la variable `$question`. 
    52  
    53 La méthode `->getRequestParameter('id')` de l'objet `sfAction` récupère ... le paramètre `id`, qu'il soit passé par la méthode GET ou POST. Par exemple si vous demandez  
    54          
    55         http://askeet/frontend_dev.php/question/show/id/1/myparam/myvalue 
    56  
    57 . . . ensuite l'action `show` sera capable de rechercher `myvalue` en faisant `$this->getRequestParameter('myparam')`. 
    58  
    59 >**Note**: la méthode `forward404Unless()` envoie au navigateur une page 404 si la question n'existe pas dans la base de données. C'est toujours bon de traiter les cas inconnus et les erreurs qui peuvent apparaître pendant l'exécution. Symfony vous fournit des méthodes simples pour vous aider à faire des choses correctes, facilement.  
    60  
    61 ### Modifier le template `showSuccess.php` 
    62  
    63 Le template généré `showSuccess.php` n'est pas exactement ce dont nous avons besoin, donc nous allons complètement le réécrire. Ouvrez le fichier `frontend/modules/question/templates/showSuccess.php` et remplacez le contenu par:  
    64  
    65     [php] 
    66     <?php use_helper('Date') ?> 
    67      
    68     <div class="interested_block"> 
    69       <div class="interested_mark"> 
    70         <?php echo count($question->getInterests()) ?> 
    71       </div> 
    72     </div> 
    73          
    74     <h2><?php echo $question->getTitle() ?></h2> 
    75          
    76     <div class="question_body"> 
    77       <?php echo $question->getBody() ?> 
    78     </div> 
    79      
    80     <div id="answers"> 
    81     <?php foreach ($question->getAnswers() as $answer): ?> 
    82       <div class="answer"> 
    83         posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?>  
    84         on <?php echo format_date($answer->getCreatedAt(), 'p') ?> 
    85         <div> 
    86           <?php echo $answer->getBody() ?> 
    87         </div> 
    88       </div> 
    89     <?php endforeach; ?> 
    90     </div> 
    91  
    92 Vous reconnaissez ici le div `interested_block` qui a déjà été ajouté hier au template `listSuccess.php`. Il affiche juste le nombre d'utilisateur intéressé par une question. Ensuite, les markup ressemblent beaucoup à ceux de `list`, mis à part qu'il n'y a pas de `link_to` dans le titre. C'est juste une réécriture du code initial pour afficher seulement les informations nécessaires à une question. 
    93  
    94 La nouvelle partie est le div `answers`. Il affiche toutes les réponses d'une question (utilisant simplement la méthode Propel `$question->getAnswers()`), et pour chaque réponse, affiche le taux de pertinence, le nom de l'auteur et la date de création en plus du contenu. 
    95  
    96 La fonction `format_date()` est un autre exemple d'assistants pour les template, qui nécessite une déclaration initiale. Vous pouvez en apprendre plus sur la syntaxe de cet assistant et sur d'autres dans [le chapitre sur l'internationalisation](http://www.symfony-project.com/book/1_0/13-I18n-and-L10n) du livre symfony (ces assistants accélèrent les taches pénibles en affichant les dates dans le bon format). 
    97   
    98 >**Note**: Propel crée le nom des méthodes, liés à une table, en ajoutant automatiquement un 's' à la fin du nom de la table. Veuillez pardonner l'affreuse méthode `->getRelevancys()` puisqu'elle vous évitera plusieurs lignes de code SQL. 
    99  
    100 ### Ajouter quelques données de test 
    101  
    102 Il est temps d'ajouter quelques données aux tables `answer` et `relevancy` à la fin du fichier `data/fixtures/test_data.yml` (vous êtes libre d'ajouter les vôtres): 
    103  
    104     Answer: 
    105       a1_q1: 
    106         question_id: q1 
    107         user_id:     francois 
    108         body:        | 
    109           You can try to read her poetry. Chicks love that kind of things. 
    110        
    111       a2_q1: 
    112         question_id: q1 
    113         user_id:     fabien 
    114         body:        | 
    115           Don't bring her to a donuts shop. Ever. Girls don't like to be 
    116           seen eating with their fingers - although it's nice.  
    117        
    118       a3_q2: 
    119         question_id: q2 
    120         user_id:     fabien 
    121         body:        | 
    122           The answer is in the question: buy her a step, so she can  
    123           get some exercise and be grateful for the weight she will 
    124           lose. 
    125        
    126       a4_q3: 
    127         question_id: q3 
    128         user_id:     fabien 
    129         body:        | 
    130           Build it with symfony - and people will love it. 
    131  
    132 Recharger les données avec: 
    133  
    134     $ php batch/load_data.php 
    135  
    136 Naviguez jusqu'à l'action affichant la première question pour vérifier si les modifications sont correctes: 
    137  
    138         http://askeet/frontend_dev.php/question/show/id/XX 
    139  
    140 >**Note**: Remplacez XX par l'`id` de votre première question. 
    141  
    142 ![question answers](/images/askeet/answers_day4.gif) 
    143  
    144 La question est maintenant affichée de manière plus fantaisiste, suivie de ses réponses. C'est mieux, non? 
    145  
    146 Modifier le modèle, partie I 
    147 ---------------------------- 
    148  
    149 Il est presque sûr que le nom complet de l'auteur sera nécessaire à un autre endroit de l'application. Vous pouvez donc considérer que c'est un attribut de l'objet `User`. Cela veut dire qu'il va y avoir une méthode dans le modèle de `User` permettant de récupérer le nom complet, au lieu de le construire dans une action supplémentaire. Ecrivons cette méthode. Ouvrez le fichier `askeet/lib/model/User.php` et ajouter la méthode suivante :  
    150  
    151     [php] 
    152     public function __toString() 
    153     { 
    154       return $this->getFirstName().' '.$this->getLastName(); 
    155     } 
    156  
    157 Pourquoi la méthode est nommée `__toString()` au lieu de `getFullName()` ou quelque chose de similaire? Parce que la méthode `__toString()` est la méthode par défaut utilisée par PHP5 pour représenter un objet sous la forme d'un string. Cela signifie que vous pouvez simplement remplacer       
    158  
    159 [php] 
    160     posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?>  
    161  du template `askeet/apps/frontend/modules/question/templates/showSuccess.php` 
    162  
    163 par 
    164  
    165     [php] 
    166     posted by <?php echo $answer->getUser() ?>  
    167  
    168 pour obtenir le même résultat. Chouette, n'est ce pas ? 
    169  
    170 Ne vous répétez pas 
    171 ------------------- 
    172  
    173 Un des principes de la méthode agile est d'éviter la duplication de code. Il dit "Don't Repeat Yourself" (D.R.Y.). C'est parce que le code dupliqué est deux fois plus long à revoir, modifier, tester et valider plutôt qu'un unique bout de code encapsulé. Il rend la maintenance de l'application plus complexe. Et si vous prêtez attention à la dernière partie du tutoriel d'aujourd'hui, vous noterez probablement, un peu de code dupliqué entre le template `listSuccess.php` d'hier et le template `showSuccess.php`: 
    174  
    175     [php] 
    176     <div class="interested_block"> 
    177       <div class="interested_mark"> 
    178         <?php echo count($question->getInterests()) ?> 
    179       </div> 
    180     </div> 
    181  
    182 Donc notre première session de [refactorisation](http://en.wikipedia.org/wiki/Refactoring) enlèvera ce bout de code des deux template et le mettra dans un **fragment**, ou un bout de code réutilisable. Créez le fichier `_interested_user.php` dans le répertoire `askeet/apps/frontend/modules/question/templates/` avec le code suivant: 
    183  
    184     [php] 
    185     <div class="interested_mark"> 
    186       <?php echo count($question->getInterests()) ?> 
    187     </div> 
    188  
    189 Ensuite, remplacez le code original des deux templates (`listSuccess.php` et `showSuccess.php`) par: 
    190  
    191     [php] 
    192     <div class="interested_block"> 
    193       <?php include_partial('interested_user', array('question' => $question)) ?> 
    194     </div> 
    195  
    196 Un fragment n'a pas d'accès natif aux objets courants. Le fragment utilise la variable `$question`, donc elle doit être définie dans l'appel de `include_partial`. L'additionnel `_` devant le nom du fichier du fragment aide à distinguer ceux des template actuels du dossier `templates/`. Si vous souhaitez en savoir plus sur les fragments, lisez le  [chapitre sur la Vue](http://www.symfony-project.com/book/1_0/07-Inside-the-View-Layer) du livre symfony. 
    197  
    198 Modifier le modèle, partie II 
    199 ----------------------------- 
    200  
    201 L'appel `$question->getInterests()` du nouveau fragment fait une requête à la base de données et renvoie un tableau d'objets de la classe `Interest`. C'est une requête lourde juste pour le nombre de personne intéressé, et elle pourrait surcharger la base de données. Rappelez-vous que cet appel est aussi fait dans le template `listSuccess.php`, mais cette fois en boucle, pour chaque question de la liste. Ca serait une bonne idée de l'optimiser. 
    202  
    203 Une bonne solution est d'ajouter une colonne à la table `Question` appelée `interested_users`, et de mettre à jour cette colonne chaque fois qu'un intérêt est créé pour cette question. 
    204  
    205 >**Caution**: Nous sommes sur le point de modifier le modèle sans manière apparente de le tester, puisqu'il n'y a actuellement aucune manière d'ajouter des enregistrements `Interest` grâce à askeet. Vous ne devriez jamais modifier quelque chose sans pouvoir le tester. 
    206 > 
    207 > Heureusement, nous avons une manière de tester cette modification, et vous la découvrirez plus tard dans cette partie. 
    208  
    209 ### Ajouter un champ dans le modèle objet de User 
    210  
    211 Allez y sans crainte et modifiez le modèle de données `askeet/config/schema.xml` en ajoutant à la table `ask_question`:  
    212  
    213     [xml] 
    214     <column name="interested_users" type="integer" default="0" /> 
    215  
    216 Ensuite reconstruisez le modèle: 
    217  
    218     $ symfony propel-build-model 
    219  
    220 C'est exact, nous reconstruisons déjà le modèle sans nous inquiéter des extensions de celui ci. C'est parce que l'extension de la classe User faite dans `askeet/lib/model/User.php`, hérite de la classe générée par Propel `askeet/lib/model/om/BaseUser.php`. C'est pourquoi nous devrions jamais éditer le code du répertoire `askeet/lib/model/om/`: il est remplacé chaque fois que la commande `build-model` est appelée. Symfony aide à soulager le cycle de vie normal des changements de modèles dans les étapes de n'importe quel projet Web. 
    221  
    222 Vous avez besoin également de mettre à jour la base de données actuelle. Pour éviter d'écrire quelques déclarations SQL, vous pouvez reconstruire votre schéma SQL et recharger vos données de test: 
    223  
    224     $ symfony propel-build-sql 
    225     $ mysql -u youruser -p askeet < data/sql/schema.sql 
    226     $ php batch/load_data.php 
    227  
    228 >**Note**: TIMTOWTDI: There is more than one way to do it (il y a plus d'une manière de le faire). Au lieu de reconstruire la base de données, vous pouvez ajouter une nouvelle colonne à la main: 
    229 > 
    230 >     $ mysql -u youruser -p askeet -e "alter table ask_question add interested_users int default '0'" 
    231 > 
    232  
    233 ### Modifier la méthode `save()` de l'objet `Interest` 
    234  
    235 La mise à jour de la valeur de ce nouveau champ doit être faite chaque fois qu'un utilisateur déclare son intérêt pour une question, par exemple à chaque fois qu'un enregistrement est ajouté dans la table `Interest`. Vous pouvez implémenter ceci avec les trigger de MySQL, mais cela serait une solution dépendant de la base de données, et nous voulons pouvoir changer de base de données facilement. 
    236  
    237 La meilleur solution est de modifier le modèle en remplaçant la méthode `save()` de la classe `Interest`. Cette méthode est appelée chaque fois qu'un objet de la classe `Interest` est créé. Donc ouvrez le fichier  `askeet/lib/model/Interest.php` et écrivez la méthode suivante:  
    238  
    239     [php] 
    240     public function save($con = null) 
    241     {   
    242         $ret = parent::save($con); 
    243          
    244         // update interested_users in question table 
    245         $question = $this->getQuestion(); 
    246         $interested_users = $question->getInterestedUsers(); 
    247         $question->setInterestedUsers($interested_users + 1); 
    248         $question->save($con); 
    249  
    250         return $ret; 
    251     } 
    252  
    253 La nouvelle méthode `save()` renvoie la question correspondant à l'intérêt courant, et incrémente son champ `interested_users`. Puis, il fait l'habituel `save()`. Par contre`$this->save() ferait une boucle infinie, donc nous utilisons la méthode de classe `parent::save()` à la place. 
    254  
    255 ### Sécuriser la requête de mise à jour avec une transaction 
    256  
    257 Que se passe t-il si la base de données crash entre la mise à jour de l'objet `Question` et celui de l'objet `Interest` ? Vous finiriez avec des données corrompues. C'est le même problème qui est rencontré dans une banque lors d'un transfert d'argent. Une première requête diminue le montant d'un compte, et une seconde augmente un autre compte. 
    258  
    259 Si  deux requêtes sont hautement dépendantes, vous devriez sécuriser leur exécution avec une **transaction**. Une transaction est une assurance que les deux requêtes réussiront, ou aucunes d'elles. Si quelque chose de mauvais arrive à l'une des requêtes de la transaction, toutes les précédentes réussies sont annulées, et la base de données retourne dans l'état avant la transaction. 
    260  
    261 Notre méthode `save()` est une bonne opportunité pour illustrer l'implémentation des transactions dans Symfony. Remplacez le code par:  
    262  
    263     [php] 
    264     public function save($con = null) 
    265     { 
    266       $con = Propel::getConnection(); 
    267       try 
    268       { 
    269         $con->begin(); 
    270    
    271         $ret = parent::save($con); 
    272    
    273         // update interested_users in question table 
    274         $question = $this->getQuestion(); 
    275         $interested_users = $question->getInterestedUsers(); 
    276         $question->setInterestedUsers($interested_users + 1); 
    277         $question->save($con); 
    278    
    279         $con->commit(); 
    280    
    281         return $ret; 
    282       } 
    283       catch (Exception $e) 
    284       { 
    285         $con->rollback(); 
    286         throw $e; 
    287       } 
    288     } 
    289  
    290 Premièrement, la méthode ouvre une connexion directe à la base de données par Creole. Entre les déclarations de `->begin()` et `->commit()`, la transaction s'assure que tout sera fait ou rien. Si quelque chose échoue, une exception sera envoyée, et la base de données exécutera un retour à l'état précédent. 
    291  
    292 ### Changer le template 
    293  
    294 Maintenant que la méthode `->getInterestedUsers()` de l'objet `Question` fonctionne correctement, il est temps de simplifier le fragment `_interested_user.php` en remplaçant:  
    295  
    296     [php] 
    297     <?php echo count($question->getInterests()) ?> 
    298  
    299 par 
    300  
    301     [php] 
    302     <?php echo $question->getInterestedUsers() ?> 
    303  
    304 >**Note**: Grâce à notre brillante idée d'employer un fragment au lieu de laisser le code reproduit dans les template, cette modification  nécessite seulement que nous le fassions qu'une fois. Sinon, nous aurions du modifier les template `listSuccess.php` et `showSuccess.php` et pour des gens paresseux comme nous, cela aurait été accablant. 
    305  
    306 En termes de nombre de requêtes et temps d'exécution, cela devrait être meilleur. Vous pouvez le vérifier grâce au nombre de requêtes à la base de données indiquée dans la barre d'outils de débogage, après l'icône base de données. Notez que vous pouvez aussi récupérer des détails sur les requêtes SQL de la page courante en cliquant sur l'icône base de données: 
    307  
    308 ![database queries before refactoring](/images/askeet/debug_before_day4.gif) 
    309 ![database queries after refactoring](/images/askeet/debug_after_day4.gif) 
    310  
    311 ### Tester la validité de la modification 
    312  
    313 Nous allons vérifier que rien n'est cassé en rappelant l'action `show`, mais avant ça, relancer l'importation batch des données que nous avons écrites hier: 
    314  
    315     $ cd /home/sfprojects/askeet/batch 
    316     $ php load_data.php 
    317  
    318 Quand nous créons les enregistrements de la table `Interest`, l'objet `sfPropelData` utilisera la méthode `save()` et devrait correctement mettre à jour les enregistrements `User` connexes. Donc c'est une bonne manière de tester les modifications du modèle, même s'il n'y a encore aucune interface CRUD établie avec l'objet `Interest`. 
    319  
    320 Vérifiez-le en demandant la page d'accueil et le détail de la première question: 
    321  
    322     http://askeet/frontend_dev.php/ 
    323     http://askeet/frontend_dev.php/question/show/id/XX 
    324  
    325 Le nombre d'utilisateurs intéressés n'a pas changé. Cest une modification réussie!  
    326  
    327 La même chose pour les réponses 
    328 ------------------------------- 
    329  
    330 Ce qui a été fait pour `count($question->getInterests())` peut être aussi bien fait pour `count($answer->getRelevancys())`. La seule différence sera qu'une réponse peut avoir des votes positifs et négatifs, alors qu'une question peut seulement être notée comme intéressante. 
    331 Maintenant que vous avez compris comment modifier le modèle, nous pouvons aller plus vite. Voici les changements, juste pour rappel. Vous n'avez pas à les copier à la main pour le tutoriel de demain, si vous utilisez [l'espace de stockage SVN d'askeet](http://svn.askeet.com/tags/release_day_4/). 
    332  
    333 * Ajoutez les colonnes suivantes à la table `answer` dans `schema.xml` 
    334  
    335         [xml] 
    336         <column name="relevancy_up" type="integer" default="0" /> 
    337         <column name="relevancy_down" type="integer" default="0" /> 
    338  
    339 * Reconstruisez le modèle et mettez à jour en conséquence la base de données 
    340  
    341         $ symfony propel-build-model 
    342         $ symfony propel-build-sql 
    343         $ mysql -u youruser -p askeet < data/sql/schema.sql 
    344  
    345 * La fonction `->save()` de la classe `Relevancy` dans `lib/model/Relevancy.php` 
    346  
    347         [php] 
    348         public function save($con = null) 
    349         { 
    350           $con = Propel::getConnection(); 
    351           try 
    352           { 
    353             $con->begin(); 
    354        
    355             $ret = parent::save(); 
    356        
    357             // update relevancy in answer table 
    358             $answer = $this->getAnswer(); 
    359             if ($this->getScore() == 1) 
    360             { 
    361               $answer->setRelevancyUp($answer->getRelevancyUp() + 1); 
    362             } 
    363             else 
    364             { 
    365               $answer->setRelevancyDown($answer->getRelevancyDown() + 1); 
    366             } 
    367             $answer->save($con); 
    368        
    369             $con->commit(); 
    370        
    371             return $ret; 
    372           } 
    373           catch (Exception $e) 
    374           { 
    375             $con->rollback(); 
    376             throw $e; 
    377           } 
    378         } 
    379  
    380 * Ajoutez les deux méthodes suivantes dans la classe `Answer` du modèle: 
    381  
    382         [php] 
    383         public function getRelevancyUpPercent() 
    384         { 
    385           $total = $this->getRelevancyUp() + $this->getRelevancyDown(); 
    386          
    387           return $total ? sprintf('%.0f', $this->getRelevancyUp() * 100 / $total) : 0; 
    388         } 
    389          
    390         public function getRelevancyDownPercent() 
    391         { 
    392           $total = $this->getRelevancyUp() + $this->getRelevancyDown(); 
    393          
    394           return $total ? sprintf('%.0f', $this->getRelevancyDown() * 100 / $total) : 0; 
    395         } 
    396  
    397 * Changez la partie concernant la réponse dans `question/templates/showSuccess.php` par: 
    398  
    399         [php] 
    400         <div id="answers"> 
    401         <?php foreach ($question->getAnswers() as $answer): ?> 
    402           <div class="answer"> 
    403             <?php echo $answer->getRelevancyUpPercent() ?>% UP <?php echo $answer->getRelevancyDownPercent() ?> % DOWN 
    404             posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?>  
    405             on <?php echo format_date($answer->getCreatedAt(), 'p') ?> 
    406             <div> 
    407               <?php echo $answer->getBody() ?> 
    408             </div> 
    409           </div> 
    410         <?php endforeach; ?> 
    411         </div> 
    412  
    413 * Ajoutez quelques données de test 
    414  
    415         Relevancy: 
    416           rel1: 
    417             answer_id: a1_q1 
    418             user_id:   fabien 
    419             score:     1 
    420      
    421           rel2: 
    422             answer_id: a1_q1 
    423             user_id:   francois 
    424             score:     -1 
    425      
    426 * Lancez le batch de popularisation 
    427  
    428 * Vérifiez la page `question/show` 
    429  
    430 ![relevancies on answers](/images/askeet/home_day4.gif) 
    431  
    432 Routage 
    433 ------- 
    434  
    435 Depuis le début du tutorial, nous appelons l'URL: 
    436  
    437     http://askeet/frontend_dev.php/question/show/id/XX 
    438      
    439 Les règles de routage par défaut de Symfony comprennent cette requête comme si vous aviez réellement demandé : 
    440  
    441     http://askeet/frontend_dev.php?module=question&action=show&id=XX 
    442  
    443 Mais avoir un systême de routage ouvre beaucoup d'autres possibilités. Nous pouvons utiliser le titre de la question comme une URL, pour pouvoir demander la même page avec: 
    444  
    445     http://askeet/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend 
    446  
    447 Ceci optimise la manière dont les moteurs de recherche indexent les pages de votre site Web, et rend les urls plus lisibles. 
    448  
    449 ### Créons une version alternative au titre 
    450  
    451 Premièrement, nous avons besoin d'une version convertie du titre - un titre dépouillé - pour être employée comme URL. [Il y a plus d'une façon de le faire](http://en.wikipedia.org/wiki/Perl), et nous allons choisir de stocker la version alternative dans une nouvelle colonne de la table `Question`. Dans le `schema.xml`, ajoutez la ligne suivante dans la table `Question`:  
    452  
    453     [xml] 
    454     <column name="stripped_title" type="varchar" size="255" /> 
    455     <unique name="unique_stripped_title"> 
    456       <unique-column name="stripped_title" /> 
    457     </unique> 
    458  
    459 ... et reconstruisez le modèle et mettez à jour la base de données: 
    460  
    461     $ symfony propel-build-model 
    462     $ symfony propel-build-sql 
    463     $ mysql -u youruser -p askeet < data/sql/lib.model.schema.sql 
    464  
    465 Nous allons redéfinir la méthode `setTitle()` de l'objet `Question` de sorte qu'il mette le titre dépouillé en même temps. 
    466  
    467 ### Classe personnalisée 
    468  
    469 Avant cela, nous allons créer une classe personnalisée pour réellement transformer un titre en titre dépouillé, puisque cette fonction ne concerne pas vraiment l'objet `Question` (nous l'utiliserons probablement aussi pour l'objet `Answer`).  
    470  
    471 Créons un nouveau fichier `myTools.class.php` dans le répertoire `askeet/lib/`:  
    472  
    473     [php] 
    474     <?php 
    475      
    476     class myTools 
    477     { 
    478       public static function stripText($text) 
    479       { 
    480         $text = strtolower($text); 
    481      
    482         // strip all non word chars 
    483         $text = preg_replace('/\W/', ' ', $text); 
    484      
    485         // replace all white space sections with a dash 
    486         $text = preg_replace('/\ +/', '-', $text); 
    487      
    488         // trim dashes 
    489         $text = preg_replace('/\-$/', '', $text); 
    490         $text = preg_replace('/^\-/', '', $text); 
    491      
    492         return $text; 
    493       } 
    494     } 
    495  
    496 Maintenant ouvrez le fichier classe `askeet/lib/model/Question.php` et ajoutez-y: 
    497  
    498     [php] 
    499     public function setTitle($v) 
    500     { 
    501       parent::setTitle($v); 
    502        
    503       $this->setStrippedTitle(myTools::stripText($v)); 
    504     } 
    505  
    506 Notez que la classe personnalisée `myTools` n'a pas besoin d'être déclarée: Symfony la charge automatiquement quand elle est nécessaire, à condition quelle soit située dans le répertoire `lib/`. 
    507  
    508 Vous pouvez maintenant recharger vos données. 
    509  
    510     $ symfony cc 
    511     $ php batch/load_data.php 
    512  
    513 Si vous voulez en savoir plus sur les classes personnalisées et l'aide personnalisée, lisez le [chapitre sur les extensions](http://www.symfony-project.com/book/1_0/07-Inside-the-View-Layer) du livre Symfony. 
    514  
    515 ### Changer les liens de l'action `show` 
    516  
    517 Dans le template `listSuccess.php`, changez la ligne 
    518  
    519     [php] 
    520     <h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2> 
    521  
    522 par 
    523  
    524     [php] 
    525     <h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2> 
    526  
    527 Maintenant ouvrez `actions.class.php` du module `question`, et changez l'action `show` en:  
    528  
    529     [php] 
    530     public function executeShow() 
    531     { 
    532       $c = new Criteria(); 
    533       $c->add(QuestionPeer::STRIPPED_TITLE, $this->getRequestParameter('stripped_title')); 
    534       $this->question = QuestionPeer::doSelectOne($c); 
    535    
    536       $this->forward404Unless($this->question); 
    537     } 
    538  
    539 Essayer à nouveau d'afficher la liste des questions et d'accéder à chacune en cliquant sur le titre: 
    540  
    541     http://askeet/frontend_dev.php/ 
    542      
    543 Les urls affichent correctement les titres dépouillés des questions: 
    544  
    545     http://askeet/frontend_dev.php/question/show/stripped-title/what-shall-i-do-tonight-with-my-girlfriend 
    546  
    547 ### Changer les règles de routage 
    548  
    549 Mais ce n'est pas exactement comme nous cherchons à les afficher. Il est maintenant temps d'éditer les règles de routage. Ouvrez le fichier de configuration `routing.yml` (situé dans le répertoire `askeet/apps/frontend/config/` et ajoutez la règle suivante au début du fichier: 
    550  
    551     question: 
    552       url:   /question/:stripped_title 
    553       param: { module: question, action: show } 
    554  
    555 Dans la ligne `url`, le mot `question` est un texte personnalisé qui apparait dans l'url finale, alors que `stripped_title` est un paramètre (il est précédé de`:`). Ils forment un **pattern** que le système de routage de Symfony applique aux liens de l'action `question/show`  parce que tous les liens de nos template utilisent l'assistant `link_to()`. 
    556  
    557 Il est temps pour le test final: réaffichez la page d'accueil, cliquez sur le titre de la première question. Non seulement la première question s'affiche (montrant que rien n'est cassé) mais la barre d'adresse de votre navigateur affiche: 
    558  
    559     http://askeet/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend 
    560  
    561 Si vous voulez en apprendre plus à propos des dispositifs de routages, lisez le [chapitre sur la politique de routage](http://www.symfony-project.com/book/1_0/09-Links-and-the-Routing-System) du livre Symfony. 
    562  
    563 A demain 
    564 -------- 
    565  
    566 Aujourdhui, le site web en lui-même n'a pas beaucoup de nouvelles fonctionnalités. Cependant, nous avons vu plus de codage sur les template, vous savez comment modifier le modèle, et globalement le code a été refait dans beaucoup d'endroit. 
    567  
    568 Cela arrive tout le temps dans la vie dun projet Symfony: le code qui peut être réutilisé est refait en fragment ou en classe personnalisée, le code qui apparait dans une action ou un template et qui appartient réellement au modèle est déplacé dans celui-ci. Même si cela sépare le code en un bon nombre de petits fichiers disséminés dans beaucoup de dossiers, la maintenance et l'évolution est plus facile. De plus, la structure de fichiers d'un projet Symfony le rend facile à localiser selon sa nature (Aide, modèle, template, action, classe personnalisée, etc.). 
    569  
    570 Le travail de refactorisation réalisé aujourdhui va accélérer le développement dans les jours à venir. 
    571 Et nous ferons périodiquement encore plus de refactorisation dans la vie de ce projet, puisque la manière dont nous développons - faire une fonctionnalité utilisable sans se préoccuper des  fonctionnalités à venir - requiert une bonne structure du code si nous ne voulons pas finir avec un désordre total. 
    572  
    573 Qu'est-il prévu pour demain? Nous allons commencer à écrire un formulaire et voir comment récupérer ses informations. Nous allons également diviser la liste des questions de la page d'accueil en plusieurs pages. Dans le même temps, n'hésitez pas à télécharger le code d'aujourd'hui dans l'espace de stockage SVN (tagged release_day_4):  
    574  
    575     http://svn.askeet.com/tags/release_day_4/ 
    576    
    577 et à nous envoyer vos questions en utilisant la [mailing-list askeet](mailto:askeet-subscribe@symfony-project.com) ou le [forum dédié](http://www.symfony-project.com/forum/index.php/f/8/). 
    578    
    579 [1]: http://en.wikipedia.org/wiki/Agile_software_development  "Agile Software development definition at Wikipedia" 
    580 [2]: http://propel.phpdb.org/docs/user_guide/                 "Propel documentation" 
    581  
    582  
    583 }}}