Symfony makes good use of the flat-string array syntax, a sudo-serialization technique. This is great for objects that may require different options based on other column values (such as a “type” column). The synax works like this:
this=that
key=value
something=Something Else
This flat string is then parsed into an associative array with the sfToolkit::stringToArray() method. Using this, we can translate these values into the form framework by creating an OptionsForm class. The class will parse the values into individual fields, and provide widgets for the users to edit. When bound, the form will collapse these values back into a flat string.
The first step is to create an option form that extends the sfForm class:
class sfOptionsForm extends sfForm
Next, allow the options string (or array) to be passed in via the constructor:
public function __construct($defaults)
{
// convert options string to array
if(!is_array($defaults))
{
$defaults = sfToolkit::stringToArray($defaults);
}
$this->option_defaults = $defaults;
return parent::__construct();
}
Then, override the setup method to create basic widgets and validators.
public function setup()
{
foreach ($this->option_defaults as $field => $default)
{
$this->widgetSchema[$field] = new sfWidgetFormInput();
$this->validatorSchema[$field] = new sfValidatorString(array('required' => false));
}
$this->setDefaults($this->option_defaults);
}
That’s it! I would recommend extending this form in order to customize your widgets, but this is enough to get you started. Now, you need to add some methods to the form in order for the binding to be successful. In this example, we are using sfOptionsForm for an object field called “options”. In our parent form, we need to set this same field to our new form.
$this->embedForm('options', new sfOptionsForm($this->getObject()->getOptions());
The trickiest part of this whole process occurs when the form is bound. In order for this to function properly, we need to override our parent form’s bind() method.
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
// do special binding for options if options exist
if (array_key_exists('options', $taintedValues) && array_key_exists('options', $this->embeddedForms))
{
// Flattens the option form into a string
$taintedValues['options'] = $this->embeddedForms['options']->getFlattenedValues($taintedValues['options']);
// unset imbedded form, replace field with a string widget / validator
unset($this['options']);
$this->widgetSchema['options'] = new sfWidgetFormInput();
$this->validatorSchema['options'] = new sfValidatorString();
}
$ret = parent::bind($taintedValues, $taintedFiles);
return $ret;
}
There are a few snags with this method. One occurs when a form does not validate on save. Since we unset the embedded form, we have to re-embed the form if the form is invalid. This can be rectified by adding code after the parent::bind() method in our code above.
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
// ...
$ret = parent::bind($taintedValues, $taintedFiles);
{
$this->isBound = false; // form must be unbound in order to embed a form
$this->embedForm('options', new sfOptionsForm($this->getObject()->getOptions()); // re-embed the form
}
return $ret;
}
And there you have it! You now have a dynamic form-building interface. In order for your dynamic forms to be ironclad, there are still a few hidden snags you should be aware of. First, you need to make sure default options always get passed to the sfOptionsForm class. If an empty string is passed, the embedded form will be empty. Secondly, you need to take into consideration what happens when new fields are added, or the type of your object is switched. I usually implement a check to ensure the proper options are being passed for the dynamic form type.
To see these forms in action, install the csDoctrineSlideshowPlugin, or view the code straight from the repo.
RSS Feed