tags:

views:

206

answers:

2

Hi,

I have a symfony 1.4 form with 2 embedded forms.

The parent form has a drop down which determines which of the embedded forms you fill in (hidden/shown on the frontend using JavaScript).

The problem is, when I submit the form, the validation and save is being run on both of the embedded forms, which I obviously don't want.

What is the best way to alter the parent form so that it only validates and saves the relevant embedded form based on the selection made in the parent form?

Thanks.

A: 

Below is a generalized way of doing this. All of these methods can be added to BaseFormDoctrine except doBind

1. Add method to skip forms.

/**
* @param string $name Adds $name to an array of form names to ignore when saving/updating.
*/
protected function skipSavingForm($name)
{
  $this->skipSavingForms[$name] = $name;
  $this->validatorSchema[$name] = new sfValidatorPass();
}

2. Override doBind so that the form you aren't saving isn't validated

This way even if the form that is not being saved is submitted with errors, the form still validates. This is safe since these values are not being saved. Alternatively, you could clear the values with a preValidator. However, I prefer this solution so that if the user submits the form with an error, his values for both forms are still there.

/**
* Override doBind to skip validation on the form not being saved
* @param array $values
* @see sfForm::doBind
*/
protected function doBind(array $values)
{
  try
  {
    $formDecidingValue = $this->validatorSchema[$values['form_deciding_field']]->clean();
  } catch (sfValidatorError $e) {
     //either create an sfValidatorErrorSchema and throw it or call through to parent here and let parent::doBind throw the error
     return; //either way, we want to stop processing
  }

  $this->skipSavingForm($formDecidingValue ? 'Form1' : 'Form2');
  return parent::doBind($values);
}

3. Add getFormsToSave method

/**
*@return array An array of forms to be saved
*/
public function getFormsToSave()
{
  return array_diff_key($this->getEmbeddedForms(), $this->skipSavingForms);
}

4. Override saveEmbeddedForms and updateObjectEmbeddedForms

So that the skipped form is not saved. Alternatively, if you don't need to continue to display the form on an error, you can simply unset the embeddedForm in doBind.

public function saveEmbeddedForms($con = null, $forms = null)
{
  if (null === $con)
  {
    $con = $this->getConnection();
  }

  if (null === $forms)
  {
    $forms = $this->getFormsToSave();
  }

  foreach ($forms as $form)
  {
    if ($form instanceof sfFormObject)
    {
      $form->saveEmbeddedForms($con);
      $form->getObject()->save($con);
    }
    else
    {
      $this->saveEmbeddedForms($con, $form->getFormsToSave());
    }
  }
}

public function updateObjectEmbeddedForms($values, $forms = null)
{
  if (null === $forms)
  {
    $forms = $this->getFormsToSave();
  }

  foreach ($forms as $name => $form)
  {
    if (!isset($values[$name]) || !is_array($values[$name]))
    {
      continue;
    }

    if ($form instanceof sfFormObject)
    {
      $form->updateObject($values[$name]);
    }
    else
    {
      $this->updateObjectEmbeddedForms($values[$name], $form->getFormsToSave());
    }
  }
}
jeremy
Thanks for your answer. I found a couple of issues and extended it a bit, but it was a solid base for what I wanted to do. I've posted my solution if you want to feedback. Cheers again!
Stephen Melrose
Cleaned my up. You're right that we want to skip updateObjectEmbeddedForms.
jeremy
A: 

Note: Please see jeremy's answer as mine is based on his.

Thank you for your answer jeremy. Your code had a few issues, so I thought I'd post my implemented solution explaining what I did differently.

1. Override doBind()

The override of doBind() had an issue where an uncaught sfValidatorError would be thrown if the parent value didn't return clean from the validator. I wrapped it in a try/catch to suppress this.

I also altered it to work with multiple embedded forms, not just the two I specified.

protected $selectedTemplate;

public function getTemplateToEmbeddedFormKeyMap()
{
    // An array of template values to embedded forms
    return array(
        'template1' => 'templateform1',
        'template2' => 'templateform2',
        'template3' => 'templateform3',
        'templateN' => 'templateformN'
    );
}

protected function doBind(array $values)
{
    // Clean the "template" value
    try
    {
        $this->selectedTemplate = $this->validatorSchema['template']->clean(array_key_exists('template', $values) ? $values['template'] : NULL);
    }
    catch(sfValidatorError $e) {}

    // For each template embedded form
    foreach($this->getTemplateToEmbeddedFormKeyMap() as $template => $form_key)
    {
        // If there is no selected template or the embedded form is not for the selected template
        if ($this->selectedTemplate == NULL || $this->selectedTemplate != $template)
        {
            // Don't validate it
            $this->validatorSchema[$form_key] = new sfValidatorPass();
        }
    }

    // Parent
    parent::doBind($values);
}

2. NEW STEP Override updateObjectEmbeddedForms()

Because I've disabled validation on some or all of my embedded forms, we now have some uncleaned data in the $values array. I don't want this data being passed to my model objects within the embedded forms, so I've overridden updateObjectEmbeddedForms() to remove any data related to an embedded form that isn't validated.

public function updateObjectEmbeddedForms($values, $forms = null)
{
    // For each template embedded form
    foreach($this->getTemplateToEmbeddedFormKeyMap() as $template => $form_key)
    {
        // If there is no selected template or the embedded form is not for the selected template
        if ($this->selectedTemplate == NULL || $this->selectedTemplate != $template)
        {
            // Remove the data
            unset($values[$form_key]);
        }
    }

    // Parent
    parent::updateObjectEmbeddedForms($values, $forms);
}

3. Override saveEmbeddedForms()

And finally, I didn't like I had to copy and paste the entire base saveEmbeddedForms() method and then alter it, so I refactored it to remove the embedded forms I don't want to save before passing them to the parent.

public function saveEmbeddedForms($con = null, $forms = null)
{
    // Get the embedded forms
    if ($forms === NULL)
    {
        $forms = $this->getEmbeddedForms();
    }

    // For each template embedded form
    foreach($this->getTemplateToEmbeddedFormKeyMap() as $template => $form_key)
    {
        // If there is no selected template or the embedded form is not for the selected template
        if ($this->selectedTemplate == NULL || $this->selectedTemplate != $template)
        {
            // Remove the form so it isn't saved
            unset($forms[$form_key]);
        }
    }

    // Parent
    parent::saveEmbeddedForms($con, $forms);
}

Thanks again for the answer jeremy, it got me to this which works for my use case.

Stephen Melrose