Development

Documentation/fr_FR/askeet/trunk/D7

You must first sign up to be able to contribute.

Version 6 (modified by forresst, 10 years ago)
bientot la fin de la traduction .…

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 version en anglais pour des informations plus fiables.

Calendrier de l’avent septième jour : Manipulation du modèle et de la vue

Précédemment dans Symfony

Six jours se sont déjà écoulés, et vous pouvez penser que l'application n'est pas très utile jusqu'ici. C'est parce que certains considèrent l'utilité d'une application par rapport au nombre de pages disponibles, et ils voient que l'askeet peut seulement lui afficher une liste de questions, lui afficher des réponses, et manipuler des sessions d'utilisateur.

La raison pour laquelle nous ne donnons pas tellement d'importance au nombre de pages, c'est qu'il est si facile d'ajouter de nouvelles pages avec symfony. Vous en voulez la preuve ? Ok, aujourd'hui nous afficherons une liste des dernières questions posées et une liste des réponses postées, une liste d'utilisateurs intéressés par une question, le profil d'un utilisateur, et nous ajouterons une barre de navigation à chaque page pour accéder à ces dispositifs. Comme cela ne représentera pas beaucoup de travail pendant une heure, nous installerons également la configuration de vue et irons voir ce qui a été fait pendant cette semaine. Prêt ? Partez.

La pré factorisation

Ainsi, nous allons ajouter les listes paginées avec des commandes de mise en page semblables à celles dans question/templates/_list.php. Nous n'aimons pas nous répéter, ainsi nous extrairons le code de mise en page à partir de celui des aides personnalisées. Une aide est une fonction de PHP rendue accessible aux templates (de la même manière que les aides link_to() et format_date()).

Créez le fichier GlobalHelper.php dans askeet/apps/frontend/lib/helper et ajoutez lui le code suivant:

[php]
<?php

function pager_navigation($pager, $uri)
{
  $navigation = '';

  if ($pager->haveToPaginate())
  {  
    $uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page=';

    // First and previous page
    if ($pager->getPage() != 1)
    {
      $navigation .= link_to(image_tag('first.gif', 'align=absmiddle'), $uri.'1');
      $navigation .= link_to(image_tag('previous.gif', 'align=absmiddle'), $uri.$pager->getPreviousPage()).'&nbsp;';
    }

    // Pages one by one
    $links = array();
    foreach ($pager->getLinks() as $page)
    {
      $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page);
    }
    $navigation .= join('&nbsp;&nbsp;', $links);

    // Next and last page
    if ($pager->getPage() != $pager->getCurrentMaxLink())
    {
      $navigation .= '&nbsp;'.link_to(image_tag('next.gif', 'align=absmiddle'), $uri.$pager->getNextPage());
      $navigation .= link_to(image_tag('last.gif', 'align=absmiddle'), $uri.$pager->getLastPage());
    }

  }

  return $navigation;
}

L'aide à la navigation dans les pages améliore le code que nous avons précédemment écrit : elle permet n'importe quelle règle de routage, afin de ne pas faire afficher les liens previous pour la première page ni les liens next pour la dernière page. Nous avons également ajouté quatre nouvelles images (first.gif, previous.gif, next.gif et last.gif) pour rendre les liens plus jolis. Récupérez-les dans le répertoire SVN de l'askeet. Vous réutiliserez probablement cette aide à l'avenir pour vos propres projets.

Pour employer cette aide dans le fragment de question/templates/_list.php, appelez la fonction d'aide comme suit:

[php]
<?php use_helper('Text', 'Global') ?>

<?php foreach($question_pager->getResults() as $question): ?>
  <div class="question">
    <div class="interested_block">
      <?php include_partial('interested_user', array('question' => $question)) ?>
    </div>

    <h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>

    <div class="question_body">
      <?php echo truncate_text($question->getBody(), 200) ?>
    </div>
  </div>
<?php endforeach; ?>

<div id="question_pager">
  <?php echo pager_navigation($question_pager, 'question/list') ?>
</div>

