Tuesday, September 20, 2011

How to kill Symfony’s Forms and live well

I hate the sfFormDoctrine. I don't make a mystery of it.

If you are reading this article, it's probably because you also have been searching for information on how to make them work, mostly in contests a bit more articulated, like in the case of embedding.
The good news is that you've found the definitive solution.
The bad news is that you have been gasping for months in a glass of water and wasted precious time reading usless chapters of Symfony's manual about Forms.

Very simply: the problems that you always had with the sfFormDoctrine don't depend on you.
It is the very concept of Form to to be wrong.
Now I will show you to get rid of them and live happy.

A dutiful specification

I have a debt of gratitude towards Fabien Pontencier and his team. My business lives because of Symfony. My salary comes from the use of Symfony's framework. Out of all the PHP frameworks that I have worked with professionally, Symfony is without a doubt the best. If I have to tell the whole story, I think Symfony is a masterpiece of architecture.

I do not want to give the impression of disregard towards Fabien's work with what I am about to say. I sincerly admire it.

But from the moment in this article I strongly contest some infrastructural choices of his framework and propose a solution that ruthlessly destroys one of the components ofSymfony's core, I felt like spitting it out: I might have written very strong and extremely politically incorrect assertions.

Forgive me. This is just my personal opinion.

Why so much hate?

I hate the sfFormDoctrine. I don't make a mystery of it.

If you are reading this article, it's probably because you also have been searching for information on how to make them work, mostly in contests a bit more articulated, like in the case of embedding.
The good news is that you've found the definitive solution.
The bad news is that you have been gasping for months in a glass of water and wasted precious time reading usless chapters of Symfony's manual about Forms.

Very simply: the problems that you always had with the sfFormDoctrine don't depend on you.
It is the very concept of Form to to be wrong.
Now I will show you to get rid of them and live happy.

A heretical point of view on the sfFormDoctrine

Where is the sfFormDoctrine in MVC context?

It seems to me that there are at least 3 fundamental tasks:

  1. to execute HTML rendering of the widgets, of the labels and of the error messages
  2. to invoke the methods of the Model – and if necessary the methods of the Models for the embedded Forms in the correct order – in order to persist data on the database
  3. to collect in a sole point the rules of validation and execute them.

Now, feature 1 is the same as the one normally performed by the components of thePresentation Layer, or the View layer in MVC context.

The Forms allow to call back in the template methods such as:
echo $Form->renderHiddenFields(); and to produce HTML code, precisely like link_to().
It is obvious that Forms, therefore, can be placed, together with Helpers, in the View layer of the MVC.

Feature 2 is superimposed to the tasks of the Controller: it's Controller layer elements that typically determine the operating flow of the program and invoke in the right order the methods of the Models of the underlying Data layer.
Forms, therefore, perform typical functions of the MVC Controller.

Feature 3 pertains the manipulation and the correctness of the data conserved from the Business Objects.
It is clearly a task that normally comes turn from the Model.
Forms, therefore, develop typical functions of the MVC Model.

A three-headed monster

The sfFormDoctrine objects unite what the MVC paradigm tried to separate.

In the architecture of SymfonyForms are the sole component whose field of application break the boundaries of the individual layer. The separation of code and functions in M, V and C, one of the cardinal principles of Ruby-On-Rails-like frameworks, comes cheerfully betrayed to breed an allmighty and omniscient monster, that can generate the HTML of the tags, perform the validation of Business Objects and take control of the program execution flow when saving on db. It is the sole object of the whole framework that does not respectMVC principles.

It comes by itself that the typical cleanliness and the exceptional order of softwares written with Symfony are lost as soon as we need to make operations with Forms that are just outside the ordinary. No wonder, in conclusion, if the Embedded Forms are the nightmare of every Symfony developer. And yet, if we think about it, the editing of two tables using just one Form should be a banal operation: or rather, to be really honest, this is the minimum you can ask to a framework.

It is sufficient to follow an ordinary tutorial of CakePHP or Ruby on Rails to realize that this target is really embarassingly very simple to reach with other frameworks. Moreover, both frameworks use exactly the approach described in this article.

