Chapter 10 - Forms
Dealing with the display of form inputs, the validation of a form submission, and all the particular cases of forms is one of the most complex tasks in web development. Luckily, symfony provides a simple interface to a very powerful form sub-framework, and helps you to design and handle forms of any level of complexity in just a few lines of code.
NOTICE: This document is the first draft of a methodology experiment explained earlier in this blog. It documents the sfForm framework found in symfony 1.1, but with some changes in the API and usage. As such, it describes a library that is not yet written (like that) and cannot be used to learn the usage of the current sfForm implementation. It is quite long, so you might prefer to download the Markdown version and read it offline. Being a first draft, this document is a call for comments, both about its structure and its content. And if you are interested in implementing the differences between what this document describes and what is currently implemented in the symfony framework, please contact me.
Displaying a Form
A simple contact form featuring a name, an email, a subject and a message fields typically renders as follows:

In symfony, a form is an object defined in the action and passed to the template. In order to display a form, you must first define the fields it contains - symfony uses the term “widget”. The simplest way to do it is to create a new sfForm object in the action method.
[php]
// in modules/foo/actions/actions.class.php
public function executeContact($request)
{
$this->form = new sfForm();
$this->form->setWidgets(array(
'name' => 'text',
'email' => array('type' => 'text', 'default' => 'me@example.com'),
'subject' => array('type' => 'choice', 'choices' => array('Subject A', 'Subject B', 'Subject C')),
'message' => 'textarea'
));
}
sfForm::setWidgets() expects an associative array of widget name / widget definition. The definition must at least contain the widget type. text, choice, and textarea are some one the numerous widget types offered by symfony; you will find a complete list further in this chapter.
For simple widgets, specifying the widget type name is enough. If you need to define more than just the widget type, use an associative array with at least a type key. The previous example shows two widget options you can use: default sets the widget value, and is available for all widgets. choices is an option specific to the choice widget (which renders as a drop-down list): it defines the available options the user can select.
So the foo/contact action defines a form object, and then handles it to the contactSuccess template in a $form variable. The template can use this object to render the various parts of the form in HTML. The simplest way to do it is to call echo $form, and this will render all the fields as form controls with labels. You can also use the form object to generate the form tag:
[php]
// in modules/foo/templates/contactSuccess.php
<?php echo $form->renderFormTag('foo/contact') ?>
<table>
<?php echo $form ?>
<tr>
<td colspan="2">
<input type="submit" />
</td>
</tr>
</table>
</form>
With the parameters passed to setWidgets(), symfony has enough information to display the form correctly. The resulting HTML renders exactly like the screenshot above, with this underlying code:
[php]
<form action="/frontend_dev.php/foo/contact" method="POST">
<table>
<tr>
<th><label for="name">Name</label></th>
<td><input type="text" name="name" id="name" /></td>
</tr>
<tr>
<th><label for="email">Email</label></th>
<td><input type="text" name="email" id="email" value="me@example.com" /></td>
</tr>
<tr>
<th><label for="subject">Subject</label></th>
<td>
<select name="subject" id="subject">
<option value="0">Subject A</option>
<option value="1">Subject B</option>
<option value="2">Subject C</option>
</select>
</td>
</tr>
<tr>
<th><label for="message">Message</label></th>
<td><textarea rows="4" cols="30" name="message" id="message"></textarea></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" />
</td>
</tr>
</table>
</form>
Each widget results in a table row containing a <label> tag, and a form input tag. Symfony deduces the label name from the widget name by uppercasing it (the subject widget name gives the ‘Subject’ label ). As for the input tag, it depends on the widget type. Symfony adds an id attribute to each widget, based on its name. Lastly, the rendering of the form is always XHTML-compliant.
Customizing the Form Display
Using echo $form is great for prototyping, but you probably want to control exactly the resulting HTML code. The form object contains an array of fields, and calling echo $form actually iterates over the fields and renders them one by one. To get more control, you can iterate over the fields manually, and call the renderRow() on each field. The following listing produces exactly the same HTML code as previously, but the template echoes each field individually:
[php]
// in modules/foo/templates/contactSuccess.php
<?php echo $form->renderFormTag('foo/contact') ?>
<table>
<?php echo $form['name']->renderRow() ?>
<?php echo $form['email']->renderRow() ?>
<?php echo $form['subject']->renderRow() ?>
<?php echo $form['message']->renderRow() ?>
<tr>
<td colspan="2">
<input type="submit" />
</td>
</tr>
</table>
</form>
Rendering fields one by one allows you to change the order in which they are displayed, and also to customize their appearance. renderRow() expects an a list of HTML attributes as first argument, so you can define a custom class, id, or JavaScript event handler for instance. This list can be set as an associative array, or as a string with key=value pairs separated by spaces; symfony will understand both syntax. The second argument of renderRow() is an optional label that overrides the one deduced from the widget name. Here is an example of customization for the contact form:
[php]
// in modules/foo/templates/contactSuccess.php
<?php echo $form->renderFormTag('foo/contact') ?>
<table>
<!-- HTML attributes can be set by an associative array... -->
<?php echo $form['name']->renderRow(array('size' => 25, 'class' => 'foo'), 'Your Name') ?>
<?php echo $form['email']->renderRow(array('onclick' => 'this.value = "";'), 'Your Email') ?>
<!-- ...or by a string -->
<?php echo $form['subject']->renderRow('id=contact_subject class=bar') ?>
<?php echo $form['message']->renderRow('size=20x5') ?>
<tr>
<td colspan="2">
<input type="submit" />
</td>
</tr>
</table>
</form>
But maybe you need to output the label and the input of each field in <li> tags rather than in a <tr> tags. A field “row” is made of a label, an optional error message (added by the validation system, explained later in this chapter), a help text, and a widget (note that the widget can consist of more than one form control). Just as you can output the various fields of a form one by one, you can also render the various parts of a form independently. Instead of using renderRow(), use any of render() (for the widget), renderError(), renderLabel(), and renderHelp(). For instance, if you want to render the whole form with <li> tags, write the template as follows:
[php]
// in modules/foo/templates/contactSuccess.php
<?php echo $form->renderFormTag('foo/contact') ?>
<ul>
<?php foreach($form as $field): ?>
<li>
<?php echo $field->renderLabel() ?>
<?php echo $field->render()>
</li>
<?php endforeach; ?>
<li>
<input type="submit" />
</li>
</ul>
</form>
This renders to HTML as follows:
[php]
<form action="/frontend_dev.php/foo/contact" method="POST">
<ul>
<li>
<label for="name">Name</label>
<input type="text" name="name" id="name" />
</li>
<li>
<label for="email">Email</label>
<input type="text" name="email" id="email" />
</li>
<li>
<label for="subject">Subject</label>
<select name="subject" id="subject">
<option value="0">Subject A</option>
<option value="1">Subject B</option>
<option value="2">Subject C</option>
</select>
</li>
<li>
<label for="message">Message</label>
<textarea rows="4" cols="30" name="message" id="message"></textarea>
</li>
<li>
<input type="submit" />
</li>
</ul>
</form>
Tip: A field row is the representation of all the elements of a form field (label, error message, help text, form input) by a formatter. By default, symfony uses a
tableformatter, and that’s whyrenderRow()returns a set of<tr>,<th>and<td>tags. Alternatively, you can obtain the same HTML code as above by simply specifying an alternativelistformatter for the form, as follows:
[php]
// in modules/foo/templates/contactSuccess.php
<?php echo $form->renderFormTag('foo/contact') ?>
<ul>
<?php $form->setFormatterName('list') ?>
<?php echo $form ?>
<li>
<input type="submit" />
</li>
</ul>
</form>
Check the API documentation of the
sfWidgetFormSchemaFormatterclass to learn how to create your own formatter.
Form Widgets
There are many available form widgets at your disposal to compose your forms. All widgets accept at least the type parameter and the default parameter.
You can also define the label of a widget, and even its HTML attributes, when you create the form:
[php]
$this->form = new sfForm();
$this->form->setWidgets(array(
'name' => array('type' => 'text', 'label' => 'Your Name', 'attributes' => array('size' => 25, 'class' => 'foo')),
'email' => array('type' => 'text', 'default' => 'me@example.com', 'label' => 'Your Email', 'attributes' => array('onclick' => 'this.value = "";')),
'subject' => array('type' => 'select', 'choices' => array('Subject A', 'Subject B', 'Subject C'), 'attributes' => 'id=contact_subject class=bar'),
'message' => array('type' => 'textarea', 'attributes' => 'size=20x5')),
));
Symfony uses these parameters to display the widget, and you can still override them by passing custom parameters to renderRow() in the template.
TIP: As an alternative to calling
setWidgets()with an associative array, you can call thesetWidget($name, $properties)method several times.
Standard Widgets
Here is a list of available widget types, and how they translate into HTML (via renderRow()):
[php]
// Text input
$form->setWidget('full_name', array('type' => 'text', 'default' => 'John Doe'));
<label for="full_name">FullName</label>
<input type="text" name="full_name" id="full_name" value="John Doe" />
// Textarea
$form->setWidget('address', array('type' => 'textarea', 'attributes' => 'size=20x5', 'default' => 'Enter your address here'));
<label for="address">Address</label>
<textarea name="address" id="address" cols="20" row="5">Enter your address here</textarea>
// Password input
// Note that 'password' type widgets don't take a 'default' parameter for security reasons
$form->setWidget('pwd', array('type' => 'password'));
<label for="pwd">Pwd</label>
<input type="password" name="pwd" id="pwd" />
// Hidden input
$form->setWidget('id', array('type' => 'hidden', 'default' => 1234));
<input type="hidden" name="id" id="id" value="1234" />
// Checkbox
$form->setWidget('single', array('type' => 'checkbox', 'value_attribute_value' => 'single', 'default' => true));
<label for="single">Single</label>
<input type="checkbox" name="single" id="single" value="true" checked="checked" />
There are more options available for each widget than what is exposed here. Check the widget API documentation for a complete description of what each widget expects and how it renders.
List Widgets
Whenever users have to make a choice in a list of values, and whether they can select one or many option in this list, a single widget answers all the needs: the choice widget. According to two optional parameters (multiple and expanded), this widget renders in a different way:
| multiple=false | multiple=true
| (default) |
----------------|----------------------|---------------------
expanded=false | Dropdown list | Dropdown box
(default) | (`<select>`) | (`<select multiple>`)
----------------|----------------------|----------------------
expanded=true | List of Radiobuttons | List of checkboxes
| |
The choice widget expects at least a choices parameter with an array of the possible options. +Using an associative array instead of a simple array allows to define both the value and the text of each option. Here is an example of each syntax:
[php]
// Dropdown list (select)
$form->setWidget('country', array(
'type' => 'choice',
'choices' => array('us' => 'USA', 'ca' => 'Canada', 'uk' => 'UK', 'other'),
'default' => 'uk',
'add_empty' => 'Select from the list'
));
// symfony renders the widget in HTML as
<label for="country">Country</label>
<select id="country" name="country">
<option value="">Select from the list</option>
<option value="us">USA</option>
<option value="ca">Canada</option>
<option value="uk" selected="selected">UK</option>
<option value="0">other</option>
</select>
// Dropdown list with multiple choices
$form->setWidget('languages', array(
'type' => 'choice',
'multiple' => 'true',
'choices' => array('en' => 'English', 'fr' => 'French', 'other'),
'default' => array('en', 0)
));
// symfony renders the widget in HTML as
<label for="languages">Language</label>
<select id="languages" multiple="multiple" name="languages[]">
<option value="en" selected="selected">English</option>
<option value="fr">French</option>
<option value="0" selected="selected">other</option>
</select>
// List of Radio buttons
$form->setWidget('gender', array(
'type' => 'choice',
'expanded' => 'true,
'choices' => array('m' => 'Male', 'f' => 'Female'),
'class' => 'gender_list'
));
// symfony renders the widget in HTML as
<label for="gender">Gender</label>
<ul class="gender_list">
<li><input type="radio" name="gender" id="gender_m" value="m"><label for="gender_m">Male</label></li>
<li><input type="radio" name="gender" id="gender_f" value="f"><label for="gender_f">Female</label></li>
</ul>
// List of checkboxes
$form->setWidget('interests', array(
'type' => 'choice',
'multiple' => 'true',
'expanded' => true,
'choices' => array('Programming', 'Other')
));
// symfony renders the widget in HTML as
<label for="interests">Interests</label>
<ul class="interests_list">
<li><input type="checkbox" name="interests[]" id="interests_0" value="0"><label for="interests_0">Programming</label></li>
<li><input type="checkbox" name="interests[]" id="interests_1" value="1"><label for="interests_1">Other</label></li>
</ul>
Tip: You probably noticed that symfony automatically defines an
idattribute for each form input, based on a combination of the name and value of the widget. You can override theidattribute widget by widget, or alternatively set a global rule for the whole form with thesetIdFormat()method:
[php]
// in modules/foo/actions/actions.class.php
$this->form = new sfForm();
$this->form->setIdFormat('my_form_%s');
Foreign Key Widgets
When editing Model objects through a form, a particular list of choices always comes up: the list of objects that can be related to the current one. This happens when models are related by a many-to-one relationship, or a many-to-many. Fortunately, the sfPropelPlugin bundled with symfony provides a propel_choice widget specifically for these cases (and sfDoctrinePlugin offers a similar doctrine_choice widget).
For instance, if a Section has many Articles, you should be able to choose a section among the list of existing ones when editing an article. To do so, an ArticleForm should use the propel_choice widget:
[php]
$articleForm = new sfForm();
$articleForm->setWidgets(array(
'id' => 'hidden',
'title' => 'text',
'section_id' => array(
'type' => 'propel_choice',
'model' => 'Section',
'column' => 'name'
)
));
This will display a list of existing sections… provided you defined a __toString() method in the Section model class. That’s because symfony first retrieves the available Section objects, and populates a choice widget with them by trying to echo each object. So the Section model should at least feature the following method:
[php]
// in lib/model/Section.php
public function __toString()
{
return $this->getName();
}
The propel_choice widget is an extension of the choice widget, so you can use the multiple option to deal with many-to-many relationships, and the expanded option to change the way the widget is rendered.
If you want to order the list of choices in a special way, or filter it so that is displays only a portion of the available choices, use the criteria option to pass a Criteria object to the propel_choice widget. Doctrine supports the same kind of customization: you can pass a Doctrine_Query object to the doctrine_choice widget with the query option.
Date Widgets
Date and time widgets output a set of dropdown lists, populated with the available values for the day, month, year, hour or minute.
[php]
// Date
$form->setWidget('dob', array(
'type' => 'date',
'label' => 'Date of birth',
'default' => '01/01/1950', // can be a timestamp or a string understandable by strtotime()
'years' => array(1950, 1951, .., 1990)
));
// symfony renders the widget in HTML as
<label for="dob">Date of birth</label>
<select id="dob_month" name="dob[month]">
<option value=""/>
<option selected="selected" value="1">01</option>
<option value="2">02</option>
...
<option value="12">12</option>
</select> /
<select id="dob_day" name="dob[day]">
<option value=""/>
<option selected="selected" value="1">01</option>
<option value="2">02</option>
...
<option value="31">31</option>
</select> /
<select id="dob_year" name="dob[year]">
<option value=""/>
<option selected="selected" value="1950">1950</option>
<option value="1951">1951</option>
...
<option value="1990">1990</option>
</select>
// Time
$form->setWidget('start', array('type' => 'time', 'default' => '12:00'));
// symfony renders the widget in HTML as
<label for="start">Start</label>
<select id="start_hour" name="start[hour]">
<option value=""/>
<option value="0">00</option>
...
<option selected="selected" value="12">12</option>
...
<option value="23">23</option>
</select> :
<select id="start_minute" name="start[minute]">
<option value=""/>
<option selected="selected" value="0">00</option>
<option value="1">01</option>
...
<option value="59">59</option>
</select>
// Date and time
$form->setWidget('end', array('type' => 'datetime', 'default' => '01/01/2008 12:00'))
// symfony renders the widget in HTML as 5 dropdown lists for month, day, year, hour, minute
Of course, you can customize the date format, to display it in European style instead of International style (%day%/%month%/%year% instead of %month%/%day%/%year%), you can switch to 2×12 hours per day instead of 24 hours, you can define custom values for the first option of each dropdown box, and you can define limits to the possible values. Once again, check the API documentation for more details about the options of the date and time widgets.
Date widgets are a good example of the power of widgets in symfony. A widget is not a simple form input. It can be a combination of several inputs, that symfony can render and read from in a transparent way.
I18n Widgets
In multilingual applications, dates must be displayed in a format according to the user culture (see Chapter 13 for details about culture and localization). To facilitate this localization in forms, symfony provides an i18n_date widget, which expects a user culture to decide of the date formatting parameters. You can also specify a month_format to have the month drop-down display month names (in the user language) instead of numbers.
[php]
// Date
$form->setWidget('dob', array(
'type' => 'i18n_date',
'culture' => $this->getUser()->getCulture(),
'month_format' => 'name', // Use any of 'name' (default), 'short_name', and 'number'
'label' => 'Date of birth',
'default' => '01/01/1950',
'years' => array(1950, 1951, .., 1990)
));
// For an English-speaking user, symfony renders the widget in HTML as
<label for="dob">Date of birth</label>
<select id="dob_month" name="dob[month]">
<option value=""/>
<option selected="selected" value="1">January</option>
<option value="2">February</option>
...
<option value="12">December</option>
</select> /
<select id="dob_day" name="dob[day]">...</select> /
<select id="dob_year" name="dob[year]">...</select>
// For an French-speaking user, symfony renders the widget in HTML as
<label for="dob">Date of birth</label>
<select id="dob_day" name="dob[day]">...</select> /
<select id="dob_month" name="dob[month]">
<option value=""/>
<option selected="selected" value="1">Janvier</option>
<option value="2">Février</option>
...
<option value="12">Décembre</option>
</select> /
<select id="dob_year" name="dob[year]">...</select>
Similar widgets exist for time (i18n_time), and datetime (i18n_datetime).
There are two dropdown lists that occur in many forms and that also rely on the user culture: country and language selectors. Symfony provides two widgets especially for this purpose. You don’t need to define the choices in these widgets, as symfony will populate them with the list of countries and languages in the language of the user (provided the user speaks any of the 250 referenced languages in symfony).
// Country list
$form->setWidget('country', array(
'type' => 'i18n_country_choice',
'culture' => $this->getUser()->getCulture(),
'default' => 'UK'
));
// For an English-speaking user, symfony renders the widget in HTML as
<label for="country">Country</label>
<select id="country" name="country">
<option value=""/>
<option value="AD">Andorra</option>
<option value="AE">United Arab Emirates</option>
...
<option value="ZWD">Zimbabwe</option>
</select>
// Language list
$form->setWidget('language', array(
'type' => 'i18n_language_choice',
'culture' => $this->getUser()->getCulture(),
'languages' => array('en', 'fr', 'de'), // optional restriced list of languages
'default' => 'en'
));
// For an English-speaking user, symfony renders the widget in HTML as
<label for="language">Language</label>
<select id="language" name="language">
<option value=""/>
<option value="de">German</option>
<option value="en" selected="selected">English</option>
<option value="fr">French</option>
</select>
File Widgets
Dealing with file input tags is not more complicated than dealing with other wigets:
[php]
// Input file
$form->setWidget('picture', array('type' => 'file'));
// symfony renders the widget in HTML as
<label for="picture">Picture</label>
<input id="picture" type="file" name="picture"/>
// Whenever a form has a file widget, renderFormTag() outputs a <form> tag with the multipart option
// Editable input file
$form->setWidget('picture', array('type' => 'file_editable', 'default' => '/images/foo.png'));
// symfony renders the widget in HTML as a file input tag, together with a preview of the current file
TIP: Third-party plugins provide many additional widgets. You can easily find a rich text editor widget, a calendar widget, or other “rich UI” widgets for various JavaScript libraries. Check the Plugins repository for more details.
Handling a Form Submission
When users fill a form and submit it, the web application server needs to rerieve the data from the request and do some stuff with it. The sfForm class provides all the necessary methods to do that in a couple lines of code.
Simple Form Handling
Since widgets output as regular HTML form fields, getting their value in the action that handles the form submission is as easy as checking the related request parameters. Symfony recommends the use of the same action to display and to handle the submission of a form. For the example contact form, the action could look like this:
[php]
// in modules/foo/actions/actions.class.php
public function executeContact($request)
{
// Define the form
$this->form = new sfForm();
$this->form->setWidgets(array(
'name' => 'text',
'email' => array('type' => 'text', 'default' => 'me@example.com'),
'subject' => array('type' => 'select', 'choices' => array('Subject A', 'Subject B', 'Subject C')),
'message' => 'textarea'),
));
// Deal with the request
if(!$request->isMethod('post'))
{
// Handle the form submission
$name = $request->getParameter('name');
// Do stuff
// ...
$this->redirect('foo/bar');
}
}
If the request method is ‘GET’, this action will terminate over a sfView::SUCCESS and therefore render the contactSuccess template to display the form. If the request method is ‘POST’, then the action handles the form submission and redirects to another action. For this to work, the <form> target action must be the same as the one displaying it. That explains why the previous examples used foo/contact as a form target:
[php]
// in modules/foo/templates/contactSuccess.php
<?php echo $form->renderFormTag('foo/contact') ?>
...
Form Handling With Data Validation
In practice, there is much more to form submission handling than just getting the values entered by the user. For most form submissions, the application controller needs to:
- Check that the data is conform to a set of predefined rules (required fields, format of the email, etc.)
- Optionally transform some of the input data to make it understandable (trim whitespaces, convert dates to PHP format, etc)
- If the data is not valid, display the form again, with error messages where applicable
- If the data is correct, do some stuff and then redirect to another action
Symfony provides an automatic way to validate the submitted data against a set of predefined rules. First, define a set of validators for each field. Second, when the form is submitted, “bind” the form object with the request object (i.e., retrieve the values submitted by the user and put them in the form). Lastly, ask the form to check that the data is valid. The following example shows how to check that the value retrieved from the email widget is, indeed, an email address, and to check that the message has a minimum size of 4 characters:
[php]
// in modules/foo/actions/actions.class.php
public function executeContact($request)
{
// Define the form
$this->form = new sfForm();
$this->form->setWidgets(array(
'name' => 'text',
'email' => array('type' => 'text', 'default' => 'me@example.com'),
'subject' => array('type' => 'select', 'choices' => array('Subject A', 'Subject B', 'Subject C')),
'message' => 'textarea'),
));
$this->form->setValidators(array(
'email' => 'email',
'message' => array('type' => 'string', 'min_length' => 4))
));
// Deal with the request
if(!$request->isMethod('post'))
{
$this->form->bind($request);
if ($this->form->isValid())
{
// Handle the form submission
$name = $request->getParameter('name');
// Do stuff
// ...
$this->redirect('foo/bar');
}
}
}
setValidators() uses a similar syntax to the setWidgets() method: for each form field, you can set either a validator name (like email), or an array of parameters with a required type parameter. email and string are two of the numerous symfony validators, listed further in this chapter. Naturally, sfForm also provide a setValidator() method to add validators one by one.
To put the request data into the form and bind them together, use the sfForm::bind() method. A form must be bound with some data to check its validity.
isValid() checks that all the registered validators pass. If this is the case, isValid() returns true, and the action can proceed with the form submission. If the form is not valid, then the action terminates with the default sfView::SUCCESS and displays the form again. But the form isn’t just displayed with the default values, as the first time it was displayed. The form inputs show up filled with the data previously entered by the user, and error messages appear wherever the validators didn’t pass.

TIP: The validation process doesn’t stop when the form meets an invalid field.
isValid()processes the whole form data and checks all the fields for errors, to avoid displaying new error messages as the user corrects its mistakes and submits the form again.
Using Clean Form Data
In the previous listing, the form receives the whole request object during the binding process. The problem is that the request contains more than just the form data. It also contains headers, cookies, parameters passed as GET arguments, and all this might pollute the binding process. A good practice is to pass only the form data to the bind() method.
Fortunately, symfony offers a way to name all form inputs using an array syntax. Define the name attribute format width the setNameFormat() method in the action when you define the form, as follows:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form->setNameFormat('contact[%s]');
That way, all the generated form inputs render with a name like form[WIDGET_NAME] instead of just WIDGET_NAME:
[php]
<label for="contact_name">Name</label>
<input type="text" name="contact[name]" id="contact_name" />
...
<label for="contact_email">Email</label>
<input type="text" name="contact[email]" id="contact_email" value="me@example.com" />
...
<label for="contact_subject">Subject</label>
<select name="contact[subject]" id="contact_subject">
<option value="0">Subject A</option>
<option value="1">Subject B</option>
<option value="2">Subject C</option>
</select>
...
<label for="contact_message">Message</label>
<textarea rows="4" cols="30" name="contact[message]" id="contact_message"></textarea>
The action can now retrieve the contact request parameter into a single variable. This variable contains an array of all the data entered by the user in the form:
[php]
// in modules/foo/actions/actions.class.php
// Deal with the request
if(!$request->isMethod('post'))
{
$this->form->bind($request->getParameter('contact'));
if ($this->form->isValid())
{
// Handle the form submission
$contact = $this->form->getValues();
$name = $contact['name'];
// Do stuff
// ...
$this->redirect('foo/bar');
}
}
When the bind() method receives an array of parameters rather than a request object, symfony automatically enables an special check on the bound fields, to avoid injection of additional fields on the client side. This security feature will make the form validation fail if the array of contact parameters contains a field that is not in the original form definition.
You will notice one more difference in the action code above with the one written previously. The action uses the array of values passed by the form object ($form->getValues()) rather than the one from the request. This is because the validators have the ability to filter the input and clean it, so it’s always better to rely on the data retrieved from the form object (by way of getValues()) than the data from the request. And for composite fields (like date widgets), the data returned by getValues() is already recomposed into the original names:
[php]
// When submitted, the form controls of a 'date' widget...
<label for="contact_dob">Date of birth</label>
<select id="contact_dob_month" name="contact[dob][month]">...</select> /
<select id="contact_dob_day" name="contact[dob][day]">...</select> /
<select id="contact_dob_year" name="contact[dob][year]">...</select>
// ...result in three request parameters in the action
$contact = $request->getParameter('contact');
$month = $contact['dob']['month'];
$day = $contact['dob']['day'];
$year = $contact['dob']['year'];
$dateOfBirth = mktime(0, 0, 0, $month, $day, $year);
// But if you use getValues(), you can retrieve directly a correct date
$contact = $this->form->getValues();
$dateOfBirth = $contact['dob'];
Make it a habit to:
- Always use array syntax for your form fields (using
$this->form->setNameFormat('contact[%s]‘)) - Always use the clean post-validation version of the user’s form input (using
$this->form->getValues()).
Where do the error messages shown in the screenshot above come from? You know that a widget is made of four components, and the error message is one of them. In fact, the default (table) formatter renders a field row as follows:
[php]
<?php if($field->hasError()): ?>
<tr>
<td colspan="2">
<?php echo $field->renderError() ?> // List of errors
</td>
</tr>
<?php endif; ?>
<tr>
<th><?php echo $field->renderLabel() ?></th> // Label
<td>
<?php echo $field->render() ?> // Widget
<?php if ($field->hasHelp()): ?>
<br /><?php echo $field->renderHelp() ?> // Help
<?php endif; ?>
</td>
</tr>
Using any of the methods above, you can customize where and how the error messages appear for each field. In addition, you can display a global error message on top of the form is if is not valid:
[php]
<?php if ($form->hasErrors()): ?>
The form has some errors you need to fix.
<?php endif; ?>
Customizing Validators
In a form, all fields that do not carry a validator are optional, and all the fields that carry a validator are required. If you need to make an optional field required, just add an empty string validator on this field. On the other hand, if you need to set a field that already has a validator as optional, pass the optional parameter to the validator. For instance, the following listing shows how to make the name field required and the email field optional:
[php]
$this->form->setValidators(array(
'name' => 'string', // simple string validator, to make the name required
'email' => array('type' => 'email', 'optional' => true),
'message' => array('type' => 'string', 'min_length' => 4)
));
You can apply more than one validator on a field. For instance, you may want to check that the email field satisfies both the email and the string validators with a minimum size of 4 characters. In such a case, use the and validator to combine two validators, and put the two email and string validators into its validators:
[php]
$this->form->setValidators(array(
'name' => 'string', // simple string validator, to make the name required
'email' => array('type' => 'and', 'optional' => true, 'validators' => array(
array('type' => 'email'),
array('type' => 'string', 'min_length' => 4)
)
'message' => array('type' => 'string', 'min_length' => 4)
));
If both validators are valid, then the email field is declared valid. Similarly, you can use the or validator to combine several validators. If one of the validators is valid, then the field is declared valid.
Each invalid validator results into an error message in the field. These error messages are in English but use the symfony internationalization helpers; if your project uses other languages, you can easily translate the error messages with an i18n dictionary. Alternatively, every validator provides an errors parameter to customize its error messages. Each validator has at least two error messages: the required message and the invalid message. Some validators can display error messages for a different purpose, and will always support the overriding of the error messages through their errors parameter:
[php]
[php]
// in modules/foo/actions/actions.class.php
$this->form->setValidators(array(
'name' => 'string', // simple string validator, to make the name required
'email' => array('type' => 'email', 'errors' => array(
'required' => 'Please provide an email',
'invalid' => 'Please provide a valid email address (me@example.com)'
)),
'message' => array('type' => 'string', 'min_length' => 4, 'errors' => array(
'required' => 'Please provide a message',
'min_length' => 'Please provide a longer message (at least 4 characters)'
))
));
Naturally, these custom messages will render in the templates through i18n helpers, so any multilingual application can also translate custom error messages in a dictionary (see Chapter 13 for details).
Applying a Validator To Several Fields
The syntax used above to define validators on a form does not allow to validate that two fields are valid together. For instance, in a registration form, there are often two password fields that must match, otherwise the registration is refused. Each password field is not valid on its own, it is only valid when associated with the other field.
That’s why setValidators() accepts a multiple key to set the validators that work on several values. A typical registration form definition would look like this:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form = new sfForm();
$this->form->setWidgets(array(
'login' => 'text',
'password1' => 'text'
'password2' => 'text'
);
$this->form->setValidators(array(
'login' => 'string', // login is required
'password1' => 'string' // password1 is required
'password2' => 'string' // password2 is required
'multiple' => array('type' => 'compare', 'left' => 'password1', 'right' => 'password2')
));
The compare validator is a special multiple validator that receives all the form input values and can pick up two of them for comparison. Naturally, you can define more than one multiple validator by using the and and the or validators.
Validators
Symfony offers quite a lot of validators. Remember that each validator expects at least a type parameter, and accepts an errors parameter where you can at least customize the required and invalid error messages.
[php]
// String validator
$form->setValidator('message', array(
'type' => 'string',
'min_length' => 4,
'max_length' => 50,
'errors' => array(
'min_length' => 'Please post a longer message',
'max_length' => 'Please be less verbose',
)
));
// Number validator
$form->setValidator('age', array(
'type' => 'number', // use the 'int' type instead if you want to force integer values
'min' => 18,
'max' => 99.99,
'errors' => array(
'min' => 'You must be 18 or more to use this service',
'max' => 'Are you kidding me? People over 30 can\'t even use the Internet',
)
)):
// Email validator
$form->setValidator('email', array('type' => 'email'));
// URL validator
$form->setValidator('website', array('type' => 'url'));
// Regular expression validator
$form->setValidator('IP', array(
'type' => 'regex',
'pattern' => '^[0-9]{3}\.[0-9]{3}\.[0-9]{2}\.[0-9]{3}$'
));
Even though some form controls (like dropdown lists, checkboxes, radiobutton groups) restrict the possible choices, a malicious user can always try to hack your forms by manipulating the page with Firebug or submitting a query with a scripting language. Consequently, you should also validate fields that only accept a limited array of values:
[php]
// Boolean validator
$form->setValidator('has_signed_terms_of_service', array('type' => 'boolean'));
// Choice validator (to restrict values in a list)
$form->setValidator('subject', array(
'type' => 'choice',
'choices' => array('Subject A', 'Subject B', 'Subject C')
));
// Multiple choice validator
$form->setValidator('languages', array(
'type' => 'choice_many',
'choices' => array('en' => 'English', 'fr' => 'French', 'other')
));
I18n choice validators exist for country lists (i18n_country_choice) and language lists (i18_language_choice). These validators require a culture parameter, and accept a restricted list of countries and languages if you want to limit the possible options.
The choice validators are often used to validate choice widgets. And since you can use the choice widget for foreign key columns, so symfony also provides a validator to check that the foreign key value exists in the foreign table:
[php]
// Propel choice validator
$form->setValidator('section_id', array(
'type' => 'propel_choice',
'model' => 'Section',
'column' => 'name'
));
Another useful Model-related validator is the unique validator, which checks that a new value entered via a form doesn’t conflict with an existing value in a database column with a unique index. For instance, two users cannot have the same login, so when editing a User object with a form, you must add a unique validator on this column:
[php]
// Propel unique validator
$form->setValidator('nickname', array(
'type' => 'propel_unique',
'model' => 'User',
'column' => 'login'
));
To make your forms even more secure and avoid Cross-Site Request Forgery attacks, you can customize the CSRF_Token validator, which is enabled by default. Customizing this validator prevents the form from being submitted if it wasn’t displayed before:
[php]
// CSRF_Token validator - set the 'token' parameter to a random string that nobody knows
$form->setValidator('_csrf_token', array(
'type' => 'CSRF_token',
'token' => 'flkd445rvvrGV34G'
));
TIP: You can set the CSRF token once for the whole site in the
settings.ymlfile:
[yml]
# in apps/myapp/config/settings.yml
all:
.settings:
# Form security secret (CSRF protection)
csrf_secret: ##CSRF_SECRET## # Unique secret to enable CSRF protection or false to disable
The “multiple” validators work with the whole form, rather than a single input. You must register these validators under the multiple key. Here is a list of available multiple validators:
[php]
// compare validator - compare two fields
$form->setValidator('multiple', array(
'type' => 'compare',
'left' => 'password1',
'comparator' => '=', // default value
'right' => 'password2'
));
// Extra field validator: looks for fields in the request not present in the form
// This validator is enabled by default as soon as you bind a form to an array rather than a request
$form->setValidator('multiple', array(
'type' => 'extra_field',
'allow_extra_fields' => false,
'filter_extra_fields' => true
));
// Unique validator for models with a composite unique key
$form->setValidator('nickname', array(
'type' => 'propel_unique_composite',
'model' => 'User',
'column' => array('login', 'email')
));
Alternative Ways to Use a Form
Using YAML for Fast Form Definition
With all the widget options, validators and form parameters, the contact form definition written in the actions class looks quite messy:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form = new sfForm();
$this->form->setNameFormat('contact[%s]');
$this->form->setIdFormat('my_form_%s');
$this->form->setWidgets(array(
'name' => 'text',
'email' => array('type' => 'text', 'default' => 'me@example.com'),
'subject' => array('type' => 'select', 'choices' => array('Subject A', 'Subject B', 'Subject C')),
'message' => 'textarea'
));
$this->form->setValidators(array(
'name' => 'string',
'email' => array('type' => 'email', 'errors' => array(
'required' => 'please provide an email',
'invalid' => 'please provide a valid email address (me@example.com)'
)),
'message' => array('type' => 'string', 'min_length' => 4, 'errors' => array(
'min_length' => 'Please provide a longer message (at least 4 characters)'
))
));
Not only is this set of associative arrays difficult to read, it is also error-prone. But there is a more readable equivalent to PHP associative arrays that you already know in symfony: YAML. And sfForm offers a fromYaml() method to populate a form object with the parameters found in a YAML form definition:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form = new sfForm();
$this->form->fromYaml(dirname(__FILE__).'../config/contact_form.yml');
[yml]
# in modules/foo/config/contact_form.yml
name_format: contact[%s]
id_format: my_form_%s
widgets:
name: text
email: { type: text, default: me@example.com }
subject: { type: select, choices: [Subject A, Subject B, Subject C] }
message: textarea
validators:
name: string
email:
type: email
errors:
required: please provide an email
invalid: please provide a valid email address (me@example.com)
message:
type: string
min_length: 4
errors: { min_length: Please provide a longer message (at least 4 characters) }
The syntax of the YAML form definition follows the sfForm object API. The first time symfony parses this file, it transforms the definition into an associative array equivalent and stores it in the cache. So using the YAML form definition is both fast to write and fast to execute.
Altering a Form Object
When you use YAML form definition, the form is defined outside the action. That makes dynamic default value assignment quite difficult. That’s why the form object supports a setDefault($field, $default) and a setDefaults($array) methods that you can call in the action after importing the form from the YAML file:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form = new sfForm();
$this->form->fromYaml(dirname(__FILE__).'../config/contact_form.yml');
$this->form->setDefaults(array('email' => 'me@example.com'));
You can also override existing widget or validator settings by calling setWidget() or setValidator() on an existing field name. Symfony will merge the new parameters with the existing ones - unless you change the type parameter, in which case the previous settings are erased. Also, if you set an empty widget or validator on a field, it erases the current widget/validator.
However, widgets and validators are objects in symfony, and offer a clean API to modify their properties. Your best option to alter a form defined in a YAML file is to use this API, which is pretty self-explanatory:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form = new sfForm();
$this->form->fromYaml(dirname(__FILE__).'../config/contact_form.yml');
// Add an empty option to the list of choices of a 'choice' widget
$form->getWidget('language')->setOption('add_empty', 'Please choose a language);
// Add a 'gender' list of options widget
$form->setWidget('gender', array('type' => 'choice', 'expanded' => true, 'choices' => array('m' => 'Male', 'f' => 'Female'), 'class' => 'gender_list'));
// Change the HTML attributes of the 'subject' widget
$form->getWidget('subject')->setAttribute('disabled', 'disabled');
// Remove the 'subject' widget
$form->removeWidget('subject');
// Note: removing a widget will also remove the related validators
// Change the 'email' widget type to 'textarea'
$form->setWidget('email', array('type' => 'textarea'));
// Change the 'min_length' error in the 'message' validator
$form->getValidator('message')->setError('min_length', 'Message too short');
// Make the 'name' field required
$form->setValidator('name', 'string');
// Remove the 'email' validator
$form->removeValidator('email');
Widget and Validator classes
When you set a widget or a validator, instead of passing an array of parameters, you can pass directly a widget object or a validator object. Widget class names are prefixed by ’sfWidgetForm’, and validator classes are prefixed by ’sfValidator’.
[php]
$choices = array('m' => 'Male', 'f' => 'Female');
$form->setWidget('gender', new sfWidgetFormSelectRadio(array('choices' => $choices, 'class' => 'gender_list')));
// Same as
$form->setWidget('gender', array('type' => 'choice', 'choices' => $choices, 'class' => 'gender_list'));
$form->addValidator('name', new sfValidatorString());
// Same as
$form->addValidator('name', 'string');
Symfony uses this alternative syntax internally. This syntax allows you to use your own widgets and validators. A custom widget is simply a class extending sfWidgetForm, and providing a configure() and a render() methods. Check the code of existing widget classes for a deeper understanding of the widgets system. The next listing exposes the code of the text widget to illustrate the widget structure:
[php]
class sfWidgetFormInput extends sfWidgetForm
{
/**
* Configures the current widget.
* This method allows each widget to add options or HTML attributes during widget creation.
* Available options:
* * type: The widget type (text by default)
*
* @param array $options An array of options
* @param array $attributes An array of default HTML attributes
* @see sfWidgetForm
*/
protected function configure($options = array(), $attributes = array())
{
$this->addOption('type', 'text');
$this->setOption('is_hidden', false);
}
/**
* Renders the widget as HTML
*
* @param string $name The element name
* @param string $value The value displayed in this widget
* @param array $attributes An array of HTML attributes to be merged with the default HTML attributes
* @param array $errors An array of errors for the field
* @return string An HTML tag string
* @see sfWidgetForm
*/
public function render($name, $value = null, $attributes = array(), $errors = array())
{
return $this->renderTag('input', array_merge(
array('type' => $this->getOption('type'), 'name' => $name, 'value' => $value),
$attributes
));
}
}
A validator class must extend sfValidatorBase and provide a configure() and a doClean() methods. Why doClean() and not validate()? Because validators do two things: they check that the input fulfills a set of rules, and they optionally clean the input (for instance by forcing the type, trimming, converting date strings to timestamp, etc.). So the doClean() method must return the cleaned input, or throw a sfValidatorError exception if the input doesn’t satisfy any of the validator rules. Here is an illustration of this concept, with the code of the int validator.
[php]
class sfValidatorInteger extends sfValidatorBase
{
/**
* Configures the current validator.
* This method allows each validator to add options and error messages during validator creation.
* Available options:
* * max: The maximum value allowed
* * min: The minimum value allowed
* Available error codes:
* * max
* * min
*
* @param array $options An array of options
* @param array $messages An array of error messages
* @see sfValidatorBase
*/
protected function configure($options = array(), $messages = array())
{
$this->addOption('min');
$this->addOption('max');
$this->addMessage('max', '"%value%" must be less than %max%.');
$this->addMessage('min', '"%value%" must be greater than %min%.');
$this->setMessage('invalid', '"%value%" is not an integer.');
}
/**
* Cleans the input value.
*
* @param mixed $value The input value
* @return mixed The cleaned value
* @throws sfValidatorError
*/
protected function doClean($value)
{
$clean = intval($value);
if (strval($clean) != $value)
{
throw new sfValidatorError($this, 'invalid', array('value' => $value));
}
if ($this->hasOption('max') && $clean > $this->getOption('max'))
{
throw new sfValidatorError($this, 'max', array('value' => $value, 'max' => $this->getOption('max')));
}
if ($this->hasOption('min') && $clean < $this->getOption('min'))
{
throw new sfValidatorError($this, 'min', array('value' => $value, 'min' => $this->getOption('min')));
}
return $clean;
}
}
Check the symfony API documentation for widget and validator classes names and syntax.
Form Classes
In a similar fashion, if you want to reuse a form object at several parts of your project, you should create a form class with the same properties and instantiate it in all the actions using it. For instance, here is how you could create a class for the contact form:
[php]
// in lib/form/ContactForm.class.php
class ContactForm extends sfForm
{
protected static $subjects = array('Subject A', 'Subject B', 'Subject C');
public function configure()
{
$this->setNameFormat('contact[%s]');
$this->setIdFormat('my_form_%s');
$this->setWidgets(array(
'name' => 'text',
'email' => 'text',
'subject' => (array('type' => 'choice', 'choices' => self::$subjects)),
'message' => 'textarea',
));
$this->setValidators(array(
'name' => 'string',
'email' => array('type' => 'email', 'errors' => array(
'required' => 'please provide an email',
'invalid' => 'please provide a valid email address (me@example.com)'
)),
'message' => array('type' => 'string', 'min_length' => 4, 'errors' => array(
'min_length' => 'Please provide a longer message (at least 4 characters)'
))
));
$this->setDefaults(array(
'email' => 'me@example.com'
));
}
}
Now getting a contact form object in the action has never been easier:
[php]
// in modules/foo/actions/actions.class.php
// Define the form
$this->form = new ContactForm();
You can alter this form the same way as the form created from the YAML definition.
Forms Based on a Model
Forms are the primary way to edit database records in web applications. And most forms in symfony applications allow the editing of a Model object. But the information necessary to build a form to edit a model already exists: it is in the schema. So symfony provides a form generator for model objects, that makes the creation of model-editing forms a snap.
Note: Similar features to the ones described below exist for Doctrine.
Generating Model Forms
Symfony can deduce the widget types and the validators to use for a model editing form, based on the schema. Take the following schema, for instance:
[yml]
// config/schema.yml
propel:
article:
id: ~
title: { type: varchar(255), required: true }
slug: { type: varchar(255), required: true, index: unique }
content: longvarchar
is_published: { type: boolean, required: true }
author_id: { type: integer, required: true, foreignTable: author, foreignReference: id, OnDelete: cascade }
created_at: ~
author:
id: ~
first_name: varchar(20)
last_name: varchar(20)
email: { type: varchar(255), required: true, index: unique }
active: boolean
A form to edit an Article object should use a hidden widget for the id, a text widget for the title, a string validator for the title, etc. Symfony generates the form for you, provided that you call the propel:build-forms task:
$ php symfony propel:build-forms
For each table in the model, this command creates two files under the lib/form/ directory: a BaseXXXForm class, overridden each time you call the propel:build-forms task, and an empty XXXForm class, extending the previous one. It is the same system as the Propel model classes generation.
The generated lib/form/base/BaseArticleForm.class.php contains the translation into widgets and validators of the columns defined for the article table in the schema.yml:
[php]
class BaseArticleForm extends BaseFormPropel
{
public function setup()
{
$this->setWidgets(array(
'id' => 'hidden',
'title' => 'text',
'slug' => 'text',
'content' => 'textarea',
'is_published' => 'checkbox',
'author_id' => array('type' => 'choice', 'model' => 'Author', 'add_empty' => false),
'created_at' => 'datetime',
));
$this->setValidators(array(
'id' => array('type' => 'propel_choice', 'model' => 'Article', 'column' => 'id', 'required' => false),
'title' => array('type' => 'string', 'max_length' => 255),
'slug' => array('type' => 'string', 'max_length' => 255),
'content' => array('type' => 'string', 'required' => false),
'is_published' => 'boolean',
'author_id' => array('type' => 'propel_choice', 'model' => 'Author', 'column' => 'id'),
'created_at' => array('type' => 'datetime', 'required' => false),
));
$this->setMultipleValidator(
array('type' => 'propel_unique', 'model' => 'Article', 'column' => array('slug'))
);
$this->setNameFormat('article[%s]');
parent::setup();
}
public function getModelName()
{
return 'Article';
}
}
Notice that even though the id column is an Integer, symfony checks that the submitted id exists in the table using a propel_choice validator. The form generator always sets the strongest validation rules, to ensure the cleanest data in the database.
Using Model Forms
You can customize generated form classes for your entire project by adding code to the empty ArticleForm::configure() method. If your modifications concern only one action, create an instance of the form in the action, and use the public form methods to override the default form settings in the action, as explained previously in this chapter.
Here is an example of model form handling in an action. In this form, the slug validator is modified to make it optional, and the author_id widget is customized to display only a subset of authors - the ‘active’ ones.
[php]
// in modules/foo/actions/actions.class.php
public function executeEditArticle($request)
{
$this->form = new ArticleForm();
$c = new Criteria();
$c->add(AuthorPeer::ACTIVE, true);
$this->form->getWidget('author_id')->setOption('criteria', $c);
$this->form->getValidator('slug')->setOption('required', false);
$this->form->setObject(ArticlePeer::retrieveByPk($request->getParameter('id')));
if ($request->isMethod('post'))
{
$this->form->bind($request->getParameter('article'));
if ($this->form->isValid())
{
$article = $this->form->save();
$this->redirect('article/edit?id='.$author->getId());
}
}
}
Instead of setting default values through an associative array (using setDefaults()), Model forms use a Model object to initialize the widget values (with setObject()). To display an empty form, just pass a new Model object.
The form submission handling is greatly simplified by the fact that the form object has an embedded Model object. Calling $this->form->bind($request->getParameter('article')) updates the embedded Article object with the request values (the data submitted by the user). Then, calling $this->form->save() on a valid form triggers the save() method on the Article object, as well as on the related objects if they exist.
TIP: The action code required to deal with a form is pretty much always the same, but that’s not a reason to copy it from one module to the other. Symfony provides a module generator that creates the whole action and template code to manipulate a Model object through symfony forms. Check the Chapter 14 for more details about the CRUD generator.
Conclusion
The symfony form component is an entire framework on its own. It facilitates the display of forms in the view through widgets, it facilitates validation and handling of forms in the controller through validators, and it facilitates the edition of Model objects through Model forms. Although designed with a clear MVC separation, the form sub-framework is always easy to use. Most of the time, code generation will reduce your custom form code to a few lines.
There is much more in symfony form classes than what this chapter exposes. In fact, there is an entire book describing all its features through usage examples. And if the form framework itself doesn’t provide the widget or the validator you need, it is designed in such an extensible way that you will only need to write a single class to get exactly what you need.
Possibly related posts (automatically generated):
Thank you SO much for this, François - as far as I can tell (I’ve not had a chance to experiment yet) it contains pretty much everything I need to know about the new Forms framework and will save me from reading the Forms book. If I have any changes, or find any bugs, I’ll let you know! Thanks again!
p.s. any chance you could make it available as a PDF?
Excellent article about this new form system.
I will put it in my bookmarks.
Thank you for your efforts.
I think it will be good to somehow highlight the source code that won’t work without plugin you are going to develop.
I second Mike’s opinion. Highlighting new things will make it clear what needs to be implemented in the plugin as well as differentiate what currently works and what doesn’t. Maybe some things work and I don’t know yet or viceversa.
Wowwww thank you very much François, it seems very easu to learn to me. If i find any nug i will post it…
Okay, fine. Where’s the patch?
@Mike and @Trak: 95% of the code exposed here doesn’t work with the current implementation. All the features exist (apart from the am/pm time format ;)), but they use a different syntax.
This post gives great examples of good form development.
I have one question/idea :
When using a sfFormWidget (choice/select) :
You said in your post : “Even though some form controls (like dropdown lists, checkboxes, radiobutton groups) restrict the possible choices, a malicious user can always try to hack your forms by manipulating the page with Firebug or submitting a query with a scripting language.”
Could’t symfony provide a way to automatically validate the choices (from the request) based on what was initially displayed in the form ?
This way you wouldn’t have to add the values (in an array) and then validate those afterwards again (double work)
Something like this ?
// Choice validator (to restrict values in a list) $form->setValidator(’subject’, array( ‘type’ => ‘choice’, ‘auto’ => true ));
i don’t know if it’s technically possible, just an idea.
Thank you for good description. Something is new for me, looking that I’m using forms 1.1 during 3 months.
I have a question - what is the best way to build application, if I have dynamically created fields on user side? I’m creating them using javascript, and want to validate them also dynamically. How to do it?
@snk00sj: That’s a great idea, except that the current architecture of the Form system doesn’t allow it. Validators don’t know about widgets, and widgets don’t know about validators. They are on separate parts of the MVC layering, VERY decoupled from each other.
@Alex: Maybe sfValidatorSchemaForEach is the answer for you, but you should try the symfony mailing-list for support.
@francois : that’s what i figured, i don’t know about the current architecture of widgets/validators. Too bad though, this allows developers not paying attention to open the door for form hacking…
@snk00sj: You should check out Zoop Framework. Their form support is further along and better integrated that that found in Symfony. If your app uses a lot of forms, Zoop is the way to go.
Zoop has two components to manage forms.
The first is GuiControls, which are analogous to Symfony’s widgets, except they have validation integrated and also can be created in either the controller or the view.
The second is called Forms. Forms automatically creates a webform based on a database record. You can customize the form once created. It is smart enough to handle even things like foreign key relationships. It spans across MVC and uses GuiControls which means it also has integrated validation. It makes for very quick CRUD work.
I really like this rewritten chapter 10! I especially like that the beginning is simple and builds on what I’ve learned in chapter 6, and that I can define a good form within actions.class.php. The first half of the chapter gives me everything I need for 80% of my forms, and the rest gives me solutions for the complicated cases. It makes “easy things easy and hard things possible”, to quote Larry Wall I think.
I am not convinced that the YAML option (”Using YAML for Fast Form Definition” section) is worth it. I could immediately think of a couple cases in my work where YAML would be too restrictive and would actually introduce more complexity than just using more verbose (but flexible) PHP expressions– like if I need to load the choices array from a YAML file or other data source, or add JavaScript to a pair of fields.
Finally, one English nitpick:
would be better something like,
I checked out some of the widget examples, and unfortunately they do not seem to work; symfony requires you to specify a type of sfWidget when calling setWidgets.
Is there any way to look/access some of this beta documentation? I’ve learned a lot just from reading this unreleased chapter, and I would love it if I could help be a technical reader.
@Ken: Re-read the notice on top of this article, and also go to read the post linked there. The way things are described here is not the way they work now.
I’d love people to do some technical reading, but you must at least prove that you have read the first paragraph to qualify for that…
@Nathan: Thanks for the niptick, I corrected the post.
As for the YAML syntax, use it when it makes sense, not when it doesn’t make sense, like in the axample you give. But the YAML syntax proposed here could be the base of the symfony 1.2 admin generator syntax (cf. http://groups.google.com/group/symfony-devs/browse_thread/thread/2fe8f9709ed9458c).
Hi again Francois, is there someone from the symfony team that have given a response to this suggestion on the documentation?????
As i said before i liked very much your work but it’s important too to know what symfony team think about this.
[...] Zaninotto has posted a (large) new tutorial to his blog today detailing the forms functionality in the symfony [...]
Francois,
Sorry about the misunderstanding, my first reading of the chapter was done more under discovering a technically correct solution to implementing forms rather than the style of the draft. I only suggested the technical reader thing because I really do feel like there is a dearth of information regarding the usage of forms in Symfony 1.1. I wanted to both see if there was anything I could do to help get more useful documentation out there, and learn more about Symfony myself (also, I started perusing this page with the intent of finding out a solution to a current forms problem I have without realizing this was still in alpha). At the moment, I feel that while it’s easy to learn how to embed simple widgets, it becomes much more difficult in the current framework to do anything more complex, like use checkboxes instead of a selection box to check off multiple options.
@saganxis: Fabien read the post and has a lot of comments. I expect some feedback from him later this week.
well is great to know that. I hope his opinion ‘d be possitive!! waiting for that moment…. I want to tell you that wheter the fabien opiion were possitive or not your work is very important for the symfony project…and fo course it’d be a plasure that the symfony team decide to include your chapter(and of course modify the API form) in the symfony book…it’s great to have you close to symfony… (…and sorry for my english ;))
Hello François,
I read quickly but with interest this post. IMHO, writting the form as Yaml files is a good idea because it allows to write less code, and this code is pretty much all over the same. But there is something I would like to know.
You give an example Loading a Yaml file giving its hard encoded path with a FromYaml method. If you give the path here, it says that it might change and then you will have to change path in some places you hopefully remember somewhere in your project.
Wouldn’t it be better if here we had a class generation from the Yaml in the cache and a form class extending it in the code (exactly the same process as admin generator) ?
@greg: To me, what the YAML file generates is not a class but a set of method calls on an object (exactly the equivalent of what is described before the YAML syntax). This is to avoid generating two classes from the YAML (one Base class, and another that the user can override), which adds more complexity than necessary.
But I agree with you that generating a form class from YAML is a more powerful feature, and could be used under the hood.
Hi François,
I’m a newcomer to symfony and that’s a very good idea. I read the actual API book about forms in sf1.1 but yours sounds much better.
In this doc you are talking about a calendar widget. Is it about to be released or does it already exist ? I checked the plugins repository without success…
Thank you
A lot of this just looks like the CakePHP FormHelper API.
@Nate: You didn’t read correctly. The sfForm framework is not a set of helpers. It’s a full-featured MVC abstraction for form handling in PHP applications, independent of symfony. CakePHP handles (some) of the view needs, in PHP4, and is far from provoding the abstraction and the power of sfForms.
Read again the documentation, and check the sfForms book for a better insight of the component.
@saganxis: You can read my response on my blog:
http://www.aide-de-camp.org/article/4/en/chapter-10-forms-revisited
@Steve Francia: Are you talking about this? http://zoopframework.com/docs/user-manual/components/forms
This doesn’t look that easy, clean and powerful to me at first sight.
Why is there no documentation about the embedForm() anywhere ?
Same remark as Arkante : no way to find a documentation about embedForm()…
Good article. I started using symfony 1.1 last project but all that complexicity of sfForms made me to make a switch to be compatibile with 1.0. I asked my friends developers how they do stuff with sfForms and shockingly most of them use old good helpers.
Example: I am trying to do file upload with sfForms. First extended myWidgetFormFile extends sfWidgetForm because did not know that sfWidgetFormInputFile() exists next how to handle submission? $this->form->bind($request->getParameter(’child’),$_FILES) it is very confusing. To improve docs of symfony whoever is responsible for should look at docs of Zend Doctrine and CodeIgniter.
Great article once again