Le nom Global se réfère au fichier GlobalHelper.php que vous venez juste de créer.

Vérifiez que tout fonctionne comme avant avec l'adresse:

http://askeet/frontend_dev.php/

Refactorisation de la navigation des pages

Liste des dernières questions

Dans le module question, créez une nouvelle action recent:

[php]
public function executeRecent()
{
  $this->question_pager = QuestionPeer::getRecentPager($this->getRequestParameter('page', 1));
}

C'est aussi simple que cela. Nous considérons que la capacité de saisir les dernières questions devrait être une méthode de classe de QuestionPeer. La classe -Peer est consacrée aux listes de retour d'objets d'une classe donnée - Ceci est expliqué en détail dans le chapitre du modèle du book de symfony. La méthode de classe getRecent() doit être créée. Ouvrez la classe askeet/lib/model/QuestionPeer.php et ajoutez lui ceci:

[php]
public static function getRecentPager($page)
{
  $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
  $c = new Criteria();
  $c->addDescendingOrderByColumn(self::CREATED_AT);
  $pager->setCriteria($c);
  $pager->setPage($page);
  $pager->setPeerMethod('doSelectJoinUser');
  $pager->init();

  return $pager;
}


Le tri en ordre décroissant des dates de création choisira les dernières questions. Cette méthode utilise self au lieu de parent parce que c'est une fonction de classe, et pas une fonction d'objet. La raison pour laquelle nous faisons ici un doSelectJoinUser() au lieu d'un simple doSelect(), c'est parce que nous savons que le template aura besoin des coordonnées de l'auteur de la question. Cela signifierait une première demande de la liste des questions, plus une demande par question pour obtenir l'utilisateur concerné. La méthode doSelectJoinUser() fait tout cela dans une seule requête : quand nous demandons

[php]
$question->getUser();

...il n'y a qu'une requête envoyée à la base de données. Le joinUser nous permet de réduire le nombre de requête à 1 + le nombre de questions à seulement 1. La base de données nous remerciera de cette optimisation facile.

La documentation Propel vous donnera toutes les explications au sujet de ce grand dispositif.

Le template de la liste des dernières questions ressemblera beaucoup à la liste des questions affichées dans la page d'accueil. Créez askeet/apps/frontend/module/question/templates/recentSuccess.php avec:

[php]
<h1>recent questions</h1>

<?php include_partial('list', array('question_pager' => $question_pager)) ?>

Vous comprenez maintenant pourquoi nous avons refactorisé la liste des question dans un fragment pendant le jour cinq. En conclusion, vous avez besoin d'ajouter une règle à recent_questions dans le fichier de configuration frontend/config/routing.yml, comme expliqué durant le quatrième jour:

recent_questions:
  url:   /question/recent/:page
  param: { module: question, action: recent, page: 1 }

Mais attendez: le fragment question/_list crée des liens avec la règle de routage question/list, en l'utilisant, cela ne fonctionnera pas pour la liste des dernières questions. Nous avons besoin que la règle de routage soit passée en paramètre au fragment afin qu'elle puisse être réutilisée pour plusieurs pages. Donc changez la dernière ligne de recentSuccess.php par:

[php]
<?php include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/recent')) ?>

Et également changer les dernières lignes du fragment _list.php par:

[php]
<div id="question_pager">
  <?php echo pager_navigation($question_pager, $rule) ?>
</div>

N'oubliez pas d'ajouter également le paramètre de règle dans l'appel au fragment _list dans modules/question/templates/listSuccess.php.

[php]
<h1>popular questions</h1>

<?php echo include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/list')) ?>

Vider le cache (la configuration a été modifiée), et c'est tout..

Pour afficher la liste des dernières questions, saisissez dans la zone URL de votre navigateur:

http://askeet/question/recent

liste des dernières questions

Liste des dernières réponses