If the result is that often the developer has the impression of having almost totally lost the control of his own application, it is clear that there is something really sick insfFormDoctrines.

My conviction is that the fundamental problem is the existence itself of the abstraction layer for the Form management.

The web is full of articles from with titles like 'A workaround to make embedded Forms and merged Forms work in almost any cases (very advanced)'.

I hope I have written 'The definitive suggestion to make the Form matter finally simple'.

Why the situation is not solveable with refactoring or patches

At the end of an umpteenth day of discouragement, working with a dynamically generated embedded Form, after reading the thousandth article on this subject, I have convinced myself to rewrite the entire Form management layer.

Surprisingly, the analysis led me to a rather paradoxical conclusion.

The Business Rules belong to the Business Object!

The first target that I planned was: to transfer the data validation control from the Form to the Model.

The idea that data validation is a Form task always seemed deeply wrong to me. Nowadays I am still convinced that it has been an unforgivable mistake from Symfony architects.
The core mistake is the unreasonable assumption that the data to validate comes unavoidably from a web form.
It is not true and it is very dangerous to assume it.

Let's see an example.

We take an application that should record data for a bank transaction. The application have a TransactionForm form to manipulate the Transaction model with a widget dedicated to the IBAN insertion.

Obviously it is vital that the IBAN is subject to validation prior to storage on db. It's a matter of money. For no reason in the world I would like to have a record with a malformed IBAN.

For this reason, the IBAN widget will have it's trusty validator. According to Symfony, I am covered from any bad surprises.

Nevertheless, if an Action performed:

 $TransAction = new TransAction(); $TransAction->setIban("wrong IBAN"); $TransAction->save(); 

the TransAction Model would cheerfully save the wrong IBAN field without any reports, violating the TransAction object Business Rule.

This possibility is anything but remote: the manipulation of a Model from an Action, without the involvement of a HTML Form, happens for example in the case of webservice.
Besides, it is self-evident: to validate a data means ensuring that the object respects the Business Rules.
And who is responsible for the Business Rules if not the Business Object, that is the Model?

