Tuesday, September 20, 2011

Symfony: Merge embedded Form (Update)

Symfony provides a nice feature called "embedded Forms" (sfForm::embedForm) to embed subforms into a parent form. This can be used to edit multiple records at the same time. So let's say you have a basic user table called 'sf_guard_user' and a profile table called 'user_profile', then you might follow this guide to merge these forms together:

lib/forms/doctrine/sfUserGuardAdminForm.php:

 class sfGuardUserAdminForm extends BasesfGuardUserAdminForm {   public function configure()   {     parent::configure();      // Embed UserProfileForm into sfGuardUserAdminForm     $profileForm = new UserProfileForm($this->object->Profile);     unset($profileForm['id'], $profileForm['sf_guard_user_id']);     $this->embedForm("profile"$profileForm);   }2 }  

Remember to add "profile" to the list of visible columns inapps/backend/modules/sfGuardUser/config/generator.yml as decribed in the linked guide. The result may look like this:

Embedded form in symfony

This does what it is expected to do, but it doesn't look very nice. Especially for 1:1 related tables I'm more interested in a solution that looks like this:

Merged forms in Symfony

You can reach this using sfForm::mergeForm, but sadly the merged model won't get updated and you'll run into problems if the forms are sharing fieldnames. The solution is the following method embedMergeForm which can be defined in BaseFormDoctrine to be avaible in all other forms:

lib/forms/doctrine/BaseFormDoctrine.php:

 abstract class BaseFormDoctrine extends sfFormDoctrine {   /**    * Embeds a form like "mergeForm" does, but will still    * save the input data.    */   public function embedMergeForm($namesfForm $form)   {     // This starts like sfForm::embedForm     $name = (string) $name;     if (true === $this->isBound() || true === $form->isBound())     {       throw new LogicException('A bound form cannot be merged');     }     $this->embeddedForms[$name] = $form;      $form = clone $form;     unset($form[self::$CSRFFieldName]);      // But now, copy each widget instead of the while form into the current     // form. Each widget ist named "formname|fieldname".     foreach ($form->getWidgetSchema()->getFields() as $field => $widget)     {       $widgetName "$name|$field";       if (isset($this->widgetSchema[$widgetName]))       {         throw new LogicException("The forms cannot be merged. A field name '$widgetName' already exists.");       }        $this->widgetSchema[$widgetName] = $widget;                           // Copy widget       $this->validatorSchema[$widgetName] = $form->validatorSchema[$field]; // Copy schema       $this->setDefault($widgetName$form->getDefault($field));            // Copy default value        if (!$widget->getLabel())       {         // Re-create label if not set (otherwise it would be named 'ucfirst($widgetName)')         $label $form->getWidgetSchema()->getFormFormatter()->generateLabelName($field);         $this->getWidgetSchema()->setLabel($widgetName$label);       }     }      // And this is like in sfForm::embedForm     $this->resetFormFields();   }    /**    * Override sfFormDoctrine to prepare the    * values: FORMNAME|FIELDNAME has to be transformed    * to FORMNAME[FIELDNAME]    */   public function updateObject($values null)   {     if (is_null($values))     {       $values $this->values;       foreach ($this->embeddedForms AS $name => $form)       {         foreach ($form AS $field => $f)         {           if (isset($values["$name|$field"]))           {             // Re-rename the form field and remove             // the original field             $values[$name][$field] = $values["$name|$field"];             unset($values["$name|$field"]);           }         }       }     }      // Give the request to the original method     parent::updateObject($values);   } }  

This method ensures that each fieldname is unique (named 'FORMNAME|FIELDNAME') and the subform is validated and saved. It is used like embedForm:

lib/forms/doctrine/sfUserGuardAdminForm.php:

 class sfGuardUserAdminForm extends BasesfGuardUserAdminForm {   public function configure()   {     parent::configure();      // Embed UserProfileForm into sfGuardUserAdminForm     // without looking like an embedded form     $profileForm = new UserProfileForm($this->object->Profile);     unset($profileForm['id'], $profileForm['sf_guard_user_id']);     $this->embedMergeForm("profile"$profileForm);   } }  

Feel free to use this method in your own project. Maybe this method get's merged into Symfony some day ;-)

Update
frostpearl reported a problem using embedFormMerge() in conjunction with the autocompleter widget from sfFormExtraPlugin. If you expire these problems try to replace all occurences of "$name|$field" with "$name-$field".

No comments: