Quantcast

symfony

Samedi 10 octobre 6 10 /10 /Oct 11:56
Hi,

Every PHP developper who had one day to deal with a LDAP tree realized sadly there was no ready to use library to do all the dirty work. That always meant back the the PHP doc and re invent the wheel over and over again.
As it is more often people ask me about symfony and LDAP, I decided to implement an abstration layer for LDAP to make queries as simple as it is today with ORMs like Propel or Doctrine : slapOrm.

It is still in early stage of development : it can generate model class from a schema file, perform complex queries but can write to the LDAP nor generate forms from schema yet. Ultimately I wish it could handle relations between LDAP objects but I do want to develop it slowly, ensuring everthing is well designed and rock solid before moving on complex features. As I am far away from being a LDAP expert, I want this tool to be a convenient way to deal with LDAP without hassle. Every feedback will be welcome.

Installing slapOrmPlugin

Grab the archive on GitHub and extract it in you project's plugin directory. Clear the cache and here you go !

The documentation is available here
Par greg - Publié dans : symfony - Communauté : symfony
Ecrire un commentaire - Voir les 0 commentaires
Jeudi 8 janvier 4 08 /01 /Jan 20:02
Hello, this is time for the second article on Doctrine with symfony about inheritance this time. As I am used to Postgresql's inheritance mechanism, I was curious as Doctrine proposes 3 ways to create inheritance with your objects model :
  • simple inheritance
  • concrete inheritance
  • column aggregation inheritance
Let's (again) take an example: we want to create an application for a show that rents bikes, motorcycles and maybe more. We can say, our shop rents vehicules. All of the vehicules can be one of bike, motorcycle, car or whatever. We can easily understand a car is different from a bike and they do not have the same caracteristics.

The first step is to create 2 tables, one vehicle and one bicycle table:
config/doctrine/schema.yml:
Vehicle:
tableName: vehicle
actAs: [Timestampable]
columns:
ref: { type: char(6), primary: true }
brand: { type: varchar(255), notnull: true }
Bicycle:
inheritance: { type: simple, extends: Vehicle }
columns:
bike_type: { type: enum, values: [city, mountain, race], default: city, notnull: true }
gears: { type: integer(32), default: 1, notnull: true }
seats: { type: integer(4), default: 1, notnull: true }
checks:
gears_min: gears > 0
seats_min: seats >


We see here, we have defined a simple inheritance. So what did Doctrine ?
data/sql/schema.sql:
CREATE TABLE vehicle (
ref CHAR(6),
brand VARCHAR(255) NOT NULL,
bike_type VARCHAR(255) DEFAULT 'city' NOT NULL,
gears BIGINT DEFAULT 1 NOT NULL,
seats INT DEFAULT 1 NOT NULL,
created_at TIMESTAMP without time zone,
updated_at TIMESTAMP without time zone,
PRIMARY KEY(ref)
);
(Our check statements do no appear) It sounds rather limited but let's see what we can do with that, assuming we add 2 bikes in the database using fixtures:
./symfony doctrine:dql "SELECT * FROM Bicycle"
>> doctrine executing dql query
DQL: SELECT * FROM Bicycle
found 2 results
-
ref: BK0478
brand: peugeot
bike_type: city
gears: '4'
seats: 1
created_at: '2009-01-04 17:18:25'
updated_at: '2009-01-04 17:18:25'
-
ref: BK0734
brand: raleigh
bike_type: mountain
gears: '21'
seats: 1
created_at: '2009-01-04 17:18:25'
updated_at: '2009-01-04 17:18:25

This isn't really an inheritance mechanism because the children doesn't inherit from its parent. It is more like the parent get richer with every child classes. This may work with very simple cases because in this application, the motorcycle may have columns with «not null» attributes that bikes will never comply with. This isn't really what we are looking for.

Let's change our schema to use the concrete inhéritance :
config/doctrine/schema.yml:
Bicycle:
inheritance: { type: concrete, extends: Vehicle }
Launche the build-all-reload and see what we have:
data/sql/schema.sql:
CREATE TABLE vehicle (
ref CHAR(6),
brand VARCHAR(255) NOT NULL,
created_at TIMESTAMP without time zone,
updated_at TIMESTAMP without time zone,
PRIMARY KEY(ref)
);

