- Mini-Project: Products & Photos
- Learn more by doing the Examples
- Basic Form Setup
- Embedding Forms
- Refactoring
- Dissecting the sfForm Object
- Rendering Embedded Forms in the View
- Saving Object Forms
- Ignoring Embedded Forms
- Easily Embedding Doctrine-Related Forms
- Form Events
- Custom Styling when a Form Element has an Error
- Final Thoughts
by Ryan Weaver, Fabien Potencier
Symfony's form framework equips the developer with the tools necessary to
easily render and validate form data in an object-oriented matter. Thanks
to the sfFormDoctrine and sfFormPropel classes offered by each ORM,
the form framework can easily render and save forms that relate closely to
the data layer.
Real-world situations, however, often require the developer to customize and
extend forms. In this chapter we'll present and solve several common, but
challenging form problems. We'll also dissect the sfForm object and
remove some of its mystery.
Mini-Project: Products & Photos
The first problem revolves around editing an individual product and an unlimited number of photos for that product. The user must be able to edit both the Product and the Product's Photos on the same form. We'll also need to allow the user to upload up to two new Product Photos at a time. Here is a possible schema:
Product:
columns:
name: { type: string(255), notnull: true }
price: { type: decimal, notnull: true }
ProductPhoto:
columns:
product_id: { type: integer }
filename: { type: string(255) }
caption: { type: string(255), notnull: true }
relations:
Product:
alias: Product
foreignType: many
foreignAlias: Photos
onDelete: cascade
When completed, our form will look something like this:

Learn more by doing the Examples
The best way to learn advanced techniques is to follow along and test the
examples step by step. Thanks to the --installer feature of
symfony, we provide a simple way for you to create a working
project with a ready to be used SQLite database, the Doctrine database schema,
some fixtures, a frontend application, and a product module to work with.
Download the installer
script
and run the following command to create the symfony project:
$ php symfony generate:project advanced_form --installer=/path/to/advanced_form_installer.php
This command creates a fully-working project with the database schema we have introduced in the previous section.
note
In this chapter, the file paths are for a symfony project running with Doctrine as generated by the previous task.
Basic Form Setup
Because the requirements involve changes to two different models (Product
and ProductPhoto), the solution will need to incorporate two different
symfony forms (ProductForm and ProductPhotoForm). Fortunately, the form
framework can easily combine multiple forms into one via
sfForm::embedForm(). First, setup the ProductPhotoForm independently. In
this example, let's use the filename field as a file upload field:
// lib/form/doctrine/ProductPhotoForm.class.php public function configure() { $this->useFields(array('filename', 'caption')); $this->setWidget('filename', new sfWidgetFormInputFile()); $this->setValidator('filename', new sfValidatorFile(array( 'mime_types' => 'web_images', 'path' => sfConfig::get('sf_upload_dir').'/products', ))); }
For this form, both the caption and filename fields are automatically
required, but for different reasons. The caption field is required because
the related column in the database schema has been defined with a notnull
property set to true. The filename field is required by default
because a validator object defaults to true for the required option.
note
sfForm::useFields() is a new function to symfony 1.3 that allows the
developer to specify exactly which fields the form should use and in which
order they should be displayed. All other non-hidden fields are removed from
the form.
So far we've done nothing more than ordinary form setup. Next, we'll combine the forms into one.
Embedding Forms
By using sfForm::embedForm(), the independent ProductForm and
ProductPhotoForms can be combined with very little effort. The work is
always done in the main form, which in this case is ProductForm. The
requirements call for the ability to upload up to two product photos at once.
To accomplish this, embed two ProductPhotoForm objects into ProductForm:
// lib/form/doctrine/ProductForm.class.php public function configure() { $subForm = new sfForm(); for ($i = 0; $i < 2; $i++) { $productPhoto = new ProductPhoto(); $productPhoto->Product = $this->getObject(); $form = new ProductPhotoForm($productPhoto); $subForm->embedForm($i, $form); } $this->embedForm('newPhotos', $subForm); }
If you point your browser to the product module, you now have the ability to
upload two ProductPhotos as well as modify the Product object itself.
Symfony automatically saves the new ProductPhoto objects and links them to
the corresponding Product object. Even the file upload, defined in
ProductPhotoForm, executes normally.
Check that the records are saved correctly in the database:
$ php symfony doctrine:dql --table "FROM Product" $ php symfony doctrine:dql --table "FROM ProductPhoto"
In the ProductPhoto table, you will notice the filenames of the photos.
Everything is working as expected if you can find files with the same names as
the ones in the database in the web/uploads/products/ directory.
note
Because the filename and caption fields are required in ProductPhotoForm,
validation of the main form will always fail unless the user is uploading
two new photos. Keep reading to learn how to fix this problem.
Refactoring
Even if the previous form works as expected, it would be better to refactor the code a bit to ease testing and to allow the code to be easily reused.
First, let's create a new form that represents a collection of
ProductPhotoForms, based on the code we have already written:
// lib/form/doctrine/ProductPhotoCollectionForm.class.php class ProductPhotoCollectionForm extends sfForm { public function configure() { if (!$product = $this->getOption('product')) { throw new InvalidArgumentException('You must provide a product object.'); } for ($i = 0; $i < $this->getOption('size', 2); $i++) { $productPhoto = new ProductPhoto(); $productPhoto->Product = $product; $form = new ProductPhotoForm($productPhoto); $this->embedForm($i, $form); } } }
This form needs two options:
product: The product for which to create a collection ofProductPhotoForms;size: The number ofProductPhotoForms to create (default to two).
You can now change the configure method of ProductForm to read as follows:
// lib/form/doctrine/ProductForm.class.php public function configure() { $form = new ProductPhotoCollectionForm(null, array( 'product' => $this->getObject(), 'size' => 2, )); $this->embedForm('newPhotos', $form); }
Dissecting the sfForm Object
In the most basic sense, a web form is a collection of fields that are rendered
and submitted back to the server. In the same light, the sfForm object is
essentially an array of form fields. While sfForm manages the process,
the individual fields are responsible for defining how each will be rendered
and validated.
In symfony, each form field is defined by two different objects:
A widget that outputs the form field's XHTML markup;
A validator that cleans and validates the submitted field data.
tip
In symfony, a widget is defined as any object whose sole job is to output XHTML markup. While most commonly used with forms, a widget object could be created to output any markup.
A Form is an Array
Recall that the sfForm object is "essentially an array of form fields."
To be more precise, sfForm houses both an array of widgets and an array
of validators for all of the fields of the form. These two arrays, called
widgetSchema and validatorSchema are properties of the sfForm class.
In order to add a field to a form, we simply add the field's widget to the
widgetSchema array and the field's validator to the validatorSchema array.
For example, the following code would add an email field to a form:
public function configure() { $this->widgetSchema['email'] = new sfWidgetFormInputText(); $this->validatorSchema['email'] = new sfValidatorEmail(); }
note
The widgetSchema and validatorSchema arrays are actually special classes
called sfWidgetFormSchema and sfValidatorSchema that implement the
ArrayAccess interface.
Dissecting the ProductForm
As the ProductForm class ultimately extends sfForm, it too houses all of
its widgets and validators in widgetSchema and validatorSchema arrays.
Let's look at how each array is organized in the finished ProductForm object.
widgetSchema => array ( [id] => sfWidgetFormInputHidden, [name] => sfWidgetFormInputText, [price] => sfWidgetFormInputText, [newPhotos] => array( [0] => array( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), [1] => array( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), ), ) validatorSchema => array ( [id] => sfValidatorDoctrineChoice, [name] => sfValidatorString, [price] => sfValidatorNumber, [newPhotos] => array( [0] => array( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), [1] => array( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), ), )
tip
Just as widgetSchema and validatorSchema are actually objects that behave
as arrays, the above arrays defined by the keys newPhotos, 0, and 1
are also sfWidgetSchema and sfValidatorSchema objects.
As expected, basic fields (id, name and price) are represented at the first
level of each array. In a form that embeds no other forms, both the widgetSchema
and validatorSchema arrays have just one level, representing the basic fields
on the form. The widgets and validators of any embedded forms are represented
as child arrays in widgetSchema and validatorSchema as seen above. The method
that manages this process is explained next.
Behind sfForm::embedForm()
Keep in mind that a form is composed of an array of widgets and an array of validators.
Embedding one form into another essentially means that the widget and validator
arrays of one form are added to the widget and validator arrays of the main form.
This is entirely accomplished via sfForm::embedForm(). The result is always a
multi-dimensional addition to the widgetSchema and validatorSchema arrays as
seen above.
Below, we'll discuss the setup of ProductPhotoCollectionForm, which binds
individual ProductPhotoForm objects into itself. This middle form acts
as a "wrapper" form and helps with overall form organization. Let's begin
with the following code from ProductPhotoCollectionForm::configure():
$form = new ProductPhotoForm($productPhoto); $this->embedForm($i, $form);
The ProductPhotoCollectionForm form itself begins as a new sfForm object.
As such, its widgetSchema and validatorSchema arrays are empty.
widgetSchema => array() validatorSchema => array()
Each ProductPhotoForm, however, is already prepared with three fields (id, filename,
and caption) and three corresponding items in its widgetSchema and validatorSchema
arrays.
widgetSchema => array ( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ) validatorSchema => array ( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, )
The sfForm::embedForm() method simply adds the widgetSchema and validatorSchema
arrays from each ProductPhotoForm to the widgetSchema and validatorSchema
arrays of the empty ProductPhotoCollectionForm object.
When finished, the widgetSchema and validatorSchema arrays of the wrapper
form (ProductPhotoCollectionForm) are multi-level arrays that hold the
widgets and validators from both ProductPhotoForms.
widgetSchema => array ( [0] => array ( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), [1] => array ( [id] => sfWidgetFormInputHidden, [filename] => sfWidgetFormInputFile, [caption] => sfWidgetFormInputText, ), ) validatorSchema => array ( [0] => array ( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), [1] => array ( [id] => sfValidatorDoctrineChoice, [filename] => sfValidatorFile, [caption] => sfValidatorString, ), )
In the final step of our process, the resulting wrapper form,
ProductPhotoCollectionForm, is embedded directly into ProductForm.
This occurs inside ProductForm::configure(), which takes advantage of
all the work that was done inside ProductPhotoCollectionForm:
$form = new ProductPhotoCollectionForm(null, array( 'product' => $this->getObject(), 'size' => 2, )); $this->embedForm('newPhotos', $form);
This gives us the final widgetSchema and validatorSchema array structure
seen above. Notice that the embedForm() method is very similar to the simple
act of combining the widgetSchema and validatorSchema arrays manually:
$this->widgetSchema['newPhotos'] = $form->getWidgetSchema(); $this->validatorSchema['newPhotos'] = $form->getValidatorSchema();
Rendering Embedded Forms in the View
The current _form.php template of the product module looks like the
following:
// apps/frontend/module/product/templates/_form.php <!-- ... --> <tbody> <?php echo $form ?> </tbody> <!-- ... -->
The <?php echo $form ?> statement is the simplest way to display a form,
even the most complex ones. It is of great help when prototyping, but as soon
as you want to change the layout, you need to replace it with your own display
logic. Remove this line now as we will replace it in this section.
The most important thing to understand when rendering embedded forms in the
view is the organization of the multi-level widgetSchema array explained in
the previous sections. For this example, let's begin by rendering the basic
name and price fields from the ProductForm in the view:
// apps/frontend/module/product/templates/_form.php <?php echo $form['name']->renderRow() ?> <?php echo $form['price']->renderRow() ?> <?php echo $form->renderHiddenFields() ?>
As its name implies, the renderHiddenFields() renders all the hidden fields
of the form.
note
The actions code was purposefully not shown here because it requires no special
attention. Have a look at the apps/frontend/modules/product/actions/actions.class.php
actions file. It looks like any normal CRUD and can be generated automatically via
the doctrine:generate-module task.
As we've already learned, the sfForm class houses the widgetSchema and
validatorSchema arrays that define our fields. Moreover, the sfForm class
implements the native PHP 5 ArrayAccess interface, meaning we can directly access
fields of the form by using the array key syntax seen above.
To output the fields, you can simply access them directly and call the renderRow()
method. But what type of object is $form['name']? While you might expect
the answer to be the sfWidgetFormInputText widget for the name field,
the answer is actually something slightly different.
Rendering each Form Field with sfFormField
By using the widgetSchema and validatorSchema arrays defined in each form class,
sfForm automatically generates a third array called sfFormFieldSchema.
This array contains a special object for each field that acts as a helper
class responsible for the field's output. The object, of type
sfFormField, is a combination of each field's widget and validator and is
automatically created.
<?php echo $form['name']->renderRow() ?>
In the above snippet, $form['name'] is an sfFormField object, which houses
the renderRow() method along with several other useful rendering functions.
sfFormField Rendering Methods
Each sfFormField object can be used to easily render every aspect of the field
that it represents (e.g. the field itself, the label, error messages, etc.).
Some of the useful methods inside sfFormField include the following. Other
can be found via the symfony 1.3 API.
sfFormField->render(): Renders the form field (e.g.input,select) with the correct value using the field's widget object.sfFormField->renderError(): Renders any validation errors on the field using the field's validator object.sfFormField->renderRow(): All-encompassing: renders the label, the form field, the error and the help message inside an XHTML markup wrapper.
note
In reality, each rendering function of the sfFormField class also uses information
from the form's widgetSchema property (the sfWidgetFormSchema object that
houses all of the widgets for the form). This class assists in the generation
of each field's name and id attributes, keeps track of the label for each
field, and defines the XHTML markup used with renderRow().
One important thing to note is that the formFieldSchema array always
mirrors the structure of the form's widgetSchema and validatorSchema arrays.
For example, the formFieldSchema array of the completed ProductForm
would have the following structure, which is the key to rendering each
field in the view:
formFieldSchema => array ( [id] => sfFormField [name] => sfFormField, [price] => sfFormField, [newPhotos] => array( [0] => array( [id] => sfFormField, [filename] => sfFormField, [caption] => sfFormField, ), [1] => array( [id] => sfFormField, [filename] => sfFormField, [caption] => sfFormField, ), ), )
Rendering the New ProductForm
Using the above array as our map, we can easily output the embedded ProductPhotoForm
fields in the view by locating and rendering the proper sfFormField objects:
// apps/frontend/module/product/templates/_form.php <?php foreach ($form['newPhotos'] as $photo): ?> <?php echo $photo['caption']->renderRow() ?> <?php echo $photo['filename']->renderRow() ?> <?php endforeach; ?>
The above block loops twice: once for the 0 form field array and
once for the 1 form field array. As seen in the above diagram,
the underlying objects of each array are sfFormField objects, which we can
output like any other fields.
Saving Object Forms
Under most circumstances, a form will relate directly to one or more database
tables and trigger changes to the data in those tables based on the submitted
values. Symfony automatically generates a form object for each schema model,
which extends either sfFormDoctrine or sfFormPropel depending on your
ORM. Each form class is similar and ultimately allows for submitted values to
be easily persisted in the database.
note
sfFormObject is a new class added in symfony 1.3 to handle all of the
common tasks of sfFormDoctrine and sfFormPropel. Each class extends
sfFormObject, which now manages part of the form-saving process described below.
The Form Saving Process
In our example, symfony automatically saves both the Product information and
new ProductPhoto objects without any additional effort by the developer.
The method that triggers the magic, sfFormObject::save(), executes a variety
of methods behind the scenes. Understanding this process is key to extending
the process in more advanced situations.
The form saving process consists of a series of internally executed methods,
all of which happen after calling sfFormObject::save(). The majority
of the work is wrapped in the sfFormObject::updateObject() method, which
is called recursively on all of your embedded forms.

