Development

Documentation/fr_FR/askeet/trunk/D5

You must first sign up to be able to contribute.

Version 4 (modified by Jean.Michel.CHAN.CHONG, 10 years ago)
--

Cette partie de la documentation askeet n'est pas encore traduite. Vous pouvez :

Calendrier de l'avent cinquième jour: formulaire et pagineur

Précédemment dans Symfony

Durant ce long quatrième jour, nous avons refactorisé notre application en déplaçant des bouts de codes dans des fichiers plus relatifs à leur nature. Vous avez également appris à modifier le modèle de sorte que des fonctions liées aux données, puissent être mises hors du code de l'action.

Le développement est propre, mais le nombre de fonctionnalités est un peu pauvre. Il est temps de permettre un peu plus d'interactivité entre le site askeet et ses utilisateurs. Et les origines de l'interactivité du HTML, autre que les liens hypertextes, ce sont les formulaires.

Les objectifs d'aujourd'hui sont de permettre à l'utilisateur de s'identifier et de mettre en page la liste des questions de la page d'accueil. Cela sera rapide à développer, mais vous permettra de récupérer d'hier.

Formulaire Login

Des utilisateurs sont présents dans les données de test, mais l'application ne peut pas les identifier. Donnons accès à un formulaire de connexion depuis n'importe quelle page de l'application. Ouvrez le layout global askeet/apps/frontend/templates/layout.php et ajoutez la ligne suivante avant le lien about :

[php]
<li><?php echo link_to('sign in', 'user/login') ?></li>

Note: le layout courant place ce lien juste derrière la barre d'outils de déboguage. Pour la voir, cachez la barre d?'utils en cliquant sur l'icône ?Sf?.

Il est temps de créer le module user. Tandis que le module question a été généré lors du deuxième jour, cette fois nous allons juste demander à Symfony de créer le squelette du module, et nous allons écrire le code nous même.

$ symfony init-module frontend user

Note: le squelette contient une action index par défaut et un template indexSuccess.php. Débarrassons-nous en, puisque nous n'en aurons pas besoin.

Créer l'action user/login

Dans le fichier user/actions/action.class.php (du nouveau répertoire askeet/apps/frontend/modules/), ajoutez l'action login :

[php]
public function executeLogin()
{
  $this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer());


return sfView::SUCCESS; }

L'action sauvegarde la page appelante dans un attribut de requête. Il sera ensuite possible au template de le mettre dans un champ caché, de sorte que l'action cible du formulaire puisse rediriger vers la page appelante, après une authentification réussie.

Le return sfView::SUCCESS passe le résultat de l'action au template loginSuccess.php. Cette instruction est implicite dans les actions qui ne contiennent pas d'instruction de retour, c'est pourquoi le template par défaut d'une action est appelé actionnameSuccess.php.

Avant de poursuivre cette l'action, jetons un oeil au template.

Créer le template loginSuccess.php

Sur internet beaucoup d'interactions homme-machine utilisent les formulaires, et Symfony facilite la création et la gestion de ces derniers en fournissant un ensemble d'assistants. Dans le répertoire askeet/apps/frontend/modules/user/templates/, créez le template loginSuccess.php:

[php]
<?php echo form_tag('user/login') ?>

  <fieldset>

  <div class="form-row">
    <label for="nickname">nickname:</label>
    <?php echo input_tag('nickname', $sf_params->get('nickname')) ?>
  </div>

  <div class="form-row">
    <label for="password">password:</label>
    <?php echo input_password_tag('password') ?>
  </div>

  </fieldset>

  <?php echo input_hidden_tag('referer', $sf_request->getAttribute('referer')) ?>
  <?php echo submit_tag('sign in') ?>

</form>

Ce template est votre première introduction aux assistants de formulaires. Ces fonctions Symfony aident à automatiser l'écriture des formulaires. L'assistant form_tag() ouvre un formulaire avec le comportement POST par défaut, et pointe vers l'action passée en argument. L'assistant input_tag() produit une balise <input> (c'est une suprise) en ajoutant automatiquement l'attribut id dont la valeur est égale au premier argument ; la valeur par défaut est donnée par le second argument. Vous pouvez en apprendre plus sur les assistants et sur le code HTML qu'ils génèrent dans le chapitre relatif du livre Symfony.

La chose essentielle ici est que l'action appelée quand le formulaire est envoyé (l'argument de form_tag()) est la même action login que pour l'afficher. Ainsi revenons à l'action.