The Business Object is the data. What twisted reasoning could have lead to the delirious decision to deprive the application data core of the sole protection network to move it some meters up, encapsulating inside a component that, among all its tasks, is in charge of printing HTML labels? (Someone, a real purist, could object that the Business Rule should be defined at an even lower level, at the level of RDBMS, for example by means through triggers. How could you say he's wrong?)

Preserving the code to respect the Business Rules within the Form class is dangerous and philosophically wrong. It sensationally violates the Single Responsibility Principle, because encapsulates the Model's responsibility within an object that is just one among many users of the Model and exposes the application to the risk of violating the validation rules every time Models are directly manipulated.

Not only this: it also brings the risk of having different and contradicting valdiation rules in case more than one Form involved the same Model.

The solution, in reality, is very simple: you imperitively need to move the data validation responsibility from users to object to validate.
It's obvious.

It's not an accident, in fact, that any ORM that are worthy of respect gives the tools to perform the validation.
Hibernate supplies the Hibernate Validator. And even Doctrine has it's own good validators. There is even a whole chapter in the manual about this. To ignore it is irresponsible.

For this, I eliminated from my implementation of the sfFormDoctrines every reference to the validation. In my implementation, I wished it was the Model itself to inform the Forms of what Business Rules are violated.

One less task for the sfFormDoctrines, with a great advantage for the solidity of the application.

I hate to double the code in the partial!

Inside the form are listed the widgets that must be used.

In the template that will have to visualize the Form (to understand, in the self-generated partial _Form.php) are listed many

 echo $Form ['a_field']; echo $Form ['an_other_field']; echo $Form ['a_third_field']; 

to obtain the HTML of the input field.

A lot of work can be saved asking the Form to generate itself all HTML inputs, with an

 echo $Form->render(); 

but this, Symfony's fans forgive me, is a solution that's almost ridiculous.

If you leave the control of HTML generation to the Form you need to do deadly leaps to manipulate those features for which the HTML would be the fittest tool.

For example, to move two fields in the HTML you need to intervene in the Form code with

 $this->widgetSchema->setPositions(   array(    'field_1',    'field_3',    'field_2',    'field_4'   ) ); 

Or, if you wanted to use a Form in a second page and decide to hide a field, you would have to implement a new class, adding in its configure ()

 unset($this->widgetSchema['field']) 

to add a Javascript event or to modify a CSS class of an input you need to perform disreputable tricks.

The most astonishing aspect is that Forms do everything to hinder the developer use of the three most suitable tools for these purposes: HTML, Javascript and CSS.

Bad scene.

In fact, echo $Form->render(); is good for scaffolding or little more and if you don't want to lose control too much on the HTML you will have templates substatially equivalent to long sequences of

 [...] <tr>   <th><?php echo $Form['name']->renderLabel() ?></th>   <td>     <?php echo $Form['name']->renderError() ?>     <?php echo $Form['name'] ?>   </td> </tr> <tr>   <th><?php echo $Form['surname']->renderLabel() ?></th>   <td>     <?php echo $Form['surname']->renderError() ?>     <?php echo $Form['surname'] ?>   </td> </tr> [...] 

Now, there would be nothing bad in listing all fields inside the template if it wasn't for the fact that you already have done the same identical job in the Form class with

 $this->setWidgets(array(   'id' => new sfWidgetFormInputHidden(),   'name' => new sfWidgetFormInputText(),   'surname' => new sfWidgetFormInputText(), )); 

Don't Repeat Yourself! Please, I want to write the field list just once!
And, if I could choose, I prefer to do it in the template. Besides, we are writing HTML.

In short, I find that the Form, as a widget container, after all, is not a big help.

I mean, if it was possible to eliminate the widgets from the Form and possible to write them directly in the template with something like:

 [...] <tr>   <td>     Name   </td>   <td>     <?php echo new sfWidgetFormInputText();?>   </td> </tr> <tr>   <td>     Surname   </td>   <td>     <?php echo new sfWidgetFormInputText();?>   </td> </tr> [...] 

we could emancipate the Form from the trivial and totally useless duty of managing HTML in place of the template.

Other frameworks use this approach, supplying the appropriate Helpers.
And even Symfony once supplied a set of Helpers for the tag generation directly in the template.
I got convinced that the template was really the best place where to manage HTML and layout, and that the Form as a generator of HTML was, after all, just an obstacle.

Only one problem remained: to visualize inside the HTML the validation error messages.
But it is soon said: once the Business Rules are transferred inside the Model, the template can query it directly to know which fields are not valid and visualize the respective error messages.
Oh yes, Doctrine can already very well manage by itself a stack of error messages. Is it an accident?

Another useless Form burden that we can get rid of.

No intermediary between sfWebRequest and Business Object!

Let's look at what happens in the typical method executeUpdate() of a Form based Action:

 public function executeUpdate(sfWebRequest $request) {   $this->forward404Unless($request->isMethod(sfRequest::POST) || $request->isMethod(sfRequest::PUT));   $this->forward404Unless($object = Doctrine_Core::getTable('Table')->find(array($request->getParameter('id'))),     sprintf('Object does not exist (%s).', $request->getParameter('id')));   $this->Form = new object Form($object);   $this->processForm($request, $this->Form);    $this->setTemplate('edit');  protected function processForm(sfWebRequest $request, sfForm $Form) {   $Form->bind($request->getParameter($Form->getName()), $request->getFiles($Form->getName()));   if ($Form->isValid())      $object $Form->save(); 

Roughly this is the process:

  1. User performs the submit and the executeUpdate() method, defined as Action in the Form, is invoked.

    $request contains all data introduced from the user

  2. Through Doctrine_Core::getTable('Table')->find($request->getParameter('id')) the object to update is recovered
  3. The object sfDoctrineForm is instantiated and the object instance is passed to it.
  4. sfDoctrineForm is processed. The Form executes the binding between $request and the object, so that the properties of the Model get the values defined in the input fields of the web Form.
  5. The Form is saved. With this operation the object is saved.

The question is: hmmmmm, for what purpose object Form is instantiated?

For two purposes:

  1. To perform the binding between $request and the object
  2. To save the object

It's like firing a cannon at a fly: there is no real need of instantiate an entire class to perform these few operations. You just need to ask the object directly to populate itself according to the data present in $request and subsequently to save itself:

 $object->fromArray($request->getParameter('something'); $object->save(); 

The involvement of the Form, in the standard approach of Symfony, is dictated by the necessity to perform the data validation and from the necessity to save eventual embedded Form. These are, by the way, two tasks that the Doctrine Model is already able to perform extremely well:

  • Model can easily validate itself
  • Model can save recursively the other Models eventually connected to it. Doctrine was invented for this purpose

Let's admit it: in the communication between the sfWebRequest and the Model, theForm is performing some totally useless operations.

But then, what's your purpose?

To sum it up:

  • Form as a HTML generator doubles the code and is not of help
  • It should not be used as a validation tool. May plague attack you if you only think about it.
  • It's no use at all for the communication between Form HTML and Business Object

What functionalities were left to implement, in my new version of the sfDoctrineFormobject?

None.

I began to think that this story of sfFormDoctrines was a large useless bubble. Lots of smoke and no real advantage. Just weeks of headaches to do the easiest operations.

For what benefit, then, to carry this pachyderm?
What would happen if we just simply killed it?
I tried.
It works.
Better.

The Form is dead, long life to Symfony

I tried to pretend I never read the chapter on sfFormDoctrine. And I discovered that everything becomes magically simple.

Let's get to the point. I want a Form (complete with the scary embedding feature) that allows me to save data of a cat: Cat(id, name, owner_id) and of his owner: Owner(id, name).

I used my courage and I killed the directory lib/form.

Goodbye.

After that I wrote a Model, with its good validator (I'm using a banal example: i could use the great standard Symfony validators inherited from sfValidatorBase; we'll see how later)

 class Cat extends BaseCat {   protected function validate() {     // Name's length must be at least 5     if (strlen($this->getName()) < 5) {       $errorStack = $this->getErrorStack();       $errorStack->add('nome', 'Name must be at least 5 char');     }   } } 

validate() is executed when we invoke isValid() to directly query the Model on the vaidity of its own data. The good thing about validate() is that is invoked automatically before saving the record: Doctrine, in other words, inhibits the saving of the records that violate the Business Rules. It's exactly what we were looking for.

Here's the HTML template to print the Form:

 <form method="post" Action="<?php echo url_for("cats/update");?>" >   <table>     <tr>       <td>         <?php            $w = new sfWidgetFormInputHidden();            $w->render("cat[id]", $cat->id);          ?>        </td>        <td>          <?php             $w = new sfWidgetFormInputText();            $w->render("cat[name]", $cat->name);          ?>        </td>      </tr>      <tr>        <td>          <?php            $w = new sfWidgetFormInputHidden();            $w->render("owner[id]", $cat->getOwner()->id);          ?>        </td>        <td>          <?php            $w = new sfWidgetFormInputText();            $w->render("owner[name]", $cat->getOwner()->name);          ?>        </td>      </tr>   </table>   <input type="submit" /> </form> 

The names of the widgets have been chosen so that the Action receives an objectsfWebRequest in the Form of an array, with the Cat data separated from the Owner ones:

 array(   'cat' => array(     'id' => 2,     'name' => "Fritz",   ),   'owner' => array(     'id' => 12,     'name' => "Marianne" ) ); 

The Action Class will look like this:

 class CatsActions extends sfActions {   public function executeNew(sfWebRequest $request) {     $this->cat = new Cat();   }    public function executeUpdate(sfWebRequest $request) {     $cat = new Cat();     $cat->fromArray($request->getParameter('cat'));     $cat->getOwner()->fromArray($request->getParameter('owner'));     if ($cat->isValid() && $owner->isValid()) {       $cat->save();       $this->redirect('cats/index');     }     else {       $this->cat = $cat;       $this->setTemplate('new');     }   }   public function executeEdit(sfWebRequest $request) {     $this->forward404Unless($cat = Doctrine_Core::getTable('Cats')->find(array($request->getParameter('id'))), sprintf('Object does not exist (%s).', $request->getParameter('id')));     $this->cat = $cat;   }   } 

End of the story, more or less.

Please note, among the other things, there's no more need of executeCreate(), because it's by itself capable of managing both the creation and the update of the object.

Don't try to run this code again, it would not work. For now just verify the logic of this.
To make everything run properly there are a couple of arrangements missing, still. These are some setup operations, to execute only once at the creation of the project.

Some Little Tweaks.

Doctrine's validation

In SymfonyDoctrine's validation is deactivated by default.

To activate it you need to set the attribute Doctrine_Core::ATTR_VALIDATE to the valueDoctrine_Core::VALIDATE_ALL.

You can use the callback method configureDoctrine() in the classconfig/ProjectConfiguration.class.php:

 class ProjectConfiguration extends sfProjectConfiguration {   public function configureDoctrine(Doctrine_Manager $manager) {     $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);   } } 

fromArray() is not clever

When I said that in order to execute the binding between sfWebRequest and the Model is sufficient

 $cat = new Cat(); $cat->fromArray($request->getParameter('cat')); 

I lied a little. Things are a bit more complicated.

Let's suppose we have record 12 with some values inside table cats.

It would be really nice if, loading an instance of cat with the array

 $data = array(   'id' => 12,   'name' => "Fritz" ) $cat = new Cat(); $cat->fromArray($data); 

the following execution of $cat->save() would update record 12.

Unfortunately things work differently and the saving would fail with a duplication error of primary key.

$cat = new Cat() creates a new instance of Cat. For Doctrine, this is a new record. Unfortunately, even once we set the primary key value (either manually or through fromArray()), Doctrine will keep on treating the instance as a new record.

To patch this, we need to be careful to execute $cat->assignIdentifier(12) to tell Doctrine to treat the Model like the id 12 record, and then execute an UPDATE instead of an INSERT.
Naturally, I would never like to have to write some glue code every time that I have to save data of a Form.

The idea is to implement this behavior in a class that extends sfDoctrineRecord. Generalizing the assignment of the primary key is not exactly banal, because the name of the primary key is arbitrarily chosen from the developer and because it is possible that he had chosen to use multiple primary keys. A possible (working) implementation could be:

 abstract class MyDoctrineRecord extends sfDoctrineRecord {   public $validatorSchema;    public function loadFromArray(array $array) {     $primaryKeys = $this->_table->getIdentifier();     if(!is_array($primaryKeys))       $primaryKeys = array($primaryKeys);      $primaryKeys = array_combine($primaryKeys, $primaryKeys);     $intersect = array_intersect_key(array_filter($array),$primaryKeys);      if (count($primaryKeys) == count(array_count_values($intersect))) {       $this->assignIdentifier(array_intersect_key(($array),$primaryKeys));     }     parent::fromArray(array_filter($array));   } } 

There is nothing left to do than convince all the other Table objects of the application to inherit from MyDoctrineRecord instead of sfDoctrineRecord.

Fortunately, Doctrine allows to define the classes to use as a base for both the Business Objects and the Table Classes.

 class ProjectConfiguration extends sfProjectConfiguration {   public function configureDoctrine(Doctrine_Manager $manager) {     $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine_Core::VALIDATE_ALL);     $options = array(       'baseClassName' => 'myDoctrineRecord',       //'baseTableClassName' => 'myDoctrineTable'     );     sfConfig::set('doctrine_Model_builder_options', $options);   } } 

We just have to use loadFromArray() instead of fromArray() and game over.

And what if we want to reuse sfValidator* classes?

sfDoctrineRecord classes inherit from Doctrine_Record a set of callback methods very useful for the validation: preValidate()postValidate() and validate().
However, it would be very useful to be able to use even the many sfValidator* classes provided from Symfony, like sfValidatorEmail for email validation, sfValidatorDoctrineUniquefor unique indexes validation etc.
It would be interesting, in short, to enrich the sfDoctrineRecord classes with theValidatorSchema previously used by sfDoctrineForm.

Luckily, it is not that difficult.
Let's suppose to define, in Cat Model, one sfValidatorRegex validator associatd to the name of the cat to make sure that the name starts with a capital letter:

 class Cat extends BaseCat {   public function validate() {     parent::validate();     if (strlen($this->nome) < 15) {       $errorStack = $this->getErrorStack();       $errorStack->add('name', 'Too Short');     }   }    public function setUp() {     parent::setUp();     $this->setValidatorSchema(       new sfValidatorSchema(array(         'name' => new sfValidatorRegex(array('pattern'=>'/[A-Z].*/'), array('invalid'=>'The name must start with a capital letter')       )      ));   } } 

We want that during the validation, not just the standard method sfDoctrineRecord::validate() is executed but also all the validators defined inside the Validator Schema.

It will be sufficient to enrich our implementation of sfDoctrineRecord as follows:

 abstract class MyDoctrineRecord extends sfDoctrineRecord {   public $validatorSchema = array();   public function setValidatorSchema(sfValidatorSchema $validatorSchema) {     $this->validatorSchema = $validatorSchema;   }   public function getValidatorSchema() {     return $this->validatorSchema;   }   public function validate() {     $this->setup();      try {       $this->getValidatorSchema()->clean($this->toArray(false));     }     catch(sfValidatorErrorSchema $errorSchema) {       $errorStack = $this->getErrorStack();       foreach($errorSchema->getErrors() as $key=>$error) {         /*@var $error sfValidatorError */         $errorStack->add($key, $error->getMessage());       }     }   } } 

Some Helpers

In sfDoctrineRecord::getErrorStack() the Model supplies an array with the validation errors.

sfDoctrineRecord::getErrorStack()->get($fieldName) supplies an error with the various errors spotted in the field of $fieldName Model.

To get the various messages from the Model and visualize them inside the Template we need a cycle similar to:

 foreach($Model->getErrorStack()->get($fieldName) as $error) {   echo $error; } 

It's worthy to use a Helper to reduce the code to be typed into the template. A possible prototype could be this one:

 class MyRender {   static function error($Model, $name) {     if(count($Model->getErrorStack()->get($name))>0)       return $Model->getErrorStack()->get($name);     else       return null;   }   static function errorList($Model, $name) {     $list = self::error($Model, $name);     if(!is_array($list))       $list = array($elenco);     return implode("<br/>", $list);   } } 

This way the template can be renamed as follows

 <tr>   <td>Name</td>   <td>     <?php echo MyRender::errorList($cat, 'name');?>     <?php     $w = new sfWidgetFormInputText();   	echo $w->render('cat[name]', $cat->getName();     ?>   </td> </tr> 

Download

Example for the lazy

You can download from here a full project that uses this approach.

And what about the automatic generation of the code?

Ephraim Pepe wrote a Symfony task that is able to generate Actions and templates based on this concept.

His work will be soon published.

What do we miss?

Just the sfForm class in the standard library of Symfony is made of 1340 lines of code. InSymfony 2 we still have a class hierarchy inside Symfony\Component\Form namespace with over 5000 lines of code already developed.

I have doubts, therefore, that in the next days, going deeper inside the subject, I will discover some fundamental functionalities that are not covered from my approach. For example, I know for sure that I still have to work on the implementation of the CSRF protection.

Yet I am persuaded that the direction to follow is the right one: leaving templates in control of the HTML, moving validation to Business Objects, using only the minimum necessary intermediates between sfWebRequest and Model.

In the company that I work for we are progressively abandoning the use of sfDoctrineForms in favor of this approach even for our production softwares.

If you are interested in the subject, stay in touch: we will be sharing code as we go.

No comments: