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 :

<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'outils 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 :

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.

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 de helper. Dans le répertoire askeet/apps/frontend/modules/user/templates/, créez le template loginSuccess.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. Le helper form_tag() ouvre un formulaire avec le comportement POST par défaut, et pointe vers l'action passée en argument. Le helper 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.

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.

Traiter l'envoi du formulaire Login

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

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'id et 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.

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.

C'est le but de ces deux lignes :

$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 :

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.

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.

loggedlogged_day5.gif
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

Nous avons vu que l'action list du module question était tout à fait restreint :

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é :

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.

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 :

$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.

Modifier le template listSuccess.php

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

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

par

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

<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 helper 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.

pager_day5.gif

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 :

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 :

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:

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 :

<h1>popular questions</h1>

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

Validation du formulaire de connexion

Fichier de validation

Le formulaire de connexion dispose de deux champs : nickname et password Mais que doit-il se passer si l'utilisateur soumets des données incorrectes ? Pour gérer ce cas de figure, vous devez créer un fichier login.yml dans le répertoire /frontend/modules/user/validate (Notez que login est le nom de l'action dont le formulaire doit être validé). Ajouter le contenu suivant à ce fichier :

methods:
  post: [nickname, password]
  names:
    nickname:
      required: true
      required_msg: your nickname is required
      validators: nicknameValidator
    password:
      required: true
      required_msg: your password is required
      nicknameValidator:
        class: sfStringValidator
        param:
          min: 5
          min_error: nickname must be 5 or more characters

Premièrement, sous l'entête methods, vous devez lister les champs à valider pour la méthode du formulaire (nous ne définissons ici que la méthode POST car les données GET ne sont utilisées que pour afficher le formulaire et n'ont pas besoin d'être validée). Puis, sous l'entête names, les critères de validation de chaque champs à valider sont listés, suivi du message d'erreur correspondant. Eventuellement, à l'image du champs nickname, il est possible de définir des règles de validation spécifiques, sous l'entête correspondant au nom de cette règle. Dans cet exemple, le sfStringValidator est un validateur proposé par symfoy qui valide le format d'une chaîne de caractères (les validateurs par défaut proposés par Symfony sont détaillés dans le chapitre exposant comment valider un formulaire dans le livre Symfony)

Gestionnaire d'erreur

Maintenant, qu'est-il censé se passer si un utilisateur valide des données invalides ? Les conditions écrites dans le fichier login.yml ne seront pas satisfaites, et le contrôleur de symfony va transmettre la requête à la méthode handleErrorLogin de la classe userActions - à l'image de la méthode executeLogin(), tel que défini dans l'argument de la balise form_tag. Si cette méthode n'existe pas, le comportement par défaut est d'afficher le template loginError.php. La raison de ce comportement est que la méthode handleError() d'origine retourne :

  public function handleError() { return sfView::ERROR; }

Nous avons un nouveau gabarit à écrire. Or, il serait préférable de ré-afficher le formulaire de connexion, avec les messages d'erreur affichés près des champs invalides. Donc, nous allons modifier le comportement de l'erreur de connexion pour afficher le template loginSuccess.php en cas d'erreur.

  public function handleErrorLogin() { return sfView::SUCCESS; }

Les helpers d'affichage des erreurs

Il est désormais temps d'afficher les erreurs dans le template loginSuccess.php. Pour cela, nous allons utiliser le helper form_error() de la librairie de helpers Validation, prévue à cet effet. Remplacez les deux div form_row du templates par le code suivant :

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

Le helper form_helper() à pour comportement d'afficher le message d'erreur défini dans le fichier login.yml si une erreur est détectée dans le champ défini en paramètre.

Il est temps de tester la validation du formulaire en essayant de saisir un valeur de moins de 5 caractères dans le champs nickname, ou alors en ne remplissant pas l'un des champs. Le message d'erreur apparaît juste au dessus du champ concerné :

Erreur dans le formulaire de connexion

