When writing templates, much of a developer's time is devoted to forms. Despite this, forms are generally poorly designed. Since much attention is required to deal with default values, formatting, validation, repopulation, and form handling in general, some developers tend to skim over some important details in the process. Accordingly, symfony devotes special attention to this topic. This chapter describes the tools that automate many of these requirements while speeding up forms development:
- The form helpers provide a faster way to write form inputs in templates, especially for complex elements such as dates, drop-down lists, and rich text.
- When a form is devoted to editing the properties of an object, the templating can be further accelerated by using object form helpers.
- The YAML validation files facilitate form validation and repopulation.
- Validators package the code required to validate input. Symfony bundles validators for the most common needs, and it is very easy to add custom validators.
Form Helpers
In templates, HTML tags of form elements are very often mixed with PHP code. Form helpers in symfony aim to simplify this task and to avoid opening <?php echo
tags repeatedly in the middle of <input>
tags.
Main Form Tag
As explained in the previous chapter, you must use the form_tag()
helper to create a form, since it transforms the action given as a parameter into a routed URL. The second argument can support additional options--for instance, to change the default method
, change the default enctype
, or specify other attributes. Listing 10-1 shows examples.
Listing 10-1 - The form_tag()
Helper
<?php echo form_tag('test/save') ?> => <form method="post" action="/path/to/save"> <?php echo form_tag('test/save', 'method=get multipart=true class=simpleForm') ?> => <form method="get" enctype="multipart/form-data" class="simpleForm"action="/path/to/save">
As there is no need for a closing form helper, you should use the HTML </form>
tag, even if it doesn't look good in your source code.
Standard Form Elements
With form helpers, each element in a form is given an id attribute deduced from its name attribute by default. This is not the only useful convention. See Listing 10-2 for a full list of standard form helpers and their options.
Listing 10-2 - Standard Form Helpers Syntax
// Text field (input) <?php echo input_tag('name', 'default value') ?> => <input type="text" name="name" id="name" value="default value" /> // All form helpers accept an additional options parameter // It allows you to add custom attributes to the generated tag <?php echo input_tag('name', 'default value', 'maxlength=20') ?> => <input type="text" name="name" id="name" value="default value" maxlength="20" /> // Long text field (text area) <?php echo textarea_tag('name', 'default content', 'size=10x20') ?> => <textarea name="name" id="name" cols="10" rows="20"> default content </textarea> // Check box <?php echo checkbox_tag('single', 1, true) ?> <?php echo checkbox_tag('driverslicense', 'B', false) ?> => <input type="checkbox" name="single" id="single" value="1" checked="checked" /> <input type="checkbox" name="driverslicense" id="driverslicense" value="B" /> // Radio button <?php echo radiobutton_tag('status[]', 'value1', true) ?> <?php echo radiobutton_tag('status[]', 'value2', false) ?> => <input type="radio" name="status[]" id="status_value1" value="value1" checked="checked" /> <input type="radio" name="status[]" id="status_value2" value="value2" /> // Dropdown list (select) <?php echo select_tag('payment', '<option selected="selected">Visa</option> <option>Eurocard</option> <option>Mastercard</option>') ?> => <select name="payment" id="payment"> <option selected="selected">Visa</option> <option>Eurocard</option> <option>Mastercard</option> </select> // List of options for a select tag <?php echo options_for_select(array('Visa', 'Eurocard', 'Mastercard'), 0) ?> => <option value="0" selected="selected">Visa</option> <option value="1">Eurocard</option> <option value="2">Mastercard</option> // Dropdown helper combined with a list of options <?php echo select_tag('payment', options_for_select(array( 'Visa', 'Eurocard', 'Mastercard' ), 0)) ?> => <select name="payment" id="payment"> <option value="0" selected="selected">Visa</option> <option value="1">Eurocard</option> <option value="2">Mastercard</option> </select> // To specify option names, use an associative array <?php echo select_tag('name', options_for_select(array( 'Steve' => 'Steve', 'Bob' => 'Bob', 'Albert' => 'Albert', 'Ian' => 'Ian', 'Buck' => 'Buck' ), 'Ian')) ?> => <select name="name" id="name"> <option value="Steve">Steve</option> <option value="Bob">Bob</option> <option value="Albert">Albert</option> <option value="Ian" selected="selected">Ian</option> <option value="Buck">Buck</option> </select> // Dropdown list with multiple selection (selected values can be an array) <?php echo select_tag('payment', options_for_select( array('Visa' => 'Visa', 'Eurocard' => 'Eurocard', 'Mastercard' => 'Mastercard'), array('Visa', 'Mastercard'), ), array('multiple' => true))) ?> => <select name="payment[]" id="payment" multiple="multiple"> <option value="Visa" selected="selected">Visa</option> <option value="Eurocard">Eurocard</option> <option value="Mastercard">Mastercard</option> </select> // Drop-down list with multiple selection (selected values can be an array) <?php echo select_tag('payment', options_for_select( array('Visa' => 'Visa', 'Eurocard' => 'Eurocard', 'Mastercard' => 'Mastercard'), array('Visa', 'Mastercard') ), 'multiple=multiple') ?> => <select name="payment[]" id="payment" multiple="multiple"> <option value="Visa" selected="selected">Visa</option> <option value="Eurocard">Eurocard</option> <option value="Mastercard" selected="selected">Mastercard</option> </select> // Upload file field <?php echo input_file_tag('name') ?> => <input type="file" name="name" id="name" value="" /> // Password field <?php echo input_password_tag('name', 'value') ?> => <input type="password" name="name" id="name" value="value" /> // Hidden field <?php echo input_hidden_tag('name', 'value') ?> => <input type="hidden" name="name" id="name" value="value" /> // Submit button (as text) <?php echo submit_tag('Save') ?> => <input type="submit" name="submit" value="Save" /> // Submit button (as image) <?php echo submit_image_tag('submit_img') ?> => <input type="image" name="submit" src="/legacy/images/submit_img.png" />
The submit_image_tag()
helper uses the same syntax and has the same advantages as the image_tag()
.
note
For radio buttons, the id
attribute is not set by default to the value of the name
attribute, but to a combination of the name and the value. That's because you need to have several radio button tags with the same name to obtain the automated "deselecting the previous one when selecting another" feature, and the id=name
convention would imply having several HTML tags with the same id
attribute in your page, which is strictly forbidden.
Symfony offers specialized form helpers to do asynchronous requests in the background. The next chapter, which focuses on Ajax, provides more details.
Date Input Widgets
Forms are often used to retrieve dates. Dates in the wrong format are the main reason for form-submission failures. The input_date_tag()
helper can assist the user in entering a date with an interactive JavaScript calendar, if you set the rich
option to true
, as shown in Figure 10-1.
Figure 10-1 - Rich date input tag
If the rich
option is omitted, the helper echoes three <select>
tags populated with a range of months, days, and years. You can display these drop-downs separately by calling their helpers (select_day_tag()
, select_month_tag()
, and select_year_tag()
). The default values of these elements are the current day, month, and year. Listing 10-3 shows the input date helpers.
Listing 10-3 - Input Date Helpers
<?php echo input_date_tag('dateofbirth', '2005-05-03', 'rich=true') ?> => a text input tag together with a calendar widget // The following helpers require the DateForm helper group <?php use_helper('DateForm') ?> <?php echo select_day_tag('day', 1, 'include_custom=Choose a day') ?> => <select name="day" id="day"> <option value="">Choose a day</option> <option value="1" selected="selected">01</option> <option value="2">02</option> ... <option value="31">31</option> </select> <?php echo select_month_tag('month', 1, 'include_custom=Choose a month use_short_month=true') ?> => <select name="month" id="month"> <option value="">Choose a month</option> <option value="1" selected="selected">Jan</option> <option value="2">Feb</option> ... <option value="12">Dec</option> </select> <?php echo select_year_tag('year', 2007, 'include_custom=Choose a year year_end=2010') ?> => <select name="year" id="year"> <option value="">Choose a year</option> <option value="2006">2006</option> <option value="2007" selected="selected">2007</option> ... </select>
The accepted date values for the input_date_tag()
helper are the ones recognized by the strtotime()
PHP function. Listing 10-4 shows which formats can be used, and Listing 10-5 shows the ones that must be avoided.
Listing 10-4 - Accepted Date Formats in Date Helpers
// Work fine <?php echo input_date_tag('test', '2006-04-01', 'rich=true') ?> <?php echo input_date_tag('test', 1143884373, 'rich=true') ?> <?php echo input_date_tag('test', 'now', 'rich=true') ?> <?php echo input_date_tag('test', '23 October 2005', 'rich=true') ?> <?php echo input_date_tag('test', 'next tuesday', 'rich=true') ?> <?php echo input_date_tag('test', '1 week 2 days 4 hours 2 seconds', 'rich=true') ?> // Return null <?php echo input_date_tag('test', null, 'rich=true') ?> <?php echo input_date_tag('test', '', 'rich=true') ?>
Listing 10-5 - Incorrect Date Formats in Date Helpers
// Date zero = 01/01/1970 <?php echo input_date_tag('test', 0, 'rich=true') ?> // Non-English date formats don't work <?php echo input_date_tag('test', '01/04/2006', 'rich=true') ?>
Rich Text Editing
Rich text editing is also possible in a <textarea>
tag, thanks to the integration of the TinyMCE and FCKEditor widgets. They provide a word-processor-like interface with buttons to format text as bold, italic, and other styles, as shown in Figure 10-2.
Figure 10-2 - Rich text editing
Both widgets require manual installation. As the procedure is the same for the two widgets, only the TinyMCE rich text editing is described here. You need to download the editor from the project website (http://tinymce.moxiecode.com/) and unpack it in a temporary folder. Copy the tinymce/jscripts/tiny_mce/
directory into your project web/js/
directory, and define the path to the library in settings.yml
, as shown in Listing 10-6.
Listing 10-6 - Setting Up the TinyMCE Library Path
all: .settings: rich_text_js_dir: js/tiny_mce
Once this is done, toggle the use of rich text editing in text areas by adding the rich=true
option. You can also specify custom options for the JavaScript editor using the tinymce_options
option. Listing 10-7 shows examples.
Listing 10-7 - Rich Text Area
<?php echo textarea_tag('name', 'default content', 'rich=true size=10x20') ?> => a rich text edit zone powered by TinyMCE <?php echo textarea_tag('name', 'default content', 'rich=true size=10x20 tinymce_options=language:"fr",theme_advanced_buttons2:"separator"') ?> => a rich text edit zone powered by TinyMCE with custom parameters
Country and Language Selection
You may need to display a country selection field. But since country names are not the same in all languages, the options of a country drop-down list should vary according to the user culture (see Chapter 13 for more information about cultures). As shown in Listing 10-8, the select_country_tag()
helper does it all for you: It internationalizes country names and uses the standard ISO country codes for values.
Listing 10-8 - Select Country Tag Helper
<?php echo select_country_tag('country', 'AL') ?> => <select name="country" id="country"> <option value="AF">Afghanistan</option> <option value="AL" selected="selected">Albania</option> <option value="DZ">Algeria</option> <option value="AS">American Samoa</option> ...
Similar to select_country_tag()
helper, the select_language_tag()
helper displays a list of languages, as shown in Listing 10-9.
Listing 10-9 - Select Language Tag Helper
<?php echo select_language_tag('language', 'en') ?> => <select name="language" id="language"> ... <option value="elx">Elamite</option> <option value="en" selected="selected">English</option> <option value="enm">English, Middle (1100-1500)</option> <option value="ang">English, Old (ca.450-1100)</option> <option value="myv">Erzya</option> <option value="eo">Esperanto</option> ...
Form Helpers for Objects
When form elements are used to edit the properties of an object, standard link helpers can become tedious to write. For instance, to edit the telephone
attribute of a Customer
object, you would write this:
<?php echo input_tag('telephone', $customer->getTelephone()) ?> => <input type="text" name="telephone" id="telephone" value="0123456789" />
To avoid repeating the attribute name, symfony provides an alternative object form helper for each form helper. An object form helper deduces the name and the default value of a form element from an object and a method name. The previous input_tag()
is equivalent to this:
<?php echo object_input_tag($customer, 'getTelephone') ?> => <input type="text" name="telephone" id="telephone" value="0123456789" />
The economy might not look crucial for the object_input_tag()
. However, every standard form helper has a corresponding object form helper, and they all share the same syntax. It makes generation of forms quite straightforward. That's why the object form helpers are used extensively in the scaffolding and generated administrations (see Chapter 14). Listing 10-10 lists the object form helpers.
Listing 10-10 - Object Form Helpers Syntax
<?php echo object_input_tag($object, $method, $options) ?> <?php echo object_input_date_tag($object, $method, $options) ?> <?php echo object_input_hidden_tag($object, $method, $options) ?> <?php echo object_textarea_tag($object, $method, $options) ?> <?php echo object_checkbox_tag($object, $method, $options) ?> <?php echo object_select_tag($object, $method, $options) ?> <?php echo object_select_country_tag($object, $method, $options) ?> <?php echo object_select_language_tag($object, $method, $options) ?>
There is no object_password_tag()
helper, since it is a bad practice to give a default value to a password tag, based on something the user has previously entered.
caution
Unlike the regular form helpers, the object form helpers are available only if you declare explicitly the use of the Object
helper group in your template with use_helper('Object')
.
The most interesting of all object form helpers are objects_for_select()
and object_select_tag()
, which concern drop-down lists.
Populating Drop-Down Lists with Objects
The options_for_select()
helper, described previously with the other standard helpers, transforms a PHP associative array into an options list, as shown in Listing 10-11.
Listing 10-11 - Creating a List of Options Based on an Array with options_for_select()
<?php echo options_for_select(array( '1' => 'Steve', '2' => 'Bob', '3' => 'Albert', '4' => 'Ian', '5' => 'Buck' ), 4) ?> => <option value="1">Steve</option> <option value="2">Bob</option> <option value="3">Albert</option> <option value="4" selected="selected">Ian</option> <option value="5">Buck</option>
Suppose that you already have an array of objects of class Author
, resulting from a Propel query. If you want to build a list of options based on this array, you will need to loop on it to retrieve the id
and the name
of each object, as shown in Listing 10-12.
Listing 10-12 - Creating a List of Options Based on an Array of Objects with options_for_select()
// In the action $options = array(); foreach ($authors as $author) { $options[$author->getId()] = $author->getName(); } $this->options = $options; // In the template <?php echo options_for_select($options, 4) ?>
This kind of processing happens so often that symfony has a helper to automate it: objects_for_select()
, which creates an option list based directly on an array of objects. The helper needs two additional parameters: the method names used to retrieve the value
and the text contents of the <option>
tags to be generated. So Listing 10-12 is equivalent to this simpler form:
<?php echo objects_for_select($authors, 'getId', 'getName', 4) ?>
That's smart and fast, but symfony goes even further, when you deal with foreign key columns.
Creating a Drop-Down List Based on a Foreign Key Column
The values a foreign key column can take are the primary key values of the foreign table records. If, for instance, the article
table has an author_id
column that is a foreign key to an author
table, the possible values for this column are the id
of all the records of the author
table. Basically, a drop-down list to edit the author of an article would look like Listing 10-13.
Listing 10-13 - Creating a List of Options Based on a Foreign Key with objects_for_select()
<?php echo select_tag('author_id', objects_for_select( AuthorPeer::doSelect(new Criteria()), 'getId', '__toString', $article->getAuthorId() )) ?> => <select name="author_id" id="author_id"> <option value="1">Steve</option> <option value="2">Bob</option> <option value="3">Albert</option> <option value="4" selected="selected">Ian</option> <option value="5">Buck</option> </select>
The object_select_tag()
does all that by itself. It displays a drop-down list populated with the name of the possible records of the foreign table. The helper can guess the foreign table and foreign column from the schema, so its syntax is very concise. Listing 10-13 is equivalent to this:
<?php echo object_select_tag($article, 'getAuthorId') ?>
The object_select_tag()
helper guesses the related peer class name (AuthorPeer
in the example) based on the method name passed as a parameter. However, you can specify your own class by setting the related_class
option in the third argument. The text content of the <option>
tags is the record name, which is the result of the __toString()
method of the object class (if $author->__toString()
method is undefined, the primary key is used instead). In addition, the list of options is built from a doSelect() method with an empty criteria value; it returns all the records ordered by creation date. If you prefer to display only a subset of records with a specific ordering, create a method in the peer class returning this selection as an array of objects, and set it in the peer_method
option. Lastly, you can add a blank option or a custom option at the top of the drop-down list by setting the include_blank and include_custom options. Listing 10-14 demonstrates these different options for the object_select_tag()
helper.
Listing 10-14 - Options of the object_select_tag()
Helper
// Base syntax <?php echo object_select_tag($article, 'getAuthorId') ?> // Builds the list from AuthorPeer::doSelect(new Criteria()) // Change the peer class used to retrieve the possible values <?php echo object_select_tag($article, 'getAuthorId', 'related_class=Foobar') ?> // Builds the list from FoobarPeer::doSelect(new Criteria()) // Change the peer method used to retrieve the possible values <?php echo object_select_tag($article, 'getAuthorId','peer_method=getMostFamousAuthors') ?> // Builds the list from AuthorPeer::getMostFamousAuthors(new Criteria()) // Add an <option value=""> </option> at the top of the list <?php echo object_select_tag($article, 'getAuthorId', 'include_blank=true') ?> // Add an <option value="">Choose an author</option> at the top of the list <?php echo object_select_tag($article, 'getAuthorId', 'include_custom=Choose an author') ?>
Updating Objects
A form completely dedicated to editing object properties by using object helpers is easier to handle in an action. For instance, if you have an object of class Author
with name
, age
, and address
attributes, the form can be coded as shown in Listing 10-15.
Listing 10-15 - A Form with Only Object Helpers
<?php echo form_tag('author/update') ?> <?php echo object_input_hidden_tag($author, 'getId') ?> Name: <?php echo object_input_tag($author, 'getName') ?><br /> Age: <?php echo object_input_tag($author, 'getAge') ?><br /> Address: <br /> <?php echo object_textarea_tag($author, 'getAddress') ?> </form>
The update
action of the author
module, called when the form is submitted, can simply update the object with the fromArray()
modifier generated by Propel, as shown in Listing 10-16.
Listing 10-16 - Handling a Form Submission Based on Object Form Helpers
public function executeUpdate () { $author = AuthorPeer::retrieveByPk($this->getRequestParameter('id')); $this->forward404Unless($author); $author->fromArray($this->getRequest()->getParameterHolder()->getAll(),BasePeer::TYPE_FIELDNAME); $author->save(); return $this->redirect('/author/show?id='.$author->getId()); }
Form Validation
Chapter 6 explained how to use the validateXXX()
methods in the action class to validate the request parameters. However, if you use this technique to validate a form submission, you will end up rewriting the same portion of code over and over. Symfony provides an alternative form-validation technique, relying on only a YAML file, instead of PHP code in the action class.
To demonstrate the form-validation features, let's first consider the sample form shown in Listing 10-17. It is a classic contact form, with name
, email
, age
, and message
fields.
Listing 10-17 - Sample Contact Form, in modules/contact/templates/indexSuccess.php
<?php echo form_tag('contact/send') ?> Name: <?php echo input_tag('name') ?><br /> Email: <?php echo input_tag('email') ?><br /> Age: <?php echo input_tag('age') ?><br /> Message: <?php echo textarea_tag('message') ?><br /> <?php echo submit_tag() ?> </form>
The principle of form validation is that if a user enters invalid data and submits the form, the next page should show an error message. Let's define what valid data should be for the sample form, in plain English:
- The
name
field is required. It must be a text entry between 2 and 100 characters. - The
email
field is required. It must be a text entry between 2 and 100 characters, and it must be a valid e-mail address. - The
age
field is required. It must be an integer between 0 and 120. - The
message
field is required.
You could define more complex validation rules for the contact form, but these are just fine for a demonstration of the validation possibilities.
note
Form validation can occur on the server side and/or on the client side. The server-side validation is compulsory to avoid corrupting a database with wrong data. The client-side validation is optional, though it greatly enhances the user experience. The client-side validation is to be done with custom JavaScript.
Validators
You can see that the name
and email
fields in the example share common validation rules. Some validation rules appear so often in web forms that symfony packages the PHP code that implements them into validators. A validator is simple class that provides an execute()
method. This method expects the value of a field as parameter, and returns true
if the value is valid and false
otherwise.
Symfony ships with several validators (described in the "Standard Symfony Validators" section later in this chapter), but let's focus on the sfStringValidator
for now. This validator checks that an input is a string, and that its size is between two specified character amounts (defined when calling the initialize()
method). That's exactly what is required to validate the name
field. Listing 10-18 shows how to use this validator in a validation method.
Listing 10-18 - Validating Request Parameters with Reusable Validators, in modules/contact/action/actions.class.php
public function validateSend() { $name = $this->getRequestParameter('name'); // The name field is required if (!$name) { $this->getRequest()->setError('name', 'The name field cannot be left blank'); return false; } // The name field must be a text entry between 2 and 100 characters $myValidator = new sfStringValidator(); $myValidator->initialize($this->getContext(), array( 'min' => 2, 'min_error' => 'This name is too short (2 characters minimum)', 'max' => 100, 'max_error' => 'This name is too long. (100 characters maximum)', )); if (!$myValidator->execute($name, $error)) { return false; } return true; }
If a user submits the form in Listing 10-17 with the value a
in the name
field, the execute()
method of the sfStringValidator
will return false
(because the string length is less than the minimum of two characters). The validateSend()
method will then fail, and the handleErrorSend()
method will be called instead of the executeSend()
method.
tip
The setError()
method of the sfRequest
class gives information to the template so that it can display an error message (as explained in the "Displaying the Error Messages in the Form" section later in this chapter). The validators set the errors internally, so you can define different errors for the different cases of nonvalidation. That's the purpose of the min_error
and max_error
initialization parameters of the sfStringValidator
.
All the rules defined in the example can be translated into validators:
name
:sfStringValidator
(min=2
,max=100
)email
:sfStringValidator
(min=2
,max=100
) andsfEmailValidator
age
:sfNumberValidator
(min=0
,max=120
)
The fact that a field is required is not handled by a validator.
Validation File
You could easily implement the validation of the contact form with validators in the validateSend()
method, but that would imply repeating a lot of code. Symfony offers an alternative way to define validation rules for a form, and it involves YAML. For instance, Listing 10-19 shows the translation of the name field validation rules, and its results are equivalent to those of Listing 10-18.
Listing 10-19 - Validation File, in modules/contact/validate/send.yml
fields: name: required: msg: The name field cannot be left blank sfStringValidator: min: 2 min_error: This name is too short (2 characters minimum) max: 100 max_error: This name is too long. (100 characters maximum)
In a validation file, the fields
header lists the fields that need to be validated, if they are required, and the validators that should be tested on them when a value is present. The parameters of each validator are the same as those you would use to initialize the validator manually. A field can be validated by as many validators as necessary.
note
The validation process doesn't stop when a validator fails. Symfony tests all the validators and declares the validation failed if at least one of them fails. And even if some of the rules of the validation file fail, symfony will still look for a validateXXX()
method and execute it. So the two validation techniques are complementary. The advantage is that, in a form with multiple failures, all the error messages are shown.
Validation files are located in the module validate/
directory, and named by the action they must validate. For example, Listing 10-19 must be stored in a file called validate/send.yml
.
Redisplaying the Form
By default, symfony looks for a handleErrorSend()
method in the action class whenever the validation process fails, or displays the sendError.php
template if the method doesn't exist.
The usual way to inform the user of a failed validation is to display the form again with an error message. To that purpose, you need to override the handleErrorSend()
method and end it with a redirection to the action that displays the form (in the example, module/index
), as shown in Listing 10-20.
Listing 10-20 - Displaying the Form Again, in modules/contact/actions/actions.class.php
class ContactActions extends sfActions { public function executeIndex() { // Display the form } public function handleErrorSend() { $this->forward('contact', 'index'); } public function executeSend() { // Handle the form submission } }
If you choose to use the same action to display the form and handle the form submission, then the handleErrorSend()
method can simply return sfView::SUCCESS
to redisplay the form from sendSuccess.php
, as shown in Listing 10-21.
Listing 10-21 - A Single Action to Display and Handle the Form, in modules/contact/actions/actions.class.php
class ContactActions extends sfActions { public function executeSend() { if ($this->getRequest()->getMethod() != sfRequest::POST) { // Prepare data for the template // Display the form return sfView::SUCCESS; } else { // Handle the form submission ... $this->redirect('mymodule/anotheraction'); } } public function handleErrorSend() { // Prepare data for the template // Display the form return sfView::SUCCESS; } }
The logic necessary to prepare the data can be refactored into a protected method of the action class, to avoid repeating it in the executeSend()
and handleErrorSend()
methods.
With this new configuration, when the user types an invalid name, the form is displayed again, but the entered data is lost and no error message explains the reason of the failure. To address the last issue, you must modify the template that displays the form, to insert error messages close to the faulty field.
Displaying the Error Messages in the Form
The error messages defined as validator parameters are added to the request when a field fails validation (just as you can add an error manually with the setError()
method, as in Listing 10-18). The sfRequest
object provides two useful methods to retrieve the error message: hasError()
and getError()
, which each expect a field name as parameter. In addition, you can display an alert at the top of the form to draw attention to the fact that one or many of the fields contain invalid data with the hasErrors()
method. Listings 10-22 and 10-23 demonstrate how to use these methods.
Listing 10-22 - Displaying Error Messages at the Top of the Form, in templates/indexSuccess.php
<?php if ($sf_request->hasErrors()): ?> <p>The data you entered seems to be incorrect. Please correct the following errors and resubmit:</p> <ul> <?php foreach($sf_request->getErrors() as $name => $error): ?> <li><?php echo $name ?>: <?php echo $error ?></li> <?php endforeach; ?> </ul> <?php endif; ?>
Listing 10-23 - Displaying Error Messages Inside the Form, in templates/indexSuccess.php
<?php echo form_tag('contact/send') ?> <?php if ($sf_request->hasError('name')): ?> <?php echo $sf_request->getError('name') ?> <br /> <?php endif; ?> Name: <?php echo input_tag('name') ?><br /> ... <?php echo submit_tag() ?> </form>
The conditional use of the getError()
method in Listing 10-23 is a bit long to write. That's why symfony offers a form_error()
helper to replace it, provided that you declare the use of its helper group, Validation
. Listing 10-24 replaces Listing 10-23 by using this helper.
Listing 10-24 - Displaying Error Messages Inside the Form, the Short Way
<?php use_helper('Validation') ?> <?php echo form_tag('contact/send') ?> <?php echo form_error('name') ?><br /> Name: <?php echo input_tag('name') ?><br /> ... <?php echo submit_tag() ?> </form>
The form_error()
helper adds a special character before and after each error message to make the messages more visible. By default, the character is an arrow pointing down (corresponding to the ↓
entity), but you can change it in the settings.yml
file:
all: .settings: validation_error_prefix: ' ↓ ' validation_error_suffix: ' ↓'
In case of failed validation, the form now displays errors correctly, but the data entered by the user is lost. You need to repopulate the form to make it really user-friendly.
Repopulating the Form
As the error handling is done through the forward()
method (shown in Listing 10-20), the original request is still accessible, and the data entered by the user is in the request parameters. So you could repopulate the form by adding default values to each field, as shown in Listing 10-25.
Listing 10-25 - Setting Default Values to Repopulate the Form When Validation Fails, in templates/indexSuccess.php
<?php use_helper('Validation') ?> <?php echo form_tag('contact/send') ?> <?php echo form_error('name') ?><br /> Name: <?php echo input_tag('name', $sf_params->get('name')) ?><br /> <?php echo form_error('email') ?><br /> Email: <?php echo input_tag('email', $sf_params->get('email')) ?><br /> <?php echo form_error('age') ?><br /> Age: <?php echo input_tag('age', $sf_params->get('age')) ?><br /> <?php echo form_error('message') ?><br /> Message: <?php echo textarea_tag('message', $sf_params->get('message')) ?><br /> <?php echo submit_tag() ?> </form>
But once again, this is quite tedious to write. Symfony provides an alternative way of triggering repopulation for all the fields of a form, directly in the YAML validation file, without changing the default values of the elements. Just enable the fillin:
feature for the form, with the syntax described in Listing 10-26.
Listing 10-26 - Activating fillin
to Repopulate the Form When Validation Fails, in validate/send.yml
fillin: enabled: true # Enable the form repopulation param: name: test # Form name, not needed if there is only one form in the page skip_fields: [email] # Do not repopulate these fields exclude_types: [hidden, password] # Do not repopulate these field types check_types: [text, checkbox, radio, select, hidden] # Do repopulate these content_type: html # html is the tolerant default. Other option is xml and xhtml (same as xml but without xml prolog)
By default, the automatic repopulation works for text inputs, check boxes, radio buttons, text areas, and select components (simple and multiple), but it does not repopulate password or hidden tags. The fillin
feature doesn't work for file tags.
note
The fillin
feature works by parsing the response content in XML just before sending it to the user. By default fillin
will output HTML.
If you want to have fillin
to output XHTML you have to set param: content_type: xml
. If the response is not a strictly valid XHTML document fillin
will not work.
The third available content_type
is xhtml
which is the same as xml
but it removes the xml prolog from the response which causes quirks mode in IE6.
You might want to transform the values entered by the user before writing them back in a form input. Escaping, URL rewriting, transformation of special characters into entities, and all the other transformations that can be called through a function can be applied to the fields of your form if you define the transformation under the converters:
key, as shown in Listing 10-27.
Listing 10-27 - Converting Input Before fillin
, in validate/send.yml
fillin: enabled: true param: name: test converters: # Converters to apply htmlentities: [first_name, comments] htmlspecialchars: [comments]
Standard Symfony Validators
Symfony contains some standard validators that can be used for your forms:
sfStringValidator
sfNumberValidator
sfEmailValidator
sfUrlValidator
sfRegexValidator
sfCompareValidator
sfPropelUniqueValidator
sfDoctrineUniqueValidator
sfFileValidator
sfCallbackValidator
sfDateValidator
Each has a default set of parameters and error messages, but you can easily
override them through the initialize()
validator method or in the YAML file.
The following sections describe the validators and show usage examples.
String Validator
sfStringValidator
allows you to apply string-related constraints to a parameter.
sfStringValidator: values: [foo, bar] values_error: The only accepted values are foo and bar insensitive: false # If true, comparison with values is case insensitive min: 2 min_error: Please enter at least 2 characters max: 100 max_error: Please enter less than 100 characters
Number Validator
sfNumberValidator
verifies if a parameter is a number and allows you to apply size constraints.
sfNumberValidator: nan_error: Please enter an integer min: 0 min_error: The value must be at least zero max: 100 max_error: The value must be less than or equal to 100
E-Mail Validator
sfEmailValidator
verifies if a parameter contains a value that qualifies as an e-mail address.
sfEmailValidator: strict: true email_error: This email address is invalid
RFC822 defines the format of e-mail addresses. However, it is more permissive than the generally accepted format. For instance, me@localhost
is a valid e-mail address according to the RFC, but you probably don't want to accept it. When the strict
parameter is set to true
(its default value), only e-mail addresses matching the pattern name@domain.extension
are valid. When set to false
, RFC822 is used as a rule.
URL Validator
sfUrlValidator
checks if a field is a correct URL.
sfUrlValidator: url_error: This URL is invalid
Regular Expression Validator
sfRegexValidator
allows you to match a value against a Perl-compatible regular expression pattern.
sfRegexValidator: match: No match_error: Posts containing more than one URL are considered as spam pattern: /http.*http/si
The match
parameter determines if the request parameter must match the pattern to be valid (value Yes
) or match the pattern to be invalid (value No
).
Compare Validator
sfCompareValidator
checks the equality of two different request parameters. It is very useful for password checks.
fields: password1: required: msg: Please enter a password password2: required: msg: Please retype the password sfCompareValidator: check: password1 compare_error: The two passwords do not match
The check
parameter contains the name of the field that the current field
must match to be valid.
Propel/Doctrine Unique Validators
sfPropelUniqueValidator
validates that the value of a request parameter
doesn't already exist in your database. It is very useful for unique indexes.
fields: nickname: sfPropelUniqueValidator: class: User column: login unique_error: This login already exists. Please choose another one.
In this example, the validator will look in the database for a record of class
User
where the login
column has the same value as the field to validate.
caution
sfPropelUniqueValidator is susceptible to race conditions. Although unlikely, in multiuser environments, the result may change the instant it returns. You should still be ready to handle a duplicate INSERT error.
NOTE
If you have enabled the Doctrine ORM for your project, you can also use the
sfDoctrineUniqueValidator
in the same way as described above for Propel.
File Validator
sfFileValidator
applies format (an array of mime-types) and size constraints to file upload fields.
fields: image: file: True required: msg: Please upload an image file sfFileValidator: mime_types: - 'image/jpeg' - 'image/png' - 'image/x-png' - 'image/pjpeg' mime_types_error: Only PNG and JPEG images are allowed max_size: 512000 max_size_error: Max size is 512Kb
Be aware that the file
attribute must be set to True
for the field, and the template must declare the form as multipart.
Callback Validator
sfCallbackValidator
delegates the validation to a third-party callable method or function to do the validation. The callable method or function must return true
or false
.
fields: account_number: sfCallbackValidator: callback: is_numeric invalid_error: Please enter a number. credit_card_number: sfCallbackValidator: callback: [myTools, validateCreditCard] invalid_error: Please enter a valid credit card number.
The callback method or function receives the value to be validated as a first parameter. This is very useful when you want to reuse existing methods of functions, rather than create a full validator class.
Date Validator
sfDateValidator
checks that a submitted date is valid and/or within a certain
simple range
sfDateValidator: date_error: You have entered an invalid date compare: "2007-05-01" operator: ">=" #defaults to "==" if not supplied compare_error: "Enter a date later than 1st May 2007"
tip
You can also write your own validators, as described in the "Creating a Custom Validator" section later in this chapter.
Named Validators
If you see that you need to repeat a validator class and its settings, you can package it under a named validator. In the example of the contact form, the email field needs the same sfStringValidator parameters as the name
field. So you can create a myStringValidator
named validator to avoid repeating the same settings twice. To do so, add a myStringValidator
label under the validators:
header, and set the class
and param
keys with the details of the named validator you want to package. You can then use the named validator just like a regular one in the fields
section, as shown in Listing 10-28.
Listing 10-28 - Reusing Named Validators in a Validation File, in validate/send.yml
validators: myStringValidator: class: sfStringValidator param: min: 2 min_error: This field is too short (2 characters minimum) max: 100 max_error: This field is too long (100 characters maximum) fields: name: required: msg: The name field cannot be left blank myStringValidator: email: required: msg: The email field cannot be left blank myStringValidator: sfEmailValidator: email_error: This email address is invalid
Restricting the Validation to a Method
By default, the validators set in a validation file are run when the action is called with the POST method. You can override this setting globally or field by field by specifying another value in the methods
key, to allow a different validation for different methods, as shown in Listing 10-29.
Listing 10-29 - Defining When to Test a Field, in validate/send.yml
methods: [post] # This is the default setting fields: name: required: msg: The name field cannot be left blank myStringValidator: email: methods: [post, get] # Overrides the global methods settings required: msg: The email field cannot be left blank myStringValidator: sfEmailValidator: email_error: This email address is invalid
What Does a Validation File Look Like?
So far, you have seen only bits and pieces of a validation file. When you put everything together, the validation rules find a clear translation in YAML. Listing 10-30 shows the complete validation file for the sample contact form, corresponding to all the rules defined earlier in the chapter.
Listing 10-30 - Sample Complete Validation File
fillin: enabled: true validators: myStringValidator: class: sfStringValidator param: min: 2 min_error: This field is too short (2 characters minimum) max: 100 max_error: This field is too long (100 characters maximum) fields: name: required: msg: The name field cannot be left blank myStringValidator: email: required: msg: The email field cannot be left blank myStringValidator: sfEmailValidator: email_error: This email address is invalid age: sfNumberValidator: nan_error: Please enter an integer min: 0 min_error: "You're not even born. How do you want to send a message?" max: 120 max_error: "Hey, grandma, aren't you too old to surf on the Internet?" message: required: msg: The message field cannot be left blank
Complex Validation
The validation file satisfies most needs, but when the validation is very complex, it might not be sufficient. In this case, you can still return to the validateXXX()
method in the action, or find the solution to your problem in the following sections.
Creating a Custom Validator
Each validator is a class that extends the sfValidator
class. If the validator classes shipped with symfony are not suitable for your needs, you can easily create a new one, in any of the lib/
directories where it can be autoloaded. The syntax is quite simple: The execute()
method of the validator is called when the validator is executed. You can also define default settings in the initialize()
method.
The execute()
method receives the value to validate as the first parameter and the error message to throw as the second parameter. Both are passed as references, so you can modify the error message from within the method.
The initialize()
method receives the context singleton and the array of parameters from the YAML file. It must first call the initialize()
method of its parent sfValidator
class, and then set the default values.
Every validator has a parameter holder accessible by $this->getParameterHolder()
.
For instance, if you want to build an sfSpamValidator
to check if a string is not spam, add the code shown in Listing 10-31 to an sfSpamValidator.class.php
file. It checks if the $value
contains more than max_url
times the string 'http'
.
Listing 10-31 - Creating a Custom Validator, in lib/sfSpamValidator.class.php
class sfSpamValidator extends sfValidator { public function execute (&$value, &$error) { // For max_url=2, the regexp is /http.*http/is $re = '/'.implode('.*', array_fill(0, $this->getParameter('max_url') + 1, 'http')).'/is'; if (preg_match($re, $value)) { $error = $this->getParameter('spam_error'); return false; } return true; } public function initialize ($context, $parameters = null) { // Initialize parent parent::initialize($context); // Set default parameters value $this->setParameter('max_url', 2); $this->setParameter('spam_error', 'This is spam'); // Set parameters $this->getParameterHolder()->add($parameters); return true; } }
As soon as the validator is added to an autoloadable directory (and the cache cleared), you can use it in your validation files, as shown in Listing 10-32.
Listing 10-32 - Using a Custom Validator, in validate/send.yml
fields: message: required: msg: The message field cannot be left blank sfSpamValidator: max_url: 3 spam_error: Leave this site immediately, you filthy spammer!
Using Array Syntax for Form Fields
PHP allows you to use an array syntax for the form fields. When writing your own forms, or when using the ones generated by the Propel administration (see Chapter 14), you may end up with HTML code that looks like Listing 10-33.
Listing 10-33 - Form with Array Syntax
<label for="story_title">Title:</label> <input type="text" name="story[title]" id="story_title" value="default value" size="45" />
Using the input name as is (with brackets) in a validation file will throw a parsed-induced error. The solution here is to replace square brackets []
with curly brackets {}
in the fields
section, as shown in Listing 10-34, and symfony will take care of the conversion of the names sent to the validators afterwards.
Listing 10-34 - Validation File for a Form with Array Syntax
fields: story{title}: required: Yes
Executing a Validator on an Empty Field
You may need to execute a validator on a field that is not required, on an empty value. For instance, this happens with a form where the user can change their password (but may not want to), and in this case, a confirmation password must be entered. See the example in Listing 10-35.
Listing 10-35 - Sample Validation File for a Form with Two Password Fields
fields: password1: password2: sfCompareValidator: check: password1 compare_error: The two passwords do not match
The validation process executes as follows:
If
password1
== null
andpassword2 == null
:- The
required
test passes. - Validators are not run.
- The form is valid.
- The
If
password2 == null
whilepassword1
is notnull
:- The
required
test passes. - Validators are not run.
- The form is valid.
- The
You may want to execute your password2
validator if password1
is not null
. Fortunately, the symfony validators handle this case, thanks to the group
parameter. When a field is in a group, its validator will execute if it is not empty and if one of the fields of the same group is not empty.
So, if you change the configuration to that shown in Listing 10-36, the validation process behaves correctly.
Listing 10-36 - Sample Validation File for a Form with Two Password Fields and a Group
fields: password1: group: password_group password2: group: password_group sfCompareValidator: check: password1 compare_error: The two passwords do not match
The validation process now executes as follows:
If
password1 == null
andpassword2 == null
:- The
required
test passes. - Validators are not run.
- The form is valid.
- The
If
password1 == null
andpassword2 == 'foo'
:- The
required
test passes. password2
isnot null
, so its validator is executed, and it fails.- An error message is thrown for
password2
.
- The
If
password1 == 'foo'
andpassword2 == null
:- The
required
test passes. password1
isnot null
, so the validator forpassword2
, which is in the same group, is executed, and it fails.- An error message is thrown for
password2
.
- The
If
password1 == 'foo'
andpassword2 == 'foo'
:- The
required
test passes. password2
isnot null
, so its validator is executed, and it passes.- The form is valid.
- The
Summary
Writing forms in symfony templates is facilitated by the standard form helpers and their smart options. When you design a form to edit the properties of an object, the object form helpers simplify the task a great deal. The validation files, validation helpers, and repopulation feature reduce the work necessary to build a robust and user-friendly server control on the value of a field. And even the most complex validation needs can be handled, either by writing a custom validator or by creating a validateXXX()
method in the action class.
This work is licensed under the GFDL license.