C'est à peu près la même chose que ci-dessus, et nous serons donc assez exhaustif sur celui-ci:

  • Créez le module answer:

    $ symfony init-module frontend answer
    
  • Créez une nouvelle action recent:

    [php]
    public function executeRecent()
    {
      $this->answer_pager = AnswerPeer::getRecentPager($this->getRequestParameter('page', 1));
    }
    
  • Etendez la classe AnswerPeer:

    [php]
    public static function getRecentPager($page)
    {
      $pager = new sfPropelPager('Answer', sfConfig::get('app_pager_homepage_max'));
      $c = new Criteria();
      $c->addDescendingOrderByColumn(self::CREATED_AT);
      $pager->setCriteria($c);
      $pager->setPage($page);
      $pager->setPeerMethod('doSelectJoinUser');
      $pager->init();
    
      return $pager;
    }
    
  • Créez un nouveau template recentSuccess.php:

    [php]
    <?php use_helper('Date', 'Global') ?>
    
    <h1>recent answers</h1>
    
    <div id="answers">
    <?php foreach ($answer_pager->getResults() as $answer): ?>
      <div class="answer">
        <h2><?php echo link_to($answer->getQuestion()->getTitle(), 'question/show?stripped_title='.$answer->getQuestion()->getStrippedTitle()) ?></h2>
        <?php echo count($answer->getRelevancys()) ?> points
        posted by <?php echo link_to($answer->getUser(), 'user/show?id='.$answer->getUser()->getId()) ?> 
        on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
        <div>
          <?php echo $answer->getBody() ?>
        </div>
      </div>
    <?php endforeach ?>
    </div>        
    
    <div id="question_pager">
      <?php echo pager_navigation($answer_pager, 'answer/recent') ?>
    </div>
    
  • Testez le dans votre navigateur:

    http://askeet/answer/recent
    

liste des dernières réponses

Vous vous habituez à lui, n'est-ce pas ?

Note: Ceux qui ont prêté attention au jour 4, ont probablement reconnu le morceau de code utilisé pour afficher les détails d'une réponse. Puisque ce code est employé dans au moins deux endroits, nous allons refactoriser et créer un _answer.php partiel, destiné à être utilisé aussi bien dans question/show et dans answer/recent. Les détails peuvent être trouvés dans le répertoire SVN de l'askeet.

Profil utilisateur

Le nom de l'utilisateur dans une réponse sera relié à une action de user/show (qui reste à écrire). Ce sera le profil de l'utilisateur, et il montrera les dernières questions et réponses postées, ainsi que quelques détails au sujet de l'utilisateur.

La première chose à faire est de créer l'action:

[php]
public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByPk($this->getRequestParameter('id', $this->getUser()->getSubscriberId()));
  $this->forward404Unless($this->subscriber);


$this->interests = $this->subscriber->getInterestsJoinQuestion(); $this->answers = $this->subscriber->getAnswersJoinQuestion(); $this->questions = $this->subscriber->getQuestions(); }

Les méthodes ->getInterestsJoinQuestion() et ->getAnswersJoinQuestion() sont des méthodes natives de la classe User. Vous pouvez examiner la classe askeet/lib/model/om/BaseUser.php pour voir comment elles fonctionnent.

Le template askeet/apps/frontend/modules/user/templates/showSuccess.php ne devrait pas vous poser de problème:

[php]
<h1><?php echo $subscriber ?>?s profile</h1>

<h2>Interests</h2>

<ul>
<?php foreach ($interests as $interest): $question = $interest->getQuestion() ?>
  <li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>

<h2>Contributions</h2>

<ul>
<?php foreach ($answers as $answer): $question = $answer->getQuestion() ?>
  <li>
    <?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?><br />
    <?php echo $answer->getBody() ?>
  </li>
<?php endforeach; ?>
</ul>

<h2>Questions</h2>

<ul>
<?php foreach ($questions as $question): ?>
  <li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>

Naturellement, vous pourriez souhaiter limiter le nombre de résultat retourné par chacune des méthodes ->getInterestsJoinQuestion(), ->getAnswersJoinQuestion() et getQuestion() de l'objet User, comme l'ordre de tri. Il est tout simplement fait par une de ces méthodes, dans le fichier de classe askeet/lib/model/User.php, et nous ne dévoilerons pas ici comment le faire - mais la release d'aujourd'hui l'inclura.