Le mot de passe est désormais obligatoire, mais aucun mot de passe n'est présent en base de données ! Pas d'inquiétude, quel que soit le mot de passe que vous saisissiez, la connexion fonctionnera, pour un peu que le login existe en base de données. Ce n'est pas très sécurisé, n'est-ce pas ?

Style des message d'erreur

Si vous avez testé le formulaire et obtenu une erreur, vous avez sûrement remarqué que cette erreur n'est pas affichée comme dans l'image présentée. La raison est que nous avons défini le style de la classe .form_error_class (dans le fichier web/main.css), qui est la classe par défaut associée aux champs générés par le helper form_error():

  .form_error { padding-left: 85px; color: #d8732f; }

Authentifier un utilisateur

Validateur personnalisé

Vous rappelez vous la vérification d'hier, concernant l'existence du nickname saisie dans l'action login ? Cette vérification ressemble à la validation d'un formulaire. Ce code pourrait être placé en dehors de l'action et inclus dans un validateur personnalisé. Vous pensez que c'est compliqué ? On ne peut pas dire ça. Editez le fichier de validation login.yml et modifiez son contenu de la sorte :

...
  names:
    nickname:
      required: true
      required_msg: your nickname is required
      validators: [nicknameValidator, userValidator]
...
  userValidator:
    class: myLoginValidator
    param:
    password: password
    login_error: this account does not exist or you entered a wrong password

Nous venons d'ajouter un nouveau validateur au champ nickname, basé sur la classe myLoginValidator. Cette classe n'existe pas encore, mais nous savons qu'il lui faudra le password pour complètement valider l'authentification d'un utilisateur, nous le transmettons donc en paramètre sous le label password

Stockage du mot de passe

Mais attendez une minute. Dans notre modèle de données, autant que dans les données de test, aucun mot de passe n'est défini. Il est donc temps de le définir. Mais vous devez savoir que stocker un mot de passe en clair, dans une base de données, est une très mauvaise idée pour des raisons de sécurité. Nous allons donc stocker une version hashé par sha1 du mot de passe, ainsi que la clé aléatoire utilisée pour le hasher. Si vous n'êtes pas familier avec ce principe de 'sel', consultez les techniques de crackage de mot de passe.

Ouvrez le fichier schema.xml et ajouter les colonnes suivantes dans la table User :

<column name="email" type="varchar" size="100" />
<column name="sha1_password" type="varchar" size="40" />
<column name="salt" type="varchar" size="32" />

Re-génerez le modèle de données Propel via la commande symfony propel-build-all. Vous auriez également pu ajouter les 2 colonnes à la base de données, soit manuellement, soit en utilisant le fichier lib.model.schema.sql généré à la suite de l'exécution de la commande symfony propel-build-sql. Editez maintenant le fichier askeet/lib/model/User.php et ajouter cette méthode setPassword() :

public function setPassword($password)
{
  $salt = md5(rand(100000, 999999).$this->getNickname().$this->getEmail());
  $this->setSalt($salt);
  $this->setSha1Password(sha1($salt.$password));
}

Cette fonction simule un stockage direct du mot de passe, mais effectue réellement la génération d'une clé aléatoire salt (une chaîne de 32 caractères hashé) et le mot de passe hashé (une chaîne de 40 caractères).

Ajouter le mot de passe dans les données de test

Il est temps d'ajouter un mot de passe et un email aux données de test au fichier de données. Ouvrez et modifiez le fichier askeet/data/fixtures/test_data.yml comme suit :

User:
  ...
  fabien:
    nickname: fabpot
    first_name: Fabien
    last_name: Potencier
    password: symfony
    email: fp@example.com
  francois:
    nickname: francoisz
    first_name: François
    last_name: Zaninotto
    password: adventcal
    email: fz@example.com

Comme la méthode setPassword() a été définie dans la classe User, l'objet sfPropelData remplira correctement les nouvelles colonnes sha1_password et salt définies dans le schéma lors de l'appel au script de population des données :

$ php batch/load_data.php

NOTE Notez que le l'objet sfPropelData est capable d'utiliser des méthodes qui ne correspondent pas à des colonnes réelles du modèle de données (Et voilà, nous dépassons les capacités du SQL traditionnel !)

Si vous vous demandez comment c'est possible, vous n'avez qu'a jetez un coup d'oeil dans le chapitre de population de base de données du livre symfony

Note: Le fait de ne pas définir de mot de passe pour l'utilisateur 'Anonymous Coward' permet d'interdire la connexion avec ce compte. De plus, ne tentez pas de vous connecter sur le site public Askeet avec les mots de passes donnés en exemple, il est évident que nous ne donnons pas les vrais mots de passe.

Validateur personnalisés

Maintenant, il est temps de définir le validateur myLoginValidator. Vous pouvez créer cette classe myLoginValidator.class.php dans n'importe quel dossier lib/ accessible par le module (c'est à dire, pour le cas présent, dans askeet/lib/, ou dans askeet/apps/frontend/lib/, ou encore dans askeet/apps/frontend/modules/user/lib/). Dans le cas présent, le validateur est considéré comme pouvant être utile à toute l'application, et sera donc créé dans le dossier askeet/apps/frontend/lib/ :