CREATE TABLE bicycle (
ref CHAR(6),
brand VARCHAR(255) NOT NULL,
bike_type VARCHAR(255) DEFAULT 'city' NOT NULL,
gears BIGINT DEFAULT 1 NOT NULL,
seats INT DEFAULT 1 NOT NULL,
created_at TIMESTAMP without time zone,
updated_at TIMESTAMP without time zone,
PRIMARY KEY(ref),
CHECK (gears > 0),
CHECK (seats > 0)
);

It sounds like Doctrine has created two tables and wants to handle the inheritance by itself. Let's see what we have once the fixtures are loaded:
In our database:
dbtest=> \d
List of relations
Schema | Name | Type | Owner
--------+---------+-------+-------
public | bicycle | table | toto
public | vehicle | table | toto
(2 rows)

dbtest=> SELECT * FROM vehicle ;
ref | brand | created_at | updated_at
-----+-------+------------+------------
(0 rows)



dbtest=> SELECT * FROM bicycle ;
ref | brand | bike_type | gears | seats | created_at | updated_at
--------+---------+-----------+-------+-------+---------------------+---------------------
BK0478 | peugeot | city | 4 | 1 | 2009-01-04 17:25:26 | 2009-01-04 17:25:26
BK0734 | raleigh | mountain | 21 | 1 | 2009-01-04 17:25:26 | 2009-01-04 17:25:26
(2 rows)



We can see we have 2 bikes but it also appears we have no vehicules ! So Doctrine does not handle the inheritance by itself, if we want to have 2 bikes and also say we have 2 vehicles we have to overload the Bicycle definition. The other solution is to declare the Vehicle as an abstract class:
config/doctrine/schema.yml:
Vehicle:
tableName: vehicle
abstract: true

But this seems not to work (or I missunderstood the doc) because the table definition is still present in the SQL...

Anyway, I am now able to create a motorbike table adding the following to
config/doctrine/schema.yml:
Motorcycle:
inheritance: { type: concrete, extends: Vehicle }
columns:
type: { type: enum, values: [scooter, roadster, city], notnull: true, default: scooter }
cylinder: { type: integer, notnull: true }
plate: { type: string(11), notnull: true, unique: true }
checks:
cylinder_min: cylinder > 0
plate_format: plate ~ \'[0-9]{1,4}\\-[A-Z]{2,3}\\-[0-9]{1,2}\

So after adding some fixtures I have 2 tables with independant data:
dbtest=> SELECT * FROM motorcycle ;
ref | brand | type | cylinder | plate | created_at | updated_at
--------+--------+---------+----------+------------+---------------------+---------------------
MB1837 | vespa | scooter | 125 | 324-MA-92 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
MB6430 | yamaha | city | 750 | 2301-ZC-60 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
(2 rows)



dbtest=> SELECT * FROM bicycle ;

ref | brand | bike_type | gears | seats | created_at | updated_at
--------+---------+-----------+-------+-------+---------------------+---------------------
BK0478 | peugeot | city | 4 | 1 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
BK0734 | raleigh | mountain | 21 | 1 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
(2 rows)



dbtest=> SELECT * FROM vehicle ;
ref | brand | created_at | updated_at
-----+-------+------------+------------
(0 rows)



If I try to count the vehicles I have with a DQL query it would not be as simple as:
test$ ./symfony doctrine:dql "SELECT * FROM Vehicle"
>> doctrine executing dql query
DQL: SELECT * FROM Vehicle
>> doctrine no results found

Let's then try the last type of inheritance: the columns aggregation.

Bicycle:
inheritance: { type: column_aggregation, extends: Vehicle, keyField: type, keyValue: bicycle }
For the bicycles and
Motorcycle:
inheritance: { type: column_aggregation, extends: Vehicle, keyField: type, keyValue: motorbike }
for the motorbikes.

Then doctrine complains it cannot load the data because constraints. If I go and have a check at the database I see there is only one table with all the columns but with an extra column named type and none of my sql constraints:
dbtest=> SELECT * FROM vehicle ;
ref | brand | type | bike_type | gears | seats | cylinder | plate | created_at | updated_at
-----+-------+------+-----------+-------+-------+----------+-------+------------+------------
(0 rows)
At the end of this testing phase I am a bit confused about what these types of inheritance stand for. I am not sure I will ever use one of those. I was expecting Doctrine to have like a PHP implementation of what Postgresql's inheritance system is without the problems it has ... or maybe I am missing something :o)