Il est temps pour l'essai final. Voyons ce que le premier utilisateur a fait:

http://askeet/user/show/id/1

profil utilisateur

Maintenant nous pouvons également lier un profil d'utilisateur à une question. Ajoutez la ligne suivante à question/templates/showSuccess.php et question/templates/_list.php au début du div question_body:

[php]
<div>asked by <?php echo link_to($question->getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'f') ?></div>

N'oubliez pas de déclarer l'utilisation de l'aide Date dans _list.php.

Ajouter une barre de navigation

Nous changerons la disposition globale pour ajouter une barre latérale. Cette barre posséde le contenu dynamique, mais comme nous voulons régler sa position dans la mise en page, il ne peut pas faire partie de chaque template. En outre, en mettant le code de la barre dans le template, cela signifierait le répéter plusieurs fois, et vous savez que nous n'aimons pas faire cela.

C'est pourquoi la barre sera un composant. Un composant est le résultat d'une action (par exemple le code HTML résulte de l'exécution d'un template) mis à disposition dans une variable. Le chapitre sur la vue du book symfony explique ce qu'est un composant, et les différences entre un composant et un fragment.

Ajouter le composant dans le gabarit principal

Ouvrez le gabarit global (askeet/apps/frontend/templates/layout.php). Vous souvenez-vous de cette partie du code:

[php]
<div id="content_bar">
  <!-- Nothing for the moment -->
  <div class="verticalalign"></div>
</div>

Remplacer le commentaire par

[php]
  <?php include_component_slot('sidebar') ?>

Et c'est tout.

Préciser quelle action va dans le composant

Nous avons décidé d'utiliser quelque chose d'un peu plus puissant qu'un simple composant: un connecteur de composant. C'est un élément dont l'action peut être modifiée en fonction de l'appel de l'action - permettant du contenu contextuel. C'est la configuration de vue (écrite dans le fichier view.yml) qui définit quelle action correspond à un connecteur de composant:

default:
  components:
    sidebar:      [sidebar, default]

Dans cet exemple, le connecteur de composant nommé sidebar est déclaré pour être le résultat de l'action default du module sidebar.

La configuration de vue peut être définie pour l'application entière (dans le répertoire askeet/apps/frontend/config/) ou spécialement pour un module (dans le réertoire askeet/apps/frontend/modules/mymodule/config/). Pour notre cas, nous le définirons pour l'application entière, et nous le redéfinirons, si nécessaire, pour fournir dans un contexte spécifique des liens dans la barre latérale.

Alors ouvrez askeet/apps/frontend/config/view.yml et ajoutez la configuration du connecteur de composant illustrée ci-dessus. Vous trouverez plus d'informations sur la configuration de vue dans le chapitre relatif du livre de symfony.

Ecrire l'action sidebar/default et le template

D'abord, nous laisserons symfony initialiser le nouveau module sidebar:

$ symfony init-module frontend sidebar

Ensuite, nous avons besoin d'écrire un composant default. Dans le répertoire askeet/apps/frontend/modules/sidebar/actions/, renommez actions.class.php en components.class.php, et changez son contenu par:

[php]
<?php 

class sidebarComponents extends sfComponents
{
  public function executeDefault()
  {
  }
}

Une vue de composant est un template, tout comme pour une action. La différence est dans la nomination: Une vue de composant est nommée comme un fragment (commençant par le _) plutôt que comme un template (se terminant par Success). Créez ainsi un fragment askeet/apps/frontend/modules/sidebar/templates/_default.php (et effacez indexSuccess.php qui ne sera pas utilisé) avec le contenu suivant:

[php]
<?php echo link_to('ask a new question', 'question/add') ?>

<ul>
  <li><?php echo link_to('popular questions', 'question/list') ?></li>
  <li><?php echo link_to('latest questions', 'question/recent') ?></li>
  <li><?php echo link_to('latest answers', 'answer/recent') ?></li>
</ul>