<?php
class myLoginValidator extends sfValidator
  {
    public function initialize($context, $parameters = null)
    {
      // initialize parent
      parent::initialize($context);
      // set defaults
      $this->setParameter('login_error', 'Invalid input');
      $this->getParameterHolder()->add($parameters);
      return true;
    }
    public function execute(&$value, &$error)
    {
      $password_param = $this->getParameter('password');
      $password = $this->getContext()->getRequest()->getParameter($password_param);
      $login = $value;
      // anonymous is not a real user
      if ($login == 'anonymous')
      {
         $error = $this->getParameter('login_error');
         return false;
      }
      $c = new Criteria();
      $c->add(UserPeer::NICKNAME, $login);
      $user = UserPeer::doSelectOne($c);
      // nickname exists?
      if ($user)
      {
         // password is OK?
         if (sha1($user->getSalt().$password) == $user->getSha1Password())
         {
            $this->getContext()->getUser()->setAuthenticated(true);
            $this->getContext()->getUser()->addCredential('subscriber');
            $this->getContext()->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber');
            $this->getContext()->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber');
            return true;
         }
      }
      $error = $this->getParameter('login_error');
      return false;
   }
}

Lorsque le validateur est appelé - après la validation du formulaire de connexion - la méthode initialize() est tout d'abord appelée. Cette méthode initialise la valeur par défaut du message login_error ('Invalid input') et ajoute les paramètres du validateur (ceux qui se trouvent sous l'entête param: dans le fichier login.yml) dans l'objet de collecte des paramètres.

Puis la méthode execute() est ... exécutée. La variable $password_param contient la valeur définie dans le fichier login.yml sous l'entête password. Cette variable est utilisée pour récupérer une valeur dans les paramètres transmis. Donc, la variable $password contient le mot de passe saisi par l'utilisateur. La variable $value contient la valeur du champ courant - et la classe myLoginValidator est appelée pour le champ nickname. La variable $login contient donc le nickname saisi par l'utilisateur. Désormais, nous avons toutes les données nécessaires pour que le validateur puisse valider un utilisateur.

Ce code remplace un code existant de l'action login. En outre, le test de validité du mot de passe (qui était précédement toujours vrai) est implémenté : un hashage du mot de passe saisi par l'utilisateur (en utilisant le sel stocké en base de données) est comparé au mot de passe hashé de l'utilisateur.

Si le login et le mot de passe sont correct, le validateur retourne true et l'action associée au formulaire (executeLogin()) sera exécutée. Sinon, il retourne false et c'est la fonction handleErrorLogin() qui sera exécutée