Dimanche 22 juin 2008
Bonjour à tous,

Petite bafouille ce soir, une plus longue viendra, il me faut le temps de l'écrire. Aujourd'hui le sujet est une question qu'on m'a posée il n'y a pas longtemps lors de l'une des mes formations symfony : est il possible d'envoyer un mail lorsqu'une erreur 500 se produit sur le site de production ?

La réponse est non mais oui ... en partie. Je m'explique : il est possible d'attraper les exceptions crées dans symfony et d'envoyer un mail. Mais en aucun cas les erreurs PHP ne peuvent être attrapées ... on ne peut rien faire dans ce cas.

Comment attraper les exceptions dans symfony ?

Très facile, le framework symfony 1.0 est composé de couches appelées filtres. Lors de l'exécution, chacune des requêtes passe au travers des filtres et une fois la réponse calculée, repasse dans l'autre sens. Pour plus de précisions sur ce système de filtres, je vous propose la lecture du livre à ce sujet.

 Il est évidemment possible d'ajouter un filtre dans la pile, comme par exemple un filtre qui attraperait les erreurs des filtres «en dessous» c'est à dire là ou vous auriez placé le code de vos actions et templates, utilisé votre modèle et vos bibliothèques.

Le code du filtre est très simple, il suffit de faire un try catch sur l'exécution de la pile de filtres du «dessous». En cas d'exception, envoyer un mail et propager l'exception pour que symfony s'en occupe. Dans le répertoire lib de votre application, placer un fichier catchErrorFilter.class.php qui contient le code suivant (mis à jour suite à la pertinente remarque  de Niko dans les commentaires ):

<?php

class catchErrorFilter extends sfFilter
{
  public function 
execute ($filterChain)
  {
      
// Execute next filter in the chain
    
try
    {
      
$filterChain->execute();
    }
    
// Si l'exception est due a un redirect
    // ne pas envoyer de notification
    
catch (sfStopException $e)
    {
      throw 
$e
# propager l'exception
    
}
    
// Toute autre exception est reportee
    
catch (Exception $e)
    {
      
// envoyer un mail ici

      
throw $e# propager l'
exception
    }
  }
}


Reste à configurer symfony pour enregistrer votre nouveau filtre et le placer dans la pile des filtres. Le fichier filters.yml dans le répertoire de configuration de votre application est fait pour cela, il devrait ressembler à cela :

rendering: ~
security:  ~

# insert your own filters here

catch_error:
  class: catchErrorFilter
  param:
    condition: %APP_CATCH_ERRORS%

cache:     ~
common:    ~
execution: ~


On enregistre le filtre et on demande à symfony de ne l'exécuter qu'à condition de que la variable de configuration «APP_CATCH_ERROR» soit vraie. Cela va nous permettre de n'utiliser ce filtre qu'en environnement de production et éviter ainsi de recevoir des mails lors des erreurs (plus fréquentes) sur les environnements de développement. Il convient dons de placer cette directive dans le fichier de conf app.yml :

all:
  catch_errors: off

prod:
  catch_errors: on


Voila, le tour est joué ...