Nous avons modifié le fichier de configuration view.yml, mais les actions dans l'environnement de production ne les voit pas. Ils utilisent la version en cache - celle qui ne contient pas la configuration des connecteurs de composant. Si vous voulez voir les modifications, soit vous effacez la mémoire cache soit vous naviguez dans l'environnement de développement:

$ symfony clear-cache

ou

http://askeet/frontend_dev.php/

La barre de navigation est affichée correctement sur chaque page

sidebar

Note: C'est un effet général de la configuration d'environnement de production. Ainsi vous devez vous rappeler qu'il faut employer l'environnement de développement pendant la phase de développement (quand vous changez beaucoup la configuration), et effacer la mémoire cache lorsque vous naviguez dans l'environnement de production après chaque changement dans la configuration.

Un peu plus de configuration de la vue

Pendant que nous sommes là, nous allons jeter un coup d'œil au fichier de configuration de l'application view.yml dans apps/config/:

default:
  http_metas:
    content-type: text/html; charset=utf-8

  metas:
    title:        symfony project
    robots:       index, follow
    description:  symfony project
    keywords:     symfony, project
    language:     en

  stylesheets:    [main, layout]

  javascripts:    []

  has_layout:     on
  layout:         layout

  components:
    sidebar:      [sidebar, default]

The metas section contains a configuration for the meta tags of the whole site. The title key also defines the title that is displayed in the title bar of the browser window. This title is very important, because it is the first thing that a user sees of the site if it is found by a search index. It is therefore necessary to change it to something more adapted to the askeet site:

  metas:
    title:        askeet! ask questions, find answers
    robots:       index, follow
    description:  askeet!, a symfony project built in 24 hours
    keywords:     symfony, project, askeet, php5, question, answer
    language:     en

Refresh the current page. If you don't see any change, that's because you are in the production environment, and you should clear the cache first, to get the proper window title:

window title

Note: In addition to providing a default title for your project pages, symfony creates a default robots.txt and favicon.ico in the web root directory (askeet/web/). Don't forget to change them also!

Note: You might need to change the title for each page of your site. You can do that by defining a custom view.yml configuration for each module, but that would only let you give static titles. Alternatively, you can use a dynamic value from an action with the ->setTitle() method, as described in the view configuration chapter:

  [php]
  $this->getResponse()->setTitle($title);

Look at what we have done

It is a general tradition to stop and look at what you've done when you reach the seventh day. That's a good opportunity to document a few things, including the current data model and the available actions.

As a matter of fact, you should document your code while you write it, for instance using PHP doc-style comments for each method. The thing with a symfony project is that the names used in the methods or functions often serve as an explanation of their purpose and use. The methods are kept short, and so are very readable. Most of the time, the templates only use foreach and if statements that are pretty self-explanatory. That's why the code you will find in the askeet SVN repository doesn't contain much documentation - plus the fact that we've already written seven hours worth of explanations about the work we've done!

Now let's have a look at the updated entity relationship diagram:

ERD

The list of available actions is the following:

answer/
  recent
question/
  list
  show
  recent
sidebar/
  default (component)
user/
  show
  login
  logout
  handleErrorLogin

The model also contains the following methods:

Anwser()
  getRelevancyUpPercent()
  getRelevancyDownPercent()
AnswerPeer::
  getRecentPager()
Interest->
  save()
Question->
  setTitle()
QuestionPeer::
  getQuestionFromTitle()
  getHomepagePager()
  getRecentPager()
Relevancy
  save()
User->
  __toString()
  setPassword()

myUser->
  signIn()
  signOut()
  getSubscriberId()
  getSubscriber()
  getNickName()

...plus a custom tools class and a custom validator, placed in the askeet/apps/frontend/lib/ directory.

That's not bad for seven hours, is it?

A demain

The application progressed a lot today, and it was quite fast to do. Everything is now prepared to inject some AJAX in the human-computer interaction. Tomorrow, users will be able to login and to declare their interest for a question using AJAX. Don't miss it!

You can still download today's full code from the askeet SVN repository, tagged release_day_7. The askeet mailing-list will answer any of your questions faster than lightspeed.