Traiter l'envoi du formulaire Login

Remplacez l'action login que nous venons d'écrire par le code suivant :

[php]
public function executeLogin()
{
  if ($this->getRequest()->getMethod() != sfRequest::POST)
  {
    // display the form
    $this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer());
  }
  else
  {
    // handle the form submission
    $nickname = $this->getRequestParameter('nickname');


$c = new Criteria(); $c->add(UserPeer::NICKNAME, $nickname); $user = UserPeer::doSelectOne($c);

    // nickname exists?
    if ($user)
    {
      // password is OK?
      if (true)
      {
        $this->getUser()->setAuthenticated(true);
        $this->getUser()->addCredential('subscriber');

        $this->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber');
        $this->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber');


// redirect to last page return $this->redirect($this->getRequestParameter('referer', '@homepage')); } } } }

L'action login sera utilisée pour afficher le formulaire login et pour le traiter. Par conséquent, elle doit savoir dans quel contexte elle est appelée. Si l'action n'est pas appelée en mode POST, c'est parce qu'elle est appelée depuis un lien : c'est ce dont nous avons parlé précédemment. Si la demande est en mode POST, l'action est appelée par un formulaire et il est temps de le traiter.

L?action prend la valeur du champ nickname des paramètres de la requête, et requiert la table User pour voir si cet utilisateur existe.

Il y aura, dans un avenir proche, un contrôle du mot de passe qui accordera des droits d'accès à l'utilisateur. Pour le moment, la seule chose que cette action fait est de stocker dans un attribut de session l'idet le nickname de l'utilisateur. Eventuellement, l'action redirige à la page appelante grâce au champ caché referer du formulaire, passé comme un paramètre de requête. Si le champ est vide, la valeur par défaut (@homepage, qui est le nom de la règle de routage pour question/list) est utilisée à la place.

Notez la différence entre les deux types d'attribut dans cet exemple : les request attributes ($this->getRequest()->setAttribute()) sont gardés pour le template et perdus dès que la réponse est envoyée à la page appelante. Les session attributes ($this->getUser()->setAttribute()) sont gardés le temps de la durée de vie de la session utilisateur, et d'autres actions pourront encore y accéder dans le futur. Si vous voulez en savoir plus sur les attributs, vous pouvez jeter un oeil au chapitre de support des paramètres du livre Symfony.

Accorder des privilèges

C?est une bonne chose que les utilisateurs puissent s'identifier sur le site web askeet, mais ils ne le feront pas juste pour s'amuser. S'identifier sera exigé pour poster une nouvelle question, se déclarer intéressé par une question, et pour évaluer un commentaire. Toutes les autres actions seront accessibles aux utilisateurs non identifiés. Pour mettre un utilisateur comme authentifié, vous avez besoin d'appeler la fonction ->setAuthenticated() de l'objet sfUser. Cet objet fournit aussi un mécanisme de droits d'accès (->addCredential()), pour affiner la restriction d'accès selon les profiles. Le chapitre sur les droits d'accès utilisateur du livre Symfony explique tout ça en détails.

C'est le but de ces deux lignes :

[php]
$this->getContext()->getUser()->setAuthenticated(true);
$this->getContext()->getUser()->addCredential('subscriber');

Quand le nom d'utilisateur est identifié, non seulement les données utilisateurs seront mises en variables de session, mais l'accès aux parties réservées sera attribué à l'utilisateur. Nous verrons demain comment restreindre l'accès à certaines parties de l'application aux utilisateurs authentifiés.

Ajouter l'action user/logout

Il y a une dernière astuce à propos de la fonction ->setAttribute(): le dernier argument (subscriber dans l'exemple précédent) définit l'espace de noms où l'attribut sera stocké. Non seulement un espace de noms autorise un nom déjà existant dans un autre espace de noms, mais il autorise également la suppression rapide de tous les attributs avec la simple commande :

[php]
public function executeLogout()
{
  $this->getUser()->setAuthenticated(false);
  $this->getUser()->clearCredentials();

  $this->getUser()->getAttributeHolder()->removeNamespace('subscriber');

  $this->redirect('@homepage');
}

Employer des espaces de noms nous a évité d'enlever les attributs un à un : c'est une ligne de code en moins. Parlez de paresse !

Mettre à jour le layout

Le layout montre toujours un lien 'login' même si un utilisateur est déjà connecté. Corrigeons vite cela. Dans askeet/apps/frontend/templates/layout.php, changez la ligne que nous venons d'ajouter au début du tutoriel d'aujourd'hui par :

[php]
<?php if ($sf_user->isAuthenticated()): ?>
  <li><?php echo link_to('sign out', 'user/logout') ?></li>
  <li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li>
<?php else: ?>
  <li><?php echo link_to('sign in/register', 'user/login') ?></li>
<?php endif ?>

Il est temps de tester tout ca en affichant n'importe quel page de l'application, cliquez sur le lien 'login', entrez un pseudo valide ('anonymous' devrai faire l'affaire) et validez le. Si le lien 'login' en haut de la fenêtre change en 'sign out', vous avez tout fait correctement. Eventuellement, essayez de vous déconnecter pour vérifier si le lien 'login' apparait de nouveau.

logged

Vous trouverez plus d'informations à propos de la manipulation des attributs de sessions utilisateurs dans le chapitre sur les sessions utilisateurs du livre Symfony.

Pagineur de question

Car des milliers de fans de Symfony se précipiteront sur le site web d'askeet, il est très probable que la liste de questions affichées sur la page d'accueil, va beaucoup s'allonger. Pour éviter les requêtes lentes et le scrolling excessif, il est nécessaire de mettre en page la liste des questions. Symfony fournit un objet dans ce but: le sfPropelPager. Il encapsule la requête à la base de données de sorte que seuls les enregistrements à afficher sur la page courante soient exigés. Par exemple, si un pagineur est initialisé pour afficher 10 enregistrements par page, la requête à la base de données sera limitée à 10 résultats, et l'offset est mis pour marquer la page.

Modifier l?action question/list

Pendant le troisième jour, nous avons vu que l'action list du module question était tout à fait restreint :

[php]
public function executeList ()
{
  $this->questions = QuestionPeer::doSelect(new Criteria());
}

Nous allons modifier cette action, pour passer au template un objet sfPropelPager au lieu d'un tableau. Dans le même temps, nous allons classer les questions par nombre d'intéressé :

[php]
public function executeList ()
{
  $pager = new sfPropelPager('Question', 2);
  $c = new Criteria();
  $c->addDescendingOrderByColumn(QuestionPeer::INTERESTED_USERS);
  $pager->setCriteria($c);
  $pager->setPage($this->getRequestParameter('page', 1));
  $pager->setPeerMethod('doSelectJoinUser');
  $pager->init();


$this->question_pager = $pager; }

L'initialisation de l'objet sfPropelPager permet d'indiquer quelle classe il contiendra, et le nombre maximum d'objet qu'il est possible de mettre dans une page (deux dans cet exemple). La fonction ->setPage() utilise un paramètre pour afficher la page courante. Par exemple, si ce paramètre page a la valeur 2, le sfPropelPager retournera les résultats 3 à 5. La valeur par défaut du paramètre de requête page étant 1, ce pagineur retournera les résultats 1 à 2 par défaut. Vous pouvez trouver plus d'informations à propos de l'objet sfPropelPager et ses fonctions dans le chapitre sur la mise en page du livre Symfony.

Utiliser un paramètre personnalisé

C'est toujours une bonne idée de mettre les constantes que vous utilisez dans des fichiers de configuration. Par exemple, le nombre de résultats par page (2 dans cet exemple) pourrait être remplacé par un paramètre, défini dans la configuration personnalisée de votre application. Changer la ligne de new sfPropelPager ci-dessus par :

[php]
...
  $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));

Ouvrez le fichier de configuration personnalisé (askeet/apps/frontend/config/app.yml) de votre application et ajoutez-y :

all:
  pager:
    homepage_max: 2

Ici, la clé pager est utilisée comme un espace de nom, c'est pourquoi il apparait également dans le nom du paramètre. Vous pouvez trouver plus d'informations à propos des configurations personnalisées et les règles pour appeler des paramètres faits sur commande dans le chapitre de configuration du livre Symfony.

Modifier le template listSuccess.php

Dans le template listSuccess.php, remplacez juste la ligne :

[php]
<?php foreach($questions as $question): ?>

par

[php]
<?php foreach($question_pager->getResults() as $question): ?>

de sorte que la page affiche la liste de résultats stockées dans le pagineur.

Ajouter une page de navigation

Il y a une chose de plus à ajouter à ce template : la page de navigation. Pour le moment, tout ce que fait le template est d'afficher les deux premières questions, mais nous devrions ajouter la possibilité d'aller aux pages suivantes, et ensuite de revenir aux pages précédentes. Pour faire cela, ajoutez à la fin du template :

[php]
<div id="question_pager">
<?php if ($question_pager->haveToPaginate()): ?>
  <?php echo link_to('&laquo;', 'question/list?page=1') ?>
  <?php echo link_to('&lt;', 'question/list?page='.$question_pager->getPreviousPage()) ?>

  <?php foreach ($question_pager->getLinks() as $page): ?>
    <?php echo link_to_unless($page == $question_pager->getPage(), $page, 'question/list?page='.$page) ?>
    <?php echo ($page != $question_pager->getCurrentMaxLink()) ? '-' : '' ?>
  <?php endforeach; ?>

  <?php echo link_to('&gt;', 'question/list?page='.$question_pager->getNextPage()) ?>
  <?php echo link_to('&raquo;', 'question/list?page='.$question_pager->getLastPage()) ?>
<?php endif; ?>
</div>

Ce code tire l'avantage des différentes fonctions de l'objet sfPropelPager, parmi lesquelles ->haveToPaginate(), qui retourne true seulement si le nombre de résultats de la requête excède la taille de la page ; ->getPreviousPage(), ->getNextPage() et ->getLastPage(), qui ont des significations évidentes ; ->getLinks(), fournit un tableau des numéros des pages; et ->getCurrentMaxLink(), qui renvoie le dernier numéro de page.

Cette exemple montre également un assistant de liens facile à utiliser : link_to_unless()` fera un simple link_to() si le test donné comme premier argument est false, autrement le texte sera produit sans lien, encapsulé dans un simple <span>.

Avez-vous testé le pagineur ? Vous devriez. La modification n'est pas effective tant que vous ne l'avez pas validée avec vos propres yeux. Pour cela, ouvrez juste le fichier de données de test créé lors du troisième jour, et ajoutez quelques questions pour que la navigation de page apparaisse. Relancez le batch d'importation des données et rafraichissez la page d'accueil. Voila.

paginated list

Ajoutez une règle de routage pour les pages suivantes

Par défaut, les urls des pages ressembleront à :

http://askeet/frontend_dev.php/question/list/page/XX

Tirons profit des règles de routage pour que ces pages soient interprétées :

http://askeet/frontend_dev.php/index/XX


Ouvrez juste le fichier apps/frontend/config/routing.yml et ajoutez au début :
popular_questions: url: /index/:page param: { module: question, action: list }

Tant que l'on y est, ajoutez une autre règle de routage pour la page de login :

login: 
  url:   /login 
  param: { module: user, action: login }

Refactorisation

Modèle

L?action question/list` exécute du code qui est étroitement lié au modèle, c'est pourquoi nous allons le déplacer dans le modèle. Remplacez le code de l'action question/list par :

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

?et ajoutez la fonction suivante dans la classe QuestionPeer.php située dans lib/model :

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


return $pager; }

La même idée appliquée à l'action question/show, écrite hier : l'utilisation des objets Propel pour rechercher une question par son titre dépouillé devrait appartenir au modèle. Donc changez l'action question/show par:

[php]
public function executeShow()
{
  $this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title'));


$this->forward404Unless($this->question); }

Ajoutez à QuestionPeer.php:

[php]
public static function getQuestionFromTitle($title)
{
  $c = new Criteria();
  $c->add(QuestionPeer::STRIPPED_TITLE, $title);

  return self::doSelectOne($c); 
}

Templates

La liste des questions affichées dans question/templates/listSuccess.php sera réutilisée ailleurs dans le future. Donc nous allons déplacer le code du template pour afficher une liste de question dans le fragment _list.php et remplacer le contenu de listSuccess.php par un simple :

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

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

Le contenu du fragment _list.php peut être vu dans l'espace de stockage SVN d'askeet.

A demain

Les formulaires de login et les pagineurs de liste sont utilisés dans la plupart des applications web de nos jours. Vous avez vu aujourd?hui qu'il était facile de développer avec Symfony. Une nouvelle fois, le tutorial finit par un peu de refactorisation. C'est le prix à payer quand vous développez une application petit à petit, sans la concevoir en entier avant. Demain, nous continuerons de travailler sur le processus d'authentification, en limitant l'accès à quelques parties du site aux utilisateurs enregistrés, et nous ferons quelques validations de formulaire pour éviter les soumissions incorrectes.