Par greg - Publié dans : symfony
Ecrire un commentaire - Voir les 5 commentaires
Vendredi 2 janvier 5 02 /01 /Jan 23:42
Here is the first sequel of my experiments using Doctrine with symfony 1.2. I will go trough 4 chapters that I found interesting to test :
- Using postgresql's constraints
- Using inheritance
- Different types of relationship
- Playing with DQL

The basics of using Doctrine will not be described here, there are plenty of good docs on the web (at least here or here) that show how to enable doctrine and how to use it within symfony. I will spend time on some precise points.

Using postgresql's constraints

One more time, this is the place to say constraints are not implemented to turn developers'life into hell. There are here to report discrepencies as soon as possible, it is like turning on the warnings when you are developping, it helps you.
That's one of the very good news using Doctrine with symfony : the abitlity to handle databases's constraints. The other good news is, some of the constraints will be automatically implemented in the according forms ! Let's see that with the following example :

config/doctrine/schema.yml:
Employee:
tableName: employee
columns:
ref: { type: char(6), primary: true, notnull: true }
first_name: { type: string(50), notnull: true }
last_name: { type: string(50), notnull: true }
birthdate: { type: timestamp, notnull: true }
hiredate: { type: timestamp, notnull: true }
email: { type: string(100), notnull: true }
Here, we want to store some data about employees of a company. If we set that schema.yml up in the «config/doctrine» directory and run a «symfony doctrine:build-all» we can see the following result :

lib/form/doctrine/base/BaseEmployeeForm.class.php:
$this->setValidators(array(
'ref' => new sfValidatorDoctrineChoice(array('model' => 'Employee', 'column' => 'ref', 'required' => false)),
'first_name' => new sfValidatorString(array('max_length' => 50)),
'last_name' => new sfValidatorString(array('max_length' => 50)),
'birthdate' => new sfValidatorDateTime(),
'hiredate' => new sfValidatorDateTime(),
'email' => new sfValidatorString(array('max_length' => 100)),
));
data/sql/schema.sql:
CREATE TABLE employee (
ref CHAR(6),
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
birthdate TIMESTAMP without time zone NOT NULL,
hiredate TIMESTAMP without time zone NOT NULL,
email VARCHAR(100) NOT NULL,
PRIMARY KEY(ref)
);

We can see here the default behavior we are already used to. Let's now say the «ref» field has to be a 6 chars identifier with 2 uppercase letters and 4 digits, the «birthdate» cannot be a date older than the «hiredate» and the «email» has to be a real life email address.
Doctrine allows to use database contraints with the «checks» keyword. Our schema becomes :

config/doctrine/schema.yml:

Employee:
tableName: employee
columns:
ref: { type: char(6), primary: true, notnull: true }
first_name: { type: string(50), notnull: true }
last_name: { type: string(50), notnull: true }
birthdate: { type: timestamp, notnull: true }
hiredate: { type: timestamp, notnull: true }
email: { type: string(100), notnull: true }
checks:
correct_dates: hiredate > birthdate
ref_valid: ref ~ \'[A-Z]{2}[0-9]{4}\'
Now we can see the following result:

data/sql/schema.sql:

CREATE TABLE employee (
ref CHAR(6),
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
birthdate TIMESTAMP without time zone NOT NULL,
hiredate TIMESTAMP without time zone NOT NULL,
email VARCHAR(100) NOT NULL,
PRIMARY KEY(ref),
CHECK (hiredate > birthdate),
CHECK (ref ~ '[A-Z]{2}[0-9]{4}')
);

And no change in lib/form/doctrine/base/BaseEmployeeForm.class.php.
This means the constraints we just set are passed "as is" to the database which is very handy if we need to use database's functions to check the integrity of our data. In an other hand, Doctrine does not set any symfony validator to automatically configure our forms.
Be sure to escape quotes in the constraint definition in the «schema.yml» because it will be imported as is in the php file :

lib/model/doctrine/base/BaseEmployee.class.php:
    $this->check('hiredate > birthdate');
$this->check('ref ~ \'[A-Z]{2}[0-9]{4}\'');
So, how can we tell Doctrine to set up the validators for us to use in symfony forms ? Well, Doctrine allows us to easily declare application level constraints (See official documentation).