par greg publié dans : symfony
ajouter un commentaire commentaires (4)    recommander
Jeudi 8 mai 2008
Bonjour à tous, une brève de comptoir aujourd'hui. Une fois que vous aurez ces raccourcis dans les doigts vous ne pourrez plus vous en passer. En général la combinaison ALT est utilisée pour les actions sur les mots d'une ligne et CTRL pour les commandes globales.
Je vous ai mis les indispensables et les quelques coups de coeurs que j'ai eu en écrivant cet article.

  • Déplacements :
    • ctrl a : début de ligne
    • ctrl e (end) : fin de ligne
    • alt f (forward) : avance d'un mot (<-- pratique)
    • alt b (backward) : recule d'un mot (<-- pratique)
    • ctrl x ctrl x (x switch) : alterne le curser avec son ancienne position (<-- coup de coeur)
  • Couper coller
    • alt backspace : couper le mot avant
    • alt d : couper le mot après
    • ctrl k : coupe du curser à la fin de la ligne
    • ctrl u : coupe du curser au début de la ligne (<--pratique quand vous voulez annuler la saisie d'un mot de passe par exemple)
    • ctrl y (yank) : coller
  • historique
    • flèche haut : remonter d'une ligne dans l'historique (ctrl p (previous))
    • flèche bas : descendre d'une ligne dans l'historique (ctrl n (next))
    • (vous pouvez vous ballader dans l'historique avec les flèches sans perdre votre saisie en cours...)
    • crtl r (research) : chercher les occurences d'une chaine de caractère dans l'historique (ctrl r pour passer à l'occurence d'après ctrl s pour revenir à l'occurence d'avant). (<-- indispensable)
    • ctrl o : valide la ligne en cours et vous replace sur la ligne d'après dans l'historique (utile pour rejouer des pans de votre historique de shell avec ctrl r)
  • Éditer une ligne de commandes
    • ctrl x ctrl e : édite la ligne courante dans Vi (variable $EDITOR du shell) (<-- coup de coeur)
    • alt u : transforme le mot courant en majuscules à partir du curser et avance d'un mot
    • alt l : transforme le mot courant en minuscules à partir du curser et avance d'un mot
    • alt c : met la lettre sous le curser en majuscule et avance d'un mot
    • alt r : annule les changements et remet la ligne telle qu'elle était dans l'historique (<-- pratique)
    • tab : complète le mot en cours suivant le contexte (<-- indispensable)
Pour ceux que ça intéresse, la page de man de bash est évidemment LA référence pour ce genre de choses, si vous voulez atteindre directement la section de ces raccourcis, cherchez la chaine «beginning-of-line».

par greg publié dans : bash
ajouter un commentaire commentaires (3)    recommander
Samedi 12 avril 2008
Pour les habitués de postgresql et de de ses fonctionnalités, l'utilisation de propel et de sa syntaxe limitée dans le schema.yml est une véritable écorchure mentale. Point de contraintes, point de triggers, pas d'index de type hors de celui par défaut bref...

Imaginons que nous voulons proposer à nos utilisateurs de s'inscrire à des stages. Il nous faut stocker les informations relatives à ces stages. Imaginons le schema.yml suivant :

  stage:
    reference:    { type: varchar(255), primaryKey: true }

    title:              { type: varchar(255), required: true }
    description:        { type: longvarchar, required: true }
    date:               { type: timestamp, required: true }
    limit_date:         { type: timestamp, required: true }
    limit_subscription: { type: integer, required: true, default: 0 }
    price:                { type: numeric, size: 6, scale: 2, required: true, default: 0 }
    duration:           { type: integer, required: true }

  stagiaire:
    id:
    stage_id:           { type: integer, required: true, foreignTable: stage, foreignReference: id, onDelete: cascade }
    firstname:          { type: varchar(255), required: true }
    lastname:           { type: varchar(255), required: true }
    email:              { type: varchar(255), required: true }
    phone:              { type: varchar(255), required: true }
    confirmed:          { type: boolean, required: true, default: false }
    created_at:

Notre stage possède une référence qui est la clé primaire des enregistrements. Cette clé primaire est automatiquement générée par une séquence qui se base sur des enregistrements de tables tières.

Dans l'équipe de développement, la personne responsable de la base de données va immédiatement pouvoir protéger la base contre les «étourderies» des développeurs à l'aide de l'astuce fort  bien décrite ici.

Pourquoi utiliser des contraintes fortes dans la base de données ?
- les données du système d'information sont le bien le plus précieux d'une entreprise. La cohérence des données n'est pas une option.
- il convient de nous assurer que les mécanismes de validation mis en place par les développeurs fonctionnent et que les données échangées sont en adéquations avec les données attendues en base.


Les contraintes Postgresql

Gardons à l'oeil la documentation des contraintes et commençons notre travail en créant  un fichier constraintes.sql dans le répertoire data/sql :

La première chose est de fixer la valeur de la clé primaire de la table stages qui utilise la séquence stage_seq :

ALTER TABLE stage ALTER reference SET DEFAULT nextval('stage_seq');

Ensuite, nous avons les contraintes suivantes :
- Le prix doit être positif
- La date limite d'inscription ne doit pas être après la date du stage

Pour le prix la contrainte est relativement simple. Nous allons la nommer positive_price. L'avantage de nommer les contraintes est de pouvoir les modifier après comme des objets nommés d'une part et d'autre part, dans le cas du refus d'une insertion pour violation de contrainte, postgresql nous donne le nom de la contrainte incriminée.

ALTER TABLE stage ADD CONSTRAINT positive_price CHECK (price > 0);

De la même façon, il est possible de placer des contraintes qui vérifient plusieurs colonnes entre elles, ce que l'on va utiliser pour notre histoire de dates :

ALTER TABLE stage ADD CONSTRAINT consistant_dates CHECK (limit_date < date );

Passons maintenant à la table stagiaire :
Nous voulons que le champs email soit bien un email. Nous allons utiliser le mécanisme d'expression régulières de postgresql pour décrire notre contrainte :

ALTER TABLE stage ADD CONSTRAINT valid_email CHECK (email ~*  '^[a-z][a-z0-9_.-]*@[a-z][a-z0-9_.-]*$';

Notez que nous pouvons également utiliser ce fichier pour définir des indexes supplémentaires.

Afin que Propel lise notre fichier après l'import de la structure en base, éditons le fichier data/sql/sqldb.map pour rajouter notre fichier de contraintes :

# Sqlfile -> Database map
lib.model.schema.sql=propel
constraints.sql=propel

Ainsi à chaque propel-build-all, symfony va automatiquement intégrer notre fichier de contraintes.
par greg publié dans : symfony
ajouter un commentaire commentaires (0)    recommander
Dimanche 6 avril 2008
Suite de mon exploration des formulaires avec symfony 1.1, je vais aujourd'hui tenter de défricher les formulaires autogénérés par Propel, les sfFormPropel.

Cas d'étude : imaginons que nous sommes en train de faire une interface de saisie de commentaires dans quelque chose qui pourrait s'apparenter à un blog. Nous allons essayer d'utiliser au mieux les formulaires propel pour cela.
Nous avons évidemment dans le schema.yml un objet «comment» :


blog_comment:
  id:
     blog_post_id:
     created_at:
     author_name:            { type: varchar(255), required: true }
     author_email:            { type: varchar(255), required: true }
     title:                          { type: varchar(255), required: true }
     content:                    { type: longvarchar, required: true }


Si vous faites un «symfony propel:build-all» alors vous ne remarquerez pas quà la différence de symfony 1.0, celui-ci vous fait dans le lot un «symfony propel:build-forms». Qu'est ce donc que cela. C'est tout simple, propel vous génère un fichier de description de formulaire adapaté à votre schema dans le dossier «lib/form». Vous remarquerez qu'à l'instar des fichiers du modèle, il existe un dossier «lib/forms/base» qui contient les classes qui seront regénérées à chaque build-all et que les fichiers de «lib/forms» vous sont destinés.

Reste maintenant dans l'action qui affiche un article de blog à initialiser notre formulaire :

  public function executeShow()
  {
      
$this->post BlogPostPeer::retrieveByPK($this->getRequestParameter('blog_post_id'));
      
$this->forward404Unless($this->post);

      
$this->comment_form = new BlogCommentForm();
  }

Bien que propel nous ait maché le travail, le résultat n'est pas forcément celui attendu :

 Le blog post id ne devrait pas pouvoir être choisi car fixé par défaut au post affiché sur la page. (note au passage, il est nécessaire d'avoir déclaré une méthode __toString à l'objet BlogPost pour générer le drop down menu).

Le champs created_at ne devrait pas non plus pouvoir être modifié car fixé par défaut par symfony au moment de la création de l'enregistrement dans la base.

Il va donc falloir configurer notre formulaire. Le but étant de: - changer le champs blog_post_id en champs hidden avec une valeur fixée par défaut
- ne pas s'occuper ni valider le champs created_at (en gros, l'enlever du formulaire).

Pour cela, il est nécessaire de récupérer le widgetSchema de notre formulaire pour le changer.




Notre action devient alors :

  public function executeShow()
  {
    
$this->post BlogPostPeer::retrieveByPK($this->getRequestParameter('blog_post_id'));
    
$this->forward404Unless($this->post);

    
$this->comment_form = new BlogCommentForm();
    
$widget_schema $this->comment_form->getWidgetSchema();

    
$widget_schema['blog_post_id'] = new sfWidgetFormInputHidden();
    
$this->comment_form->setDefault("blog_post_id"$this->getRequestParameter('id'));

    unset(
$widget_schema['created_at']);
  }


Vous remarquez que nous utilisons l'implémentation array_access de l'objet widgetSchema. Nous pouvons donc accéder aux différents widgets de la même façon que les éléments d'un tableau. Notre formulaire prend alors bien la tête de ce que nous attendons :

Le formulaire passé au peigne fin par la web developer toolbar possède bien juste les champs dont nous avons besoin et notemment le blog_post_id en champs hidden.

Maintenant que ce problème est (pour l'instant) réglé, penchons nous sur la validation de ce formulaire.

Nous allons utiliser la même approche que la dernière fois le formulaire postant par défaut sur la dernière action appelée.


  public function executeShow()
  {
    
$this->comment_form = new BlogCommentForm();
    
$widget_schema $this->comment_form->getWidgetSchema();
    unset(
$widget_schema['created_at']);

    if (
$this->getRequest()->getMethod() == sfRequest::POST)
    {
      
$blog_comment $this->getRequestParameter('blog_comment');

      
$this->comment_form->BindAndSave($blog_comment);
      
$this->redirectIf($this->comment_form->isValid(), 'post/show?blog_post_id='.$blog_comment['blog_post_id']);
      
$this->post BlogPostPeer::retrieveByPK($blog_comment['blog_post_id']);
    }
    else
    {
      
$this->post BlogPostPeer::retrieveByPK($this->getRequestParameter('blog_post_id'));
    }
    
$this->forward404Unless($this->post);

    
$widget_schema['blog_post_id'] = new sfWidgetFormInputHidden();
    
$this->comment_form->setDefault("blog_post_id"$this->post->getId());

  }


Quelques explications :

unset(
$widget_schema['created_at']);


Je suis obligé dès l'instanciation de mon formulaire de me débarasser de ce champs pour qu'il ne soit ni affiché ni validé. Nous testons ensuite si nous sommes dans une requête POST. Si c'est le cas alors je vais valider mon formulaire :

$this->comment_form->BindAndSave($blog_comment);

Vous remarquez que pour le coup rien de plus simple : cette action :
- clean les entrées
- valide les données
- sauvegarde le nouvel objet commentaire si le formulaire est valide.

Ne me reste qu'à rediriger l'utilisateur si son formulaire est valide :
     $this->redirectIf($this->comment_form->isValid(), 'post/show?blog_post_id='.$blog_comment['blog_post_id']);

Si ce n'est pas le cas alors je prépare de nouveau l'environnement de mon template en utilisant la valeur passée dans le formulaire pour retrouver le BlogPost courant.

Testons notre formulaire, nous découvrerons que lorsque je rentre «bleuargh» dans le champs email, mon formulaire est néanmoins validé ... à l'arnaque, au voleur !

Que se passe-t-il, c'est tout simple, notre formulaire ignore que cette valeur est une adresse email. Se basant sur le schema.yml il sait juste que c'est un champs text qui valide «bleuargh» sans aucun soucis. Il convient de changer le validateur associé à ce champs au moment de la validation.

    $validator_schema $this->getValidatorSchema();
    
$validator_schema['email'] = new sfValidatorEmail();

Là encore nous utilisons l'implémentation array_access de l'objet validator_schema pour accéder à notre validateur. Rien de plus simple alors de lui préciser sfValidatorEmail.

Refactorisons notre code.

Tous ce code de modification de notre formulaire peut sans problème être factorisé dans une classe héritant de BlogCommentForm. Je ne sais pas encore si utiliser «/lib/forms/BlogCommentForm.php» est une bonne idée ou non. J'ai plus plus l'intuition que ce formulaire est utile dans notre module et de laisser l'objet principal inchangé. J'ai donc opté pour utiliser le lib/ de mon module pour surcharger cet objet.

apps/frontend/modules/post/lib/commentForm.class.php
<?php

class commentForm extends BlogCommentForm
{
  public function 
configure()
  {
    
parent::configure();

    
$widget_schema $this->getWidgetSchema();
    
$widget_schema['blog_post_id'] = new sfWidgetFormInputHidden();
    unset(
$widget_schema['created_at']);

    
$validator_schema $this->getValidatorSchema();
    
$validator_schema['email'] = new sfValidatorEmail();
  }
}


Voila mon action qui devient plus claire, plus lisible :

apps/frontend/modules/post/actions/actions.class.php
  public function executeShow()
  {
    
$this->comment_form = new commentForm();

    if (
$this->getRequest()->getMethod() == sfRequest::POST)
    {
      
$blog_comment $this->getRequestParameter('blog_comment');

      
$this->comment_form->BindAndSave($blog_comment);
      
$this->redirectIf($this->comment_form->isValid(), 'post/show?blog_post_id='.$blog_comment['blog_post_id']);
      
$this->post BlogPostPeer::retrieveByPK($blog_comment['blog_post_id']);
    }
    else
    {
      
$this->post BlogPostPeer::retrieveByPK($this->getRequestParameter('blog_post_id'));
    }
    
$this->forward404Unless($this->post);

    
$this->comment_form->setDefault("blog_post_id"$this->post->getId());
  }


par greg publié dans : symfony
ajouter un commentaire commentaires (3)    recommander
Mercredi 26 mars 2008
Bon après l'article fleuve d'hier, je vais faire (relativement) court aujourd'hui, je vais vous parler d'un logiciel libre qui me tient particulièrement à coeur dont le nom commence par V et finit par M (buzze à ma gauche) : vim !! et le plugin Project.

J'ai lu cet excellent article sur Project et je n'ai pas pu m'empêcher d'essayer ... et là, ma vie a (une fois de plus) changé. Voici l'outil que j'attendais depuis que j'utilise Vim pour coder en PHP : un explorateur de projet. Son principe de fonctionnement est tellement simple que cela en est un peu déroutant au début... je m'explique.

vm01.png
Project sur la gauche avec le modèle à portée de souris

Project est un plugin qui gère tout bêtement un fichier texte. Ce fichier est par défaut à la racine de votre home et possède tous les projets sur lesquels vous travaillez. Lorsqu'il est chargé dans Vim avec le bon plugin, ce fichier a un comportement particulier, il vous permet d'ouvrir directement dans un split les fichiers auxquels il fait référence et il est capable de dire à Vim de changer son répertoire courant.

Prenons un exemple, une fois le plugin installé, vous voulez travailler sur un projet symfony. Ouvrez un shell et tapez tout simplement «vim». Une fois lancé rentrez «:Project». Une barre apparait alors à gauche et elle est vide si c'est la première fois que vous lancez Project. Créons un nouveau projet. Pour cela il suffit de se placer dans la fenêtre et de rentrer \C. Vim vous pose alors quelques question afin de créer une entrée projet pour vous.

Le path :
Le chemin du répertoire, «/var/www/mon_projet»,
Le CD : le chemin du nouveau répertoire courant de Vim si vous voulez le changer «.» permettra de lui affecter la même valeur que pour le path (pratique pour lancer l'utilitaire symfony depuis Vim).
Les flags : permettent d'influer sur les recherchers etc etc ... les flags par défaut sont très bien.
Les filtres : permet d'indiquer quels fichiers vous voulez voir apparaitre par exemple «*.php *.yml *.ini *.sql *.css *.js»

Une fois ces renseignement rentrés, Vim va chercher tous les fichiers et créer l'arborescence du projet pour vous. Il est alors possible de surfer comme un explorateur de fichier ... mais ce n'est pas un explorateur ! C'est un fichier texte. Vous pouvez prendre les entrées, effacer celles qui ne vous servent à rien (cache, log), les déplacer pour mettre à coté les unes des autres celles que vous utilisez souvent (/lib/model et /app). Mieux que ça ! Vous pouvez créer une arborescence logique qui répond plus à vos besoins, pourquoi ne pas mettre dans un même répertoire virtuel mes les modules de mes application et mon modèle (sans afficher om et base) pour ne pas avoir à jongler sans cesse avec mille répertoires ...

vm02-copie-1.png
Pour info, presser «espace» vous permet d'avoir une vue agrandie (comme sur l'image à droite), «enter» vous permet d'ouvrir le fichier dans le dernier split utilisé ... je vous laisse taper «:help Project» pour voir la page d'aide de ce merveilleux plugin qui n'a pas changé depuis 2006 ! Quand ça marche ...
par greg publié dans : vim
ajouter un commentaire commentaires (7)    recommander
Lundi 24 mars 2008
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_secured: 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.
par greg publié dans : symfony
ajouter un commentaire commentaires (0)    recommander
Lundi 17 mars 2008
Allez, c'est promis, le prochain article portera sur autre chose que postgresql :o)

Aujourd'hui, au menu : la fonctionnalité derrière laquelle court l'équipe de développement de mysql : les schémas.

Qu'est ce qu'un schéma ?

Pour vous donner une illustration simple, un schéma est un peu l'équivalent d'une feuille de DBDesigner. Vous pouvez y mettre ce que bon vous semble, des tables, des vues, des séquences, des fonctions etc etc sauf des utilisateurs, ces derniers sont des objets qui sont transversals à tout le moteur de base de données.
Je laisse cette idée faire son chemin lentement, rendez vous compte ! On peut mettre plusieurs feuilles de schéma dans une base !

Reprenons notre base de données de bibliothèque avec nos 3 tables ouvrage, livre et magazine. Imaginons maintenant que nous devons développer une application de compatibilité pour notre bibliothèque, nous allons forcément ajouter des objets dans la base. Un schéma est le meilleur moyen de nos assurer que nous n'allons pas polluer la base existante.

test=> CREATE SCHEMA compta;
CREATE SCHEMA
Créons maintenant la table «transaction» qui va nous permettre de concentrer les dépenses faites sur les ouvrages :

test=> CREATE TABLE compta.depense (id SERIAL PRIMARY KEY, ouvrage_id INT NOT NULL, amount NUMERIC(5,2) NOT NULL, comment TEXT, CONSTRAINT prix_positif CHECK(amount >= 0));
NOTICE:  CREATE TABLE will create implicit sequence "depense_id_seq" for serial column "depense.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "depense_pkey" for table "depense"

CREATE TABLE
test=> d compta.depense
                                 Table "compta.depense"
   Column   |     Type     |                          Modifiers
------------+--------------+-------------------------------------------------------------
 id         | integer      | not null default nextval('compta.depense_id_seq'::regclass)
 ouvrage_id | integer      | not null
 amount     | numeric(5,2) | not null
 comment    | text         |
Indexes:
    "depense_pkey" PRIMARY KEY, btree (id)
Check constraints:
    "prix_positif" CHECK (amount >= 0::numeric)

test=> INSERT INTO compta.depense (ouvrage_id, amount, comment) VALUES ( 6, 10.99, 'frais reliure');
INSERT 0 1
test=> SELECT * FROM compta.depense;
id | ouvrage_id | amount |    comment
----+------------+--------+---------------
  1 |          6 |  10.99 | frais reliure
(1 row)

test=> _
Le tour est joué ! Nous avons un schéma à nous que nous pouvons peupler au grés de nos besoins. Il suffit d'indexer le nom des objets avec le nom du schéma pour les manipuler. Cependant et jusqu'à ce jour, nous n'avons jamais précisé de nom de schéma pour accéder aux objets que nous avions créés. Cela peut nous amener à penser qu'il existe un schéma par défaut. En fait oui et non. Si nous demandons à Postgresql de nous afficher la liste des tables de notre base de données voila ce qu'il nous donne :

test=> dt
          List of relations
 Schema |   Name   | Type  |  Owner
--------+----------+-------+----------
 public | emprunt  | table | testuser
 public | livre    | table | testuser
 public | magazine | table | testuser
 public | ouvrage  | table | testuser
(4 rows)

test=> _

D'une part, il existe bien un schéma appelé «public» qui contient les objets que nous avions précédemment créés, d'autre part,  au voleur ! Ou est passé notre table «dépense» ? Cela vient de ce qui s'appelle le «search_path». Le «search_path» est une variable propre à la session de votre client qui vous permet de définir quel schéma vous allez voir par défaut.

test=> SHOW search_path ;
  search_path
----------------
 "$user",public
(1 row)

test=> _

Cela signifie que par défaut, postgresql va vous montrer d'abord les objets du schéma qui porterait le nom de l'utilisateur postgresql avec lequel vous vous connectez (s'il existe) et ensuite les objets du schéma «public». Que se passe-t-il si nous demandons à postgres de nous montrer les éléments du schéma «compta» ?

test=> SET search_path TO compta;
SET
test=> dt
          List of relations
 Schema |  Name   | Type  |  Owner
--------+---------+-------+----------
 compta | depense | table | testuser
(1 row)

test=> _

Un peu rude peut être, nous allons sans doute avoir besoin des objets du schéma public, c'est en général une bonne idée de laisser ce schéma comme étant le dernier que postgresql vous donne.
test=> SET search_path TO compta, public;
SET
test=> dt
          List of relations
 Schema |   Name   | Type  |  Owner
--------+----------+-------+----------
 compta | depense  | table | testuser
 public | emprunt  | table | testuser
 public | lib_user | table | testuser
 public | livre    | table | testuser
 public | magazine | table | testuser
(5 rows)

test=> _

Les feuilles de schéma agissent un peu comme des calques sous photosh ... heu .. gimp, on voit d'abord les objets du schéma le plus proche puis les objets du schéma le plus lointain (qu'on a dit que c'était une bonne idée si c'était «public»). Que se passe-t-il alors si nous surchargeons un objet qui existe dans «public» ?

Par exemple la table ouvrage n'est pas des plus explicite pour notre comptable, nous allons la remplacer par une vue dans notre schéma «compta» :

test=> CREATE VIEW compta.ouvrage AS SELECT public.ouvrage.*, pg_catalog.pg_class.relname AS "type"  FROM public.ouvrage, pg_class WHERE public.ouvrage.tableoid = pg_catalog.pg_class.oid;
CREATE VIEW
test=> SELECT * FROM ouvrage;
id |         created_at         |   type
----+----------------------------+----------
  6 | 2008-03-13 21:54:43.376901 | livre
  4 | 2008-03-13 21:52:59.303918 | magazine
  5 | 2008-03-13 21:53:04.249153 | magazine
  8 | 2008-03-16 23:56:01.075842 | magazine
  9 | 2008-03-16 23:56:01.075842 | magazine
(5 rows)

test=> _

Nous pouvons ainsi reconstruire dans notre schéma une base à partir des données des autres schémas. Vous remarquerez au passage que pour obtenir le type d'ouvrage, nous faisons appel à la relation «pg_class» du schéma «pg_catalog». Ce schéma est un schéma système au même tître que le «information_schema». Il permet de stocker les informations relatives à votre base de données sans vous embêter. Par défaut, vous ne le voyez par car il n'est pas dans le search_path.

Mais revenons à nos moutons, puisque nous pouvons créer une vision de la base de données orientée selon notre métier, allons y carrément :

test=> DROP VIEW ouvrage;
DROP VIEW
test=> CREATE VIEW
  ouvrage
AS
SELECT
    public.ouvrage.*,
    pg_catalog.pg_class.relname AS "type",
   sum (CASE WHEN compta.depense.amount IS NULL THEN 0 ELSE compta.depense.amount END) AS amount
  FROM
    public.ouvrage
      LEFT JOIN
        compta.depense
      ON
        public.ouvrage.id = compta.depense.ouvrage_id,
    pg_class
  WHERE
      public.ouvrage.tableoid = pg_catalog.pg_class.oid
  GROUP BY
    public.ouvrage.id,
    public.ouvrage.created_at,
    pg_catalog.pg_class.relname
  ORDER BY
    amount
  DESC
;
CREATE VIEW
test=> SELECT * FROM ouvrage;
 id |         created_at         |   type   | amount
----+----------------------------+----------+--------
  6 | 2008-03-13 21:54:43.376901 | livre    |  10.99
  4 | 2008-03-13 21:52:59.303918 | magazine |      0
  5 | 2008-03-13 21:53:04.249153 | magazine |      0
  8 | 2008-03-16 23:56:01.075842 | magazine |      0
  9 | 2008-03-16 23:56:01.075842 | magazine |      0
(5 rows)

test=>  _

Ce qui revient à définir dans le modèle de notre application de comptabilité, une nouvelle classe «ouvrage» qui répond de façon plus pertinente à notre besoin métier.

Conclusion
Les schémas sont des sortes de namespaces qui agissent un peu comme des feuilles de calques que l'on peut superposer afin d'orienter une base de données selon nos besoins. Cela permet de compartimenter notre base et en utilisant par exemple les permissions, empêcher que des applications viennent fouiller dans nos données tout en se laissant la liberté de ne laisser voir que ce qui est utile.

Pour ceux qui sont arrivés jusque là en un seul morceau, un dernier détail, notre
sum (CASE WHEN compta.depense.amount IS NULL THEN 0 ELSE compta.depense.amount END) AS amount
peut s'écrire plus simplement à l'aide de la fonction COALESCE qui retourne le premier argument non null qui lui est donné :
sum (coalesce(compta.depense.amount, 0)) AS amount
C'est quand même plus joli :o)



.
par greg publié dans : postgresql
ajouter un commentaire commentaires (0)    recommander
Dimanche 16 mars 2008
Aujourd'hui, comment récupérer le type d'un enregistrement issu d'un héritage et quelle est la grosse limitation de l'héritage avec postgresql.

Je vous ai parlé la dernière fois de l'héritage dans postgresql. Reprenons le cas de notre bibliothèque avec des ouvrages qui peuvent être des magazines ou des livres.

CREATE TABLE ouvrage (
    id integer NOT NULL,
    created_at timestamp without time zone DEFAULT now() NOT NULL
);
CREATE TABLE livre (
    isbn character varying(255) NOT NULL,
    title character varying(255) NOT NULL,
    author character varying(255) NOT NULL
)
INHERITS (ouvrage);
CREATE TABLE magazine (
    title character varying(255) NOT NULL,
    published_at timestamp without time zone,
    issue integer,
    CONSTRAINT magazine_check CHECK (((published_at IS NOT NULL) OR (number IS NOT NULL))),
    CONSTRAINT magazine_issue_check CHECK ((issue > 0))
)
INHERITS (ouvrage);

Ajoutons rapidement deux magazines et un livre :
INSERT INTO livre (isbn, title, author) VALUES ('2840117495', 'L''élégance du hérisson', 'Muriel Barbery');
INSERT INTO magazine (title, published_at, issue) VALUES ('Linux magazine', '2008-03-01', 103), ('linux magazine', '2008-02-01', 102);

et jettons maintenant un coup d'oeil à notre table ouvrage :
SELECT * FROM ouvrage;
test=> SELECT * FROM ouvrage;
 id |         created_at
----+----------------------------
  7 | 2008-03-16 23:55:13.463453
  8 | 2008-03-16 23:56:01.075842
  9 | 2008-03-16 23:56:01.075842

Impossible à partir de cette table de savoir qui est quoi. Heureusement, Postgresql nous permet de retrouver nos petits grâce à l'utilisation de tables internes :
SELECT ouvrage.*, pg_class.relname AS "type"  FROM ouvrage, pg_class WHERE ouvrage.tableoid = pg_class.oid;
id |         created_at                            |   type
----+----------------------------------------+----------
  7 | 2008-03-16 23:55:13.463453 | livre
  8 | 2008-03-16 23:56:01.075842 | magazine
  9 | 2008-03-16 23:56:01.075842 | magazine

Nous voulons maintenant créer une table qui retrace les emprunts des différents ouvrages :
CREATE TABLE emprunt (id SERIAL PRIMARY KEY, ouvrage_id INT NOT NULL REFERENCES ouvrage (id) ON DELETE CASCADE, borrowed_at TIMESTAMP NOT NULL DEFAULT now(), returned_at TIMESTAMP , CONSTRAINT returned_after_borrowed CHECK (returned_at > borrowed_at));

Essayons maintenant d'emprunter l'édition numéro 103 de «linux magazine» :
INSERT INTO emprunt (ouvrage_id) VALUES (8);
ERROR:  insert or update on table "emprunt" violates foreign key constraint "emprunt_ouvrage_id_fkey"
DETAIL:  Key (ouvrage_id)=(8) is not present in table "ouvrage".

Postgresql nous dit que l'ouvrage avec id=8 n'existe pas dans la table ouvrage, et pour cause cet id n'existe réellement que dans la table magazine et ne vérifie donc pas la clé étrangère sur ouvrage quelque soit le résultat affiché d'un SELECT sur ouvrage.

Cette limitation de postgresql est sérieuse et il n'existe pas aujourd'hui de parade heureuse. Il est indispensable de prendre cette contrainte en compte lorsque vous faite votre schéma de base de données.

Sans cette fonctionnalité, mon projet de model orienté objet basé sur postgresql tombe à l'eau. Allez, si j'ai le courage, la prochaine fois, je vous parlerai des schémas.
par greg publié dans : postgresql
ajouter un commentaire commentaires (0)    recommander
Dimanche 9 mars 2008
Gérer l'héritage d'objets dans une base de données est un problème que beaucoup de développeurs connaissent. Imaginons que nous sommes en train d'écrire une application de gestion de bibliothèque (le cas d'école). Vous avez une table  qui va regrouper les ouvrages c'est à dire l'ensemble des publications que possède votre bibliothèque. Mais cette table généraliste va forcemment ne pas avoir des attributs spécifiques à certains types d'ouvrages commes des livres de poche, des bédés ou des magazines.

Illustrons ce cas concret avec 2 bases de données : une base de données «classique» appelons la «myXsql» et une base postgresql.

Avec la base classique, nous allons avoir un schéma avec une jointure  :

heritage01.png
 Cette solution est compliquée car nous gérons une clé primaire par table, une clé étrangère dans chaque table fille pour faire le lien avec l'ouvrage correspondant.

Au niveau du modèle objet, cela se traduit tant bien que mal. Nous avons en effet plusieurs façons de gérer cela. La  façon SQL de penser donne que l'objet ouvrage est un attribut de mon objet magazine ou livre de poche et que l'on regroupe tout dans une jointure.

La  façon objet est de faire étendre la classe ouvrage aux classes magazines et livre_poche pour que chaque couche gère sa table grace à l'appel à la méthode parent. Du coup, on double le nombre de requêtes (une pour chaque table).

Postgresql propose la fonctionnalité de l'héritage de table qui va gérer tout cela pour nous. Notre schéma de base de données devient très simple.

Pour bien illuster cet exemple, voici la ligne de création de la table magazine :
CREATE TABLE ouvrage (id SERIAL, created_at TIMESTAMP NOT NULL);

CREATE TABLE magazine (titre VARCHAR(255) NOT NULL, published_at TIMESTAMP NOT NULL, number INTEGER) INHERITS (ouvrage);
CREATE TABLE livre_poche (isbn VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL) INHERITS (ouvrage);
Regardons le résultat :
test=$ d ouvrage
                                     Table "public.ouvrage"
   Column     |            Type              |                      Modifiers
----------------+-------------------------+------------------------------------------------------
 id                  | integer                     | not null default nextval('ouvrage_id_seq'::regclass)
 created_at  | timestamp               | not null
et nos tables étendues :
test=$ d magazine
                                      Table "public.magazine"
    Column    |            Type             |                      Modifiers
----------------+-----------------------------+------------------------------------------------------
 id                  | integer                     | not null default nextval('ouvrage_id_seq'::regclass)
 created_at  | timestamp               | not null
 titre               | character varying(255)      | not null
 published_at | timestamp            | not null
 number        | integer                     |
Inherits: ouvrage
test=$ d livre_poche
                                   Table "public.livre_poche"
   Column   |            Type             |                      Modifiers
--------------+-------------------------+------------------------------------------------------
 id                | integer                    | not null default nextval('ouvrage_id_seq'::regclass)
 created_at | timestamp             | not null
 isbn            | character varying(255)      | not null
 title             | character varying(255)      | not null
 author        | character varying(255)      | not null
Inherits: ouvrage

Je sens votre curiosimètre en plein effervesence et pour cause, nous n'avons pas eu à déclarer de clé primaire sur les tables magazine et livre_poche car nous héritons de celle d'ouvrage ! Un exemple valant mieux que tartines d'explications, lançons nous :

test=$ ALTER TABLE ouvrage ALTER COLUMN created_at SET DEFAULT now();
ALTER TABLE
test$ d magazine
                                      Table "public.magazine"
    Column    |            Type             |                      Modifiers
----------------+-----------------------------+------------------------------------------------------
 id                  | integer                     | not null default nextval('ouvrage_id_seq'::regclass)
 created_at  | timestamp               | not null default now()
 titre               | character varying(255)      | not null
 published_at | timestamp            | not null
 number        | integer                     |
Inherits: ouvrage

Ce premier exemple se passe de commentaire. Ajoutons maintenant un magazine :
test=$ INSERT INTO magazine (titre, published_at, number) VALUES ('art et decoration', TIMESTAMP '2008/01/01', 144);
INSERT 0 1
test=$ SELECT * FROM magazine ;
 id |         created_at                           |       titre                 |    published_at             | number
----+----------------------------------------+----------------------+------------------------------+--------
  1 | 2008-03-09 12:12:07.787696 | art et decoration | 2008-01-01 00:00:00 |    144
(1 row)

test=$ SELECT * FROM ouvrage;
 id |         created_at
----+------------------------------
  1 | 2008-03-09 12:12:07.787696
(1 row)
test=$ SELECT * FROM livre_poche ;
 id | created_at | isbn | title | author
----+--------------+-------+-----+--------
(0 rows)







par greg publié dans : postgresql
ajouter un commentaire