Chad 2010-07-15
The Symfony feature of being able to embeds forms into another form is so great. But there's a little issue for it to be fixed to be perfect -- if you have many to many relationships in the embedded form, they are never saved!1
To fix this, Jonathan Wage gave out a solution, but that's just not working for me, combining the modifications Nervo and Drahpal mentioned in that post, I come to a solution as below:
- // lib/form/doctrine/BaseFormDoctrine.class.php
- public function saveEmbeddedForms($con = null, $forms = null)
- {
- if (null === $con)
- {
- $con = $this->getConnection();
- }
- if (null === $forms)
- {
- $forms = $this->embeddedForms;
- }
- foreach ($forms as $key=>$form)
- {
- if ($form instanceof sfFormObject)
- {
- unset($form[self::$CSRFFieldName]);
- $form->bindAndSave($this->taintedValues[$key], $this->taintedFiles, $con);
- $form->saveEmbeddedForms($con);
- }
- else
- {
- $this->saveEmbeddedForms($con, $form->getEmbeddedForms());
- }
- }
- }
This will overrides the same function in sfFormObject.class.php. If you compare the two functions, you will find that I only changed two lines of code:
- // from these
- // $form->saveEmbeddedForms($con);
- // $form->getObject()->save($con);
- // to these
- unset($form[self::$CSRFFieldName]);
- $form->bindAndSave($this->taintedValues[$key], $this->taintedFiles, $con);
- $form->saveEmbeddedForms($con);
Because the many to many relationships are saved in the doSave() method of the form, and $form->getObject()->save($con) never call the it, so the relationships are never saved. On the other hand, by calling $form->bindAndSave(), it will call the save() method of the form, which in turn will call the doSave() method.
But then I meet another problem: if I have fields required to be unique in the embedded form at the same time, the M2M relationships will not be saved when creating new objects, but it does work for updating the exsting objects.
After struggling for several hours, I found that it's because the unique fields of the embedded form are already saved when saving the object of the main form, but when calling the $form->bindAndSave(), it binds only the posted tainted values:
- // sfFormObject.class.php
- public function bindAndSave($taintedValues, $taintedFiles = null, $con = null)
- {
- $this->bind($taintedValues, $taintedFiles);
- if ($this->isValid())
- {
- $this->save($con);
- return true;
- }
- return false;
- }
So, then when calling the $this->isValid() method, it trigger the doClean() method, which in turn will check to see if the tainted values which are suppose to be unique exist in database or not. As mentioned above, the object of the embeded form is already saved when saving the object of the main form, no wonder the $this->isValid() here will returns false, so the M2M relationships will not be saved.
To fix this, I overrode the bind() method of the embed form as follow:
- // the embedded form which have M2M relationships and unique fields
- public function bind(array $taintedValues = null, array $taintedFiles = null)
- {
- if($this->getObject()->getId()>0){
- $taintedValues['id']=$this->getObject()->getId();
- $this->isNew = false;
- }
- parent::bind($taintedValues, $taintedFiles);
- }
Now the M2M relationships are saved both when creating and updating the whole form, cool! :)
References:
1. http://trac.symfony-project.org/ticket/5867