Change the email line in the config/doctrine/schema.yml:
email:      { type: string(100), notnull: true, email: true }
and re launch the «doctrine:build-all» command. You should see the following change in the lib/form/doctrine/base/BaseEmployeeForm.class.php:
'email'      => new sfValidatorEmail(array('max_length' => 100)),

But no change in our SQL file. So we need to handle the database constraints by ourselve (which is not DRY). What about simpler constraints about «first_name» and «last_name» if we want to say they need to be at minimum 2 chars lenght ?

In config/doctrine/schema.yml:
    first_name: { type: string(50), notnull: true, minlength: 2 }
last_name: { type: string(50), notnull: true, minlength: 2 }
Again, this only change symfony form's validators in lib/form/doctrine/base/BaseEmployeeForm.class.php:
      'first_name' => new sfValidatorString(array('max_length' => 50, 'min_length' => 2)),
'last_name' => new sfValidatorString(array('max_length' => 50, 'min_length' => 2)),

Something else ?


That's all for this first overview. This is very good already we can use database constraints in our schema file. It seems there are other good new features with doctrine like using your own sequences but I couldn't manage to make that to work.

Another good point is it is easy to make use of Postgresql's schemas using Doctrine. But, tell me if I am wrong, I have always seen Pg schemas good at owning applications model and Doctrine does not allow (yet) users to generate a per application model, everything is under de «lib/model/doctrine» directory for the project.

Enjoy.
Par greg - Publié dans : symfony
Ecrire un commentaire - Voir les 1 commentaires
Mardi 28 octobre 2 28 /10 /Oct 02:41
If you are aware of agile development methods, you have probably heard about test driven development. As I began to play more and more with symfony's unit testing tool (known as lime), I asked mysefl how to set up a test driven development (TDD).
In fact, I do not beleive in writing test before code. It is not easy to test a method that doesn't exist, especially if the class does not exist either. So begining with writing the test before a single line of code appeared to me like a masochist plan. To me it sounds we have several phases in writing code :
1 - write code until something work
2 - refactor the code to the right layer
3 - write the complete specifications of this code with tests.
4 - complete your code until all tests pass

I do think the first part is the creative part of the developer's work. The less you set structural constraints the better and the quicker it is. This part normally finish with a working prototype of code. The danger would be to let this as is and go in production with it.
The second part supposes the developers have a good understanding of the framework decoupled MVC structure and know the right place for everything. It can happen you work with a developper who can do these two steps in the same move, this is often the signature of well experienced developers.
The rest of this article will explain my experience of the two last steps.

So when I code, the first thing I do is to code until I had something working. Then I refactore little pieces of generic code in external classes that I placed in the lib directory of my plugin / project. This simply makes the code more easy to test.

When I began to write unit tests for my functions, I realized I could take advantage of this moment to also define how I exactly want this functions to behave!



So I tested these functions for things it wasn't doing yet, deciding when I was expecting it to send exceptions and handling different limit use cases. The whole process took me 10 minutes. In 10 minutes I exactly knew how I wanted it to interact with the rest of the code and in the same time, I also ensured I will have no regressions on that parts because it s all what tests are about
.
As expected, most of the tests didn't pass at first. Then I began to implement the rest of functionnalities. I was a bit surprised to notice I couldn't reach the 100% tests because sometimes PHP does not behave the way you think, by example getElementsByTagName method strangely do send any exception when you ask for a tag that doesn't exist nor when you feed it whith a non XML document. In this case, I could spot the limitations of my code and document them as long as there was nothing I could do to make this better.


Par greg - Publié dans : symfony
Ecrire un commentaire - Voir les 0 commentaires
Dimanche 22 juin 7 22 /06 /Juin 22:18
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
Ecrire un commentaire - Voir les 7 commentaires
Samedi 12 avril 6 12 /04 /Avr 13:31
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
Ecrire un commentaire - Voir les 0 commentaires
Dimanche 6 avril 7 06 /04 /Avr 18:26
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.

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
Ecrire un commentaire - Voir les 3 commentaires
Lundi 24 mars 1 24 /03 /Mars 22:00
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.
Par greg - Publié dans : symfony
Ecrire un commentaire - Voir les 2 commentaires
 
Créer un blog gratuit sur over-blog.com - Contact - C.G.U. - Rémunération en droits d'auteur - Signaler un abus - Articles les plus commentés