découverte des formulaires dans symfony 1.1

Publié le par greg

Ajout du 22/08/2008 : depuis la parution de cet article, un livre en ligne écrit par Fabien Potencier a vu le jour. Ce livre constitue évidemment la documentation de référence, cet article n'étant que mon expérience personnelle à l'heure ou la documentation n'existait pas.

Bonjour à tous,
Allez, une bafouille pour vous faire partager ma découverte de l'univers de symfony 1.1. Comme, le nom de cet article l'indique, il s'agit bien d'une découverte. D'autres articles pourront venir confirmer ou infirmer l'expérience que je vous propose ici, c'est à dire grosso modo refaire le sfGuardPlugin mais en symfony 1.1.

préambule

Me voila donc parti pour sécuriser une interface d'administration de site web. L'application symfony est appelée «backoffice» pour changer des trop nombreux «backend». Je créé un module appelé «main» qui va regrouper les actions qui ne sont pas spécifiques à une tâche d'administration comme ... se logger par exemple.

./symfony generate:init-app backoffice
./symfony generate:init-module main

La première chose est de dire à symfony de vérouiller notre application. Cela se situe dans le fichier de conf apps/backoffice/config/security.yml

default:
  is_secure: on

Ensuite, pour permettre à notre administrateur imprudent de se logger, indiquons à symfony quel code exécuter quand quelqu'un de non authentifié essaye d'accéder à du contenu protégé, à savoir, celui qui affiche la page de login. Cela se passe cette fois dans le fichier apps/backoffice/config/settings.yml

all:
  .actions:
    login_module: main
    login_action: login

Enfin, il s'agit cette fois de déprotéger cette action afin que les utilisateurs non authentifiés puissent accéder à cette page et seulement cette page. Créer pour cela un répertoire «config» dans notre module «main» et mettre un fichier security.yml  :

login:
  is_secure: Off

Les formulaires

Dans symfony 1.1, un formulaire est un objet qui regroupe aussi bien les widgets qui permettent d'afficher chacun des champs que les validateurs qui leur sont associés. J'ai décidé de placer ma nouvelle classe dans le sous répertoire «lib» de mon module dans un fichier appelé «loginForm.class.php»

<?php

class loginForm extends sfForm
{
  public function 
configure()
  {
    
$this->setWidgets(array(
      
'login'      => new sfWidgetFormInput(),
      
'password'   => new sfWidgetFormInputPassword(),
    )); 


Cette première partie nécessite déjà quelques explications. Je créé bien ma class loginForm qui étend la classe sfForm généreusement apportée par symfony et je surcharge la méthode configure. Le constructeur aime bien apparemment faire son business sans trop être dérangé et il se débrouille pour appeler la méthode configure au bon moment ... pas de soucis donc. Je peux créer mes 2 widgets :
  • un champs de type texte judicieusement nommé «login»
  • un champs de type password habilement nommé «password»
reste maintenant à décrire les validateurs associés à nos 2 champs. Les validateurs prennent au moins un paramètre, mais le deuxième est intéressant :
  • la liste des options du validateur
  • la liste optionnelle des messages pouvant être retournés par notre validateurs au lieu de ceux par défaut.
<?php
class loginForm extends sfForm
{
  public function 
configure()
  {
    
$this->setWidgets(array(
      
'login'      => new sfWidgetFormInput(),
      
'password'   => new sfWidgetFormInputPassword(),
    ));

    
$this->setValidators(array(
      
'login'      => new sfValidatorString(
        array(
          
'required' => true,
          
'trim' => true
        
),
        array(
          
'required' => 'Veuillez rentrer un identifiant',
        )),
      
'password'   => new sfValidatorString(
        array(
          
'required' => true,
          
'trim' => true
        
),
        array(
          
'required' => 'Veuillez rentrer un mot de passe'
        
)),
    ));
    
$this->widgetSchema->setNameFormat('login[%s]');

    
$this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);
  }


Voila, notre formulaire est prêt ! Il ne reste plus qu'à écrire l'action qui va nous instancier tout ça et le passer au template. L'action va être dans le fichier d'actions apps/backoffice/modules/main/actions/actions.class.php

  public function executeLogin()
  {
    
sfForm::disableCSRFProtection();
    
$this->login_form = new loginForm
();
  }


J'ai été obligé d'enlever la protection CSRF qui me posait problème pour l'instant. Le template correspondant va vous étonner par sa consision :

<form action="<?php echo url_for('main/login'?>" method="post">
<table>
<tr><td colspan="2" align="center">Veuillez vous identifier</td></tr>
<?php echo $login_form->render() ?>
<tr><td colspan="2" align="center">
<input type="submit" name="submit" value="ok" />
</td></tr>
</table>


Voila un des premiers points intéressants pour un handicapé du template comme moi : l'objet formulaire est capable de générer le contenu du formulaire juste en invoquand la méthode render. Si ce n'est pas ce que l'on désire, il est apparemment possible de tuner le bazarre pour s'en sortir, il est également possible de récupérer les widgets et de les générer un par un nous même. Pour l'instant cela me convient très bien. No
tez que si vous jetez un coup d'oeil au code HTML généré, toutes les variables du formulaire sont placées dans un tableau nommé «login». C'est grâce à la directive

$this->widgetSchema->setNameFormat('login[%s]');

J'ai décidé de réutiliser l'action «login» pour valider mon formulaire. La méthode avec laquelle on appelle cette action va nous permettre de distinguer ceux qui veulent juste afficher le formulaire de ceux qui cherchent à valider une authentification. Le fait d'utiliser la même action me permet de ne pas avoir grand chose à faire pour réafficher le formulaire de login si l'authentification échoue (hé oui, pas bête). Me voila reparti dans l'action «executeLogin» :

  public function executeLogin()
  {
    
sfForm::disableCSRFProtection();
    
$this->login_form = new loginForm();

    if (
$this->getRequest()->getMethod() != sfRequest::POST)
    {
      return 
sfView::SUCCESS;
    }

Ok, du coup ça, c'est fait, il ne me reste plus qu'à valider mon formulaire et vous allez voir la grande magie des formulaires dans symfony 1.1 :

    $this->login_form->bind($this->getRequestParameter('login'));

    if (!
$this->login_form->isValid())
    {
      return 
sfView::SUCCESS
;
    }


Épatant non ? la méthode bind permet d'attribuer les valeurs nommées de notre tableau associatif «login» aux validateurs de notre formulaire puis de déclencher ceux ci ! Plus fort encore, les messages d'erreurs sont directement gérés puisque j'utilise la méthode «render» de l'objet sfForm, il ne me reste plus qu'à tester si tout c'est bien passé à l'aide de la méthode «isValid» et si ce n'est pas le cas, de réafficher la template de login.

Je vois les plus sceptiques d'entres vous plisser les yeux en me regardant l'air un peu amusé en se disant «c'est bien beau tout ça, mais l'authentification dans l'histoire, elle est ou ?» Et bien, elle n'est tout simplement pas faite. la seule chose que j'ai fait jusque là est de vérifier que j'avais quelque chose dans les champs.

L'authentification est en effet quelque part lié à mon formulaire. Je dois renvoyer sur la page de login si celle ci échoue avec un message d'erreur. Il s'agit donc là aussi d'un validateur. Mais ce validateur ne peut pas nous être fourni avec symfony, je vais donc le créer dans le même répertoire lib que le formulaire apps/backoffice/modules/main/lib/loginValidator.class.php

<?php

class loginValidator extends sfValidatorBase
{
  public function 
configure($options = array(), $messages = array())
  {
    
$this->setMessage('invalid''Identification erronée');
  }

  public function 
doClean($values)
  {
    if (
$user AdminPeer::getByLogin($values['login']))
    {
      
$salt substr(strtoupper($values['login']), 02);

      if (
$user->getPassword() == crypt($values['password'], $salt))
      {
        
$values['user'] = $user;
        return 
$values;
      }
    }
    throw new 
sfValidatorError($this'invalid');
  }


Quelques explications encore. Dans cet exemple, je me moque des valeurs passées en paramètre, je force la configuration de mon validateur avec le message «identification erronée». C'est pas terrible mais c'est pour tester.
Je lui fournis ensuite la méthode «doClean» qui permet d'indiquer comment fonctionne le validateur.

Je regarde si un utilisateur correspondant au login existe dans la base et si c'est le cas je vérifie que le hash du password donnée correspond au hash présent dans la base (je vous fais grâce du grain de sel utilisé pour le cryptage). Si le mot de passe correspond alors, j'ajoute l'objet utilisateur dans le tableau «values».

Le «cleanage»

Cela mérite là encore quelques explications : lorsque je déclenche la validation, symfony considère les valeurs qu'on lui passe comme étant peu fiables (on dit qu'elles sont teintées). La validation permet de vérifier que tout correspond à nos attentes d'une part mais aussi de nettoyer les valeurs rentrées par un utilisateur ! Notre objet sfForm utilise pour cela un tableau associatif que chaque validateur remplit avec les valeurs nettoyées qui pourront être utilisées dans notre code en toute tranquilité plus tard. Dans cet exemple, je crée la clé «user» pour y placer l'objet user correspondant à l'utilisateur qui vient de s'authentifier avec succès.

Si le login/mot de passe ne correspondent pas à une entrée de ma base, j'envoie alors une exception de type «sfValidatorError» qui prend en paramètre le type de problème concerné (ici «invalid») ce qui permettra à notre formulaire de savoir quel message afficher à l'utilisateur.

Dernier petit souci, ou déclarer notre validateur dans le formulaire ? Notre validateur ne correspond en effet à aucun des champs de notre formulaire mais il s'applique de façons plus globale...

Les formulaires possèdent 3 endroits ou l'on peut placer un ou un ensemble de validateurs :

  • preValidator : exécuté(s) avant les validateurs des champs
  • les champs : chaque champs possède un ou plusieurs validateurs pour le valider/cleaner
  • postValidator : executé(s) après les validateurs des champs.
Dans ce cas, j'ai décidé de placer le validateur après pour pouvoir utiliser les valeurs «cleanées» de login et de password. J'ai donc à ajouter dans la méthode «configure» de mon formulaire la ligne suivante :

$this->getValidatorSchema()->setPostValidator(new loginValidator());

La suite de notre controlleur executeLogin est alors très simple, si le formulaire est valide, alors il ne me reste qu'à récupérer l'objet utilisateur stocké dans les valeurs cleanées pour l'authentifier et l'envoyer le contenu sécurisé. Ce qui nous donne au final :

  public function executeLogin()
  {
    
sfForm::disableCSRFProtection();
    
$this->login_form = new loginForm();

    if (
$this->getRequest()->getMethod() != sfRequest::POST)
    {
      return 
sfView::SUCCESS;
    }

    
$this->login_form->bind($this->getRequestParameter('login'));

    if (!
$this->login_form->isValid())
    {
      return 
sfView::SUCCESS;
    }

    
$user $this->login_form->getValue('user');

    
$sf_user $this->getUser();
    
$sf_user->setAuthenticated(true);
    
$sf_user->setAttribute('id'$user->getId());
    if (
$user->getIsAdmin())
    {
      
$sf_user->addCredentials('admin');
    }

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


Seule petite chose agaçante que je n'ai pas réussi à résoudre pour l'instant, je déclenche l'authentification même si les validateurs de champs n'ont pas validé le formulaire (mot de passe vide par exemple). Cela occasionne une requête sql inutile. J'ai vu qu'il existait des validateurs ET et OU logiques ... on va bien voir.

Publié dans symfony

Pour être informé des derniers articles, inscrivez vous :
Commenter cet article
W
Excellente présentation ! C'est très bien expliqué et c'est plutôt rare ! Merci beaucoup :)
Répondre
G
<br /> Merci pour le compliment.<br /> <br /> <br />
P
Petite erreur login:   is_secured: Off est à corrigé par : login:   is_secure: Off
Répondre
G
<br /> Merci, voila qui est corrigé. Le fait est que l'erreur de syntaxe sur cette directive n'a pas empéché symfony de fonctionner. Ce qui signifie que les options «login_module» et «login_action»<br /> neutralisent «is_secure» sur l'action concernée. À vérifier dans le code de symfony.<br /> <br /> <br />