note
The majority of the saving process takes place from within the sfFormObject::doSave()
method, which is called by sfFormObject::save() and wrapped in a database
transaction. If you need to modify the saving process itself, sfFormObject::doSave()
is usually the best place to do it.
Ignoring Embedded Forms
The current ProductForm implementation has one major shortfall. Because the
filename and caption fields are required in ProductPhotoForm, validation
of the main form will always fail unless the user is uploading two new photos.
In other words, the user can't simply change the price of the Product without
also being required to upload two new photos.

Let's redefine the requirements to include the following. If the user leaves
all the fields of a ProductPhotoForm blank, that form should be ignored
completely. However, if at least one field has data (i.e. caption or filename),
the form should validate and save normally. To accomplish this, we'll employ
an advanced technique involving the use of a custom post validator.
The first step, however, is to modify the ProductPhotoForm form to make the
caption and filename fields optional:
// lib/form/doctrine/ProductPhotoForm.class.php public function configure() { $this->setValidator('filename', new sfValidatorFile(array( 'mime_types' => 'web_images', 'path' => sfConfig::get('sf_upload_dir').'/products', 'required' => false, ))); $this->validatorSchema['caption']->setOption('required', false); }
In the above code, we have set the required option to false when
overriding the default validator for the filename field. Additionally,
we have explicitly set the required option of the caption field to false.
Now, let's add the post validator to the ProductPhotoCollectionForm:
// lib/form/doctrine/ProductPhotoCollectionForm.class.php public function configure() { // ... $this->mergePostValidator(new ProductPhotoValidatorSchema()); }
A post validator is a special type of validator that validates across all of
the submitted values (as opposed to validating the value of a single field).
One of the most common post validators is sfValidatorSchemaCompare which
verifies, for example, that one field is less than another field.
Creating a Custom Validator
Fortunately, creating a custom validator is actually quite easy. Create a
new file, ProductPhotoValidatorSchema.class.php and place it in the
lib/validator/ directory (you'll need to create this directory):
// lib/validator/ProductPhotoValidatorSchema.class.php class ProductPhotoValidatorSchema extends sfValidatorSchema { protected function configure($options = array(), $messages = array()) { $this->addMessage('caption', 'The caption is required.'); $this->addMessage('filename', 'The filename is required.'); } protected function doClean($values) { $errorSchema = new sfValidatorErrorSchema($this); foreach($values as $key => $value) { $errorSchemaLocal = new sfValidatorErrorSchema($this); // filename is filled but no caption if ($value['filename'] && !$value['caption']) { $errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'caption'); } // caption is filled but no filename if ($value['caption'] && !$value['filename']) { $errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'filename'); } // no caption and no filename, remove the empty values if (!$value['filename'] && !$value['caption']) { unset($values[$key]); } // some error for this embedded-form if (count($errorSchemaLocal)) { $errorSchema->addError($errorSchemaLocal, (string) $key); } } // throws the error for the main form if (count($errorSchema)) { throw new sfValidatorErrorSchema($this, $errorSchema); } return $values; } }
tip
All validators extend sfValidatorBase and require only the doClean()
method. The configure() method can also be used to add options or messages to
the validator. In this case, two messages are added to the validator.
Similarly, additional options can be added via the addOption() method.
The doClean() method is responsible for cleaning and validating the bound
values. The logic of the validator itself is quite simple:
If a photo is submitted with only the filename or a caption, we throw an error (
sfValidatorErrorSchema) with the appropriate message;If a photo is submitted with no filename and no caption, we remove the values altogether to avoid saving an empty photo;
If no validation errors have occurred, the method returns the array of cleaned values.
tip
Because the custom validator in this situation is meant to be used as a
post validator, the doClean() method expects an array of the bound
values and returns an array of cleaned values. Custom validators, however,
can just as easily be created for individual fields. In that case, the
doClean() method will expect just one value (the value of the submitted
field) and will return just one value.
The last step is to override the saveEmbeddedForms() method of ProductForm
to remove empty photo forms to avoid saving an empty photo in the database (it
would otherwise throws an exception as the caption column is required):
public function saveEmbeddedForms($con = null, $forms = null) { if (null === $forms) { $photos = $this->getValue('newPhotos'); $forms = $this->embeddedForms; foreach ($this->embeddedForms['newPhotos'] as $name => $form) { if (!isset($photos[$name])) { unset($forms['newPhotos'][$name]); } } } return parent::saveEmbeddedForms($con, $forms); }
Easily Embedding Doctrine-Related Forms
New to symfony 1.3 is the sfFormDoctrine::embedRelation() function which
allows the developer to embed n-to-many relationship into a form
automatically. Suppose, for example, that in addition to allowing the user to
upload two new ProductPhotos, we also want to allow the user to modify the
existing ProductPhoto objects related to this Product.
Next, use the embedRelation() method to add one additional
ProductPhotoForm object for each existing ProductPhoto object:
// lib/form/doctrine/ProductForm.class.php public function configure() { // ... $this->embedRelation('Photos'); }
Internally, sfFormDoctrine::embedRelation() does almost exactly what we did
manually to embed our two new ProductPhotoForm objects. If two ProductPhoto
relations exist already, then the resulting widgetSchema and validatorSchema
of our form would take the following shape:
widgetSchema => array ( [id] => sfWidgetFormInputHidden, [name] => sfWidgetFormInputText, [price] => sfWidgetFormInputText, [newPhotos] => array(...) [Photos] => array( [0] => array( [id] => sfWidgetFormInputHidden, [caption] => sfWidgetFormInputText, ), [1] => array( [id] => sfWidgetFormInputHidden, [caption] => sfWidgetFormInputText, ), ), ) validatorSchema => array ( [id] => sfValidatorDoctrineChoice, [name] => sfValidatorString, [price] => sfValidatorNumber, [newPhotos] => array(...) [Photos] => array( [0] => array( [id] => sfValidatorDoctrineChoice, [caption] => sfValidatorString, ), [1] => array( [id] => sfValidatorDoctrineChoice, [caption] => sfValidatorString, ), ), )

The next step is to add code to the view that will render the new embedded Photo forms:
// apps/frontend/module/product/templates/_form.php <?php foreach ($form['Photos'] as $photo): ?> <?php echo $photo['caption']->renderRow() ?> <?php echo $photo['filename']->renderRow(array('width' => 100)) ?> <?php endforeach; ?>
This snippet is exactly the one we used earlier to embed the new photo forms.
The last step is to convert the file upload field by one which allows the user
to see the current photo and to change it by a new one
(sfWidgetFormInputFileEditable):
public function configure() { $this->useFields(array('filename', 'caption')); $this->setValidator('filename', new sfValidatorFile(array( 'mime_types' => 'web_images', 'path' => sfConfig::get('sf_upload_dir').'/products', 'required' => false, ))); $this->setWidget('filename', new sfWidgetFormInputFileEditable(array( 'file_src' => '/uploads/products/'.$this->getObject()->filename, 'edit_mode' => !$this->isNew(), 'is_image' => true, 'with_delete' => false, ))); $this->validatorSchema['caption']->setOption('required', false); }
Form Events
New to symfony 1.3 are form events that can be used to extend any form object from anywhere in the project. Symfony exposes the following four form events:
form.post_configure: This event is notified after every form is configuredform.filter_values: This event filters the merged, tainted parameters and files arrays just prior to bindingform.validation_error: This event is notified whenever form validation failsform.method_not_found: This event is notified whenever an unknown method is called
Custom Logging via form.validation_error
Using the form events, it's possible to add custom logging for validation errors on any form in your project. This might be useful if you want to track which forms and fields are causing confusion for your users.
Begin by registering a listener with the event dispatcher for the
form.validation_error event. Add the following to the setup() method
of ProjectConfiguration, which is located inside the config directory:
public function setup() { // ... $this->getEventDispatcher()->connect( 'form.validation_error', array('BaseForm', 'listenToValidationError') ); }
BaseForm, located in lib/form, is a special form class that all form
classes extend. Essentially, BaseForm is a class where code can be placed
and accessed by all form objects across the project. To enable logging of
validation errors, simply add the following to the BaseForm class:
public static function listenToValidationError($event) { foreach ($event['error'] as $key => $error) { self::getEventDispatcher()->notify(new sfEvent( $event->getSubject(), 'application.log', array ( 'priority' => sfLogger::NOTICE, sprintf('Validation Error: %s: %s', $key, (string) $error) ) )); } }

Custom Styling when a Form Element has an Error
As a final exercise, let's turn to a slightly lighter topic related to the
styling of form elements. Suppose, for example, that the design for the Product
page includes special styling for fields that have failed validation.

Suppose your designer has already implemented the stylesheet that will apply the error
styling to any input field inside a div with the class form_error_row.
How can we easily add the form_row_error class to the fields with errors?
The answer lies in a special object called a form schema formatter. Every symfony form uses a form schema formatter to determine the exact html formatting to use when outputting the form elements. By default, symfony uses a form formatter that employs HTML table tags.
First, let's create a new form schema formatter class that employs slightly
lighter markup when outputting the form. Create a new file named
sfWidgetFormSchemaFormatterAc2009.class.php and place it in the
lib/widget/ directory (you'll need to create this directory):
class sfWidgetFormSchemaFormatterAc2009 extends sfWidgetFormSchemaFormatter { protected $rowFormat = "<div class="form_row"> %label% \n %error% <br/> %field% %help% %hidden_fields%\n</div>\n", $errorRowFormat = "<div>%errors%</div>", $helpFormat = '<div class="form_help">%help%</div>', $decoratorFormat = "<div>\n %content%</div>"; }
Though the format of this class is strange, the general idea is that the renderRow()
method will use the $rowFormat markup to organize its output. A form schema
formatter class offers many other formatting options which I won't cover here
in detail. For more information, consult the
symfony 1.3 API.
To use the new form schema formatter across all form objects in your project,
add the following to ProjectConfiguration:
class ProjectConfiguration extends sfProjectConfiguration { public function setup() { // ... sfWidgetFormSchema::setDefaultFormFormatterName('ac2009'); } }
The goal is to add a form_row_error class to the form_row div element
only if a field has failed validation. Add a %row_class% token to the
$rowFormat property and override the sfWidgetFormSchemaFormatter::formatRow()
method as follows:
class sfWidgetFormSchemaFormatterAc2009 extends sfWidgetFormSchemaFormatter { protected $rowFormat = "<div class="form_row%row_class%"> %label% \n %error% <br/> %field% %help% %hidden_fields%\n</div>\n", // ... public function formatRow($label, $field, $errors = array(), $help = '', $hiddenFields = null) { $row = parent::formatRow( $label, $field, $errors, $help, $hiddenFields ); return strtr($row, array( '%row_class%' => (count($errors) > 0) ? ' form_row_error' : '', )); } }
With this addition, each element that is output via the renderRow() method
will automatically be surrounded by a form_row_error div if the field has
failed validation.
Final Thoughts
The form framework is simultaneously one of the most powerful and most complex components inside symfony. The trade-off for tight form validation, CSRF protection, and object forms is that extending the framework can quickly become a daunting task. Gaining a deeper understanding of the form system, however, is the key toward unlocking its potential. I hope this chapter has taken you one step closer.
Future development of the form framework will focus on preserving the power while decreasing complexity and giving more flexibility to the developer. The form framework is only now in its infancy.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.