How to validate a form
Overview
Form validation can occur on the server side and/or on the client side. The server side validation is compulsory - to avoid wrong data to corrupt a script or a database - and the client side validation is optional, though it greatly enhances the user experience. Symfony automates the server side validation to speed up the development of common web applications.
Base example
Let's illustrate the validation features of Symfony starting with a normal Contact form, without any kind validation, showing the following fields:
In a newly created contact module, the first action to write is the one that displays the form by calling the default template:
class contactActions extends sfActions
{
public function executeIndex()
{
return sfView::SUCCESS;
}
}
And the corresponding indexSuccess.php template contains:
<?php echo form_tag('contact/send') ?>
<label for="name">Name</label> : <?php echo input_tag('name') ?><br />
<label for="email">Email</label> : <?php echo input_tag('email') ?><br />
<label for="age">Age</label> : <?php echo input_tag('age') ?><br />
<label for="message">Message</label> : <?php echo textarea_tag('message') ?><br />
<?php echo submit_tag() ?>
</form>
If you wonder what the _tag() functions do, you should probably take a look at the form helpers chapter. The form can now be displayed in the browser by typing the URL index.php/contact.
To handle the form submission, the send action must be created. For this example, we just need the application to display an "OK" message after submission:
class contactActions extends sfActions
{
...
public function executeSend()
{
$this->email = $this->getRequestParameter('email');
}
}
The sendSuccess.php template just contains:
Your message was sent to our services. The answer will be sent at <?php echo $email ?>
You can test the whole process of submitting the form and getting the confirmation, it already works fine. Except that if you try to enter invalid data in the fields, the action may very well crash. The fields do require validation.
Rules
Let's write the validation rules in plain text:
- name: required text field, size must be between 2 and 100 characters
- email: required text field, must contain a valid email address
- age: required number field, must contain a integer between 0 and 120
- message : required field
Symfony can apply these rules almost automatically, provided that you add a new configuration file to the module and change a few details in the template.
Configuration file
By convention, if you want to validate the form data on the call to the send action, a configuration file called send.yml must be created in the validate directory of the module. To validate only the name field, you need the following configuration:
fields:
name:
required:
msg: The name field cannot be left blank
sfStringValidator:
min: 2
min_error: You didn't enter a valid name (at least 2 characters). Please try again.
max: 100
max_error: You didn't enter a valid name (less than 100 characters). Please try again.
Let's have a closer look at this file:
Under the fields header appears the list of the the fields to be checked. Each can have a required section with an error message to be displayed if the form is submitted without the field. Each field can also have a list of validators, together with parameters.
Action modification
The default behavior makes symfony call a predefined handleError() method whenever an error is detected in the validation process. This method will simply display the sendError() template.
But if you prefer to display the form again with an error message in it, you need to override the default handleError() method for the form handling action and end it with a redirection to the index action of the contact module. Do this by adding the following code to your contact actions:
class ContactActions extends sfActions
{
...
public function handleErrorSend()
{
$this->forward('contact', 'index');
}
}
If you try to fill in the form with this new configuration, and type a wrong name, the form is displayed again, but the data you entered is lost and no error message explains the reason of the failure.
Template modification
To address these two issues, you just need to modify the indexSuccess.php template.
Since the forward method kept the original request, the template has access to the data entered by the user:
<?php echo form_tag('contact/send') ?>
<label for="name">Name</label> : <?php echo input_tag('name', $sf_params->get('name')) ?><br />
<label for="email">Email</label> : <?php echo input_tag('email', $sf_params->get('email')) ?><br />
<label for="age">Age</label> : <?php echo input_tag('age', $sf_params->get('age')) ?><br />
<label for="message">Message</label> : <?php echo textarea_tag('message', $sf_params->get('message')) ?><br />
<?php echo submit_tag() ?>
</form>
Note: You can avoid setting manually the value of the fields from the request by using a special filter. See the Repopulation section below.
You can detect whether the form has errors by calling the ->hasErrors() method of the sfRequest object. To get the list of the error messages, you need the method ->getErrors(). So you should add the following lines at the top of the template:
<?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 $error): ?>
<li><?php echo $error ?></li>
<?php endforeach ?>
</ul>
<?php endif ?>
Now you may suggest that the field with incorrect data should be highlighted, for instance with a repetition of the error message clearly attached to the label with a ↓. To that extent, simply add the following line before every field:
<?php if ($sf_request->hasError('name')): ?>↓ <?php echo $sf_request->getError('name') ?> ↓<?php endif ?><br />
Complete configuration file
You can add the other rules to the send.yml configuration file to force the validation of all fields. This is what a complete YAML validation file looks like:
fields:
name:
required:
msg: The name field cannot be left blank
sfStringValidator:
min: 2
min_error: You didn't enter a valid name (at least 2 characters). Please try again.
max: 100
max_error: You didn't enter a valid name (less than 100 characters). Please try again.
email:
required:
msg: The email field cannot be left blank
sfRegexValidator:
match: Yes
match_error: "You didn't enter a valid email address (for example: name@domain.com). Please try again."
pattern: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/i
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
Named validator
If you reuse the same validator with the same parameter more than once in a validation file, you may wish to avoid repetition. You can define a named validator under the validators header, and reuse this validator in the fields section as follows:
validators:
myEmailValidator:
class: sfRegexValidator:
param:
match: Yes
match_error: "You didn't enter a valid email address (for example: name@domain.com). Please try again."
pattern: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/i
fields:
email:
required:
msg: The email field cannot be left blank
myEmailValidator:
match_error: "The email is compulsory."
As you can see, it is possible to override any parameter defined in the validators header when you actually declare the use of this validator in a fields section.
Request mode
By default, the validation will be triggered only by a POST request. You can choose to have it triggered also on a GET request, by overriding the value of the methods key:
methods: [post, get]
It is even possible to declare distinct fields for disstinct request modes. For instance, if you want the name field to be validated in GET but not in POST, you can override the global methods declaration from within the fields section:
name:
methods: [get]
required:
msg: The name field cannot be left blank
sfStringValidator:
min: 2
min_error: You didn't enter a valid name (at least 2 characters). Please try again.
max: 100
max_error: You didn't enter a valid name (less than 100 characters). Please try again.
Note: In previous versions of symfony, the validation file had another syntax, slighly longer and more complex. This older syntax still works but is deprecated.
Available validators
The available validators can be found in the symfony lib validator directory. For the moment, they are:
sfStringValidator: allows you to apply string-related constraints to a parameter
nameValidator:
class: sfStringValidator
param:
min: 2
min_error: Please enter a name of at least 2 characters
max: 100
max_error: Please enter a name of at most 100 characters
sfNumberValidator: verifies if a parameter is a number and allows you to apply size constraints
ageValidator:
class: sfNumberValidator
param:
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 ?"
sfRegexValidator: allows you to match a value against a regular expression pattern
spamValidator:
class: sfRegexValidator
param:
match: No
match_error: Posts containing more than one http address are considered as spam
pattern: /http.*http/si
The match param determines if the request parameter must match the pattern to be valid (value Yes) or match the pattern to be invalid (value No)
sfEmailValidator: verifies if a parameter contains a value that qualifies as an email address
emailValidator:
class: sfEmailValidator
param:
email_error: This email address is invalid
sfCompareValidator: checks the equality of two different request parameters; very useful for password check
fields:
password1:
required:
msg: Please enter a password
password2:
required:
msg: Please retype the password
sfCompareValidator:
check: password1
compare_error: The passwords you entered do not match. Please try again.
The check param contains the name of the field that the current field must match to be valid.
sfPropelUniqueValidator: validates that the value of a request parameter doesn't already exist in your database. Very useful for primary keys.
loginValidator:
class: sfPropelUniqueValidator
param:
class: User
column: login
unique_error: This login already exists. Please choose another one.
In the example above, the validator will look in the database for a record of class User where the column login has the same value as the parameter to validate. Note that this validator relies on Propel.
sfFileValidator: applies format (an array of mime types) and size constraints to file upload fields
fields:
image:
required:
msg: Please upload an image file
file: true
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
Beware that the attribute file has to be set to true for the field in the fields section - and that the template must declare the form as multipart. Find more information in the file upload chapter.
sfCallbackValidator: Passes the hand to a third party method or function to do the validation (and return true or false).
ageValidator:
class: sfCallbackValidator
param:
callback: is_integer
invalid_error: Please enter a number.
creditCardValidator:
class: sfCallbackValidator
param:
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 don't want to create a full validator class but reuse existing methods of functions.
Repopulation
Note: This feature is only available with a symfony release higher than 1096 and is still considered Alpha, since the interface can change. Feel free to give your feedback about it.
One common concern about forms is the value that the form fields will have when the form is displayed again after a failed validation. If you defined default values, or if you use object helpers, it can be quite tricky to determine how to handle the values from the request. In addition, some controls (namely the checkbox and the select tags) have special ways to pass their value in the request parameters.
Fortunately, symfony takes care of the form repopulation for you. If you want your form to be filled in with the values previously entered by the user, simply add these lines to your validation file:
fillin:
enabled: on # enable the form repopulation
param:
name: test # name of the form
This forces you to give a name attribute to your form, but it opens the possibility to repopulate a form in a page that contains more than one.
The repopulation works for text and hidden inputs, textareas, radiobuttons, checkboxes and selects (simple and multiple).
You might want to transform the values entered by the user before putting them in a form input. Escaping, url rewriting, transformation of special characters into entities, etc., all the transformations that can be called through a function (existing or defined by you) can be applied to the fields of your form if you define the transformation under the converters: key:
fillin:
enabled: on
param:
name: test
converters: # converters to apply
htmlentities: [first_name, comments]
htmlspecialchars: [comments]
The repopulation feature is based on a filter called sfFillInFormFilter, and it means that you can take advantage of form repopulation even if you don't use the symfony validation files. To enable the filter, just add it to your filters.yml as you would do with a normal filter (see more in the filter chapter).
Complex validation needs
Custom validator
Each Validator is a particular class that can have certain parameters. If the validation classes shipped with Symfony are not enough for your needs, you can easily create new ones. Here is the example of a validation class for email addresses, the actually existing sfEmailValidator:
class sfEmailValidator extends sfValidator
{
public function execute (&$value, &$error)
{
if (!preg_match('~^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$~i', $value))
{
$error = $this->getParameter('email_error');
return false;
}
return true;
}
public function initialize ($context, $parameters = null)
{
// initialize parent
parent::initialize($context, $parameters);
// set defaults
if (!$this->hasParameter('email_error')
{
$this->setParameter('email_error', 'Invalid input');
}
return true;
}
}
Now the email validation in the send.yml file can be replaced with:
fields:
...
email:
required:
msg: The email field cannot be left blank
sfEmailValidator:
email_error: You didn't enter a valid email address (for example: name@domain.com). Please try again.
If you need to create a new validation class and if it is a generic one, you should ask to have it included in the framework.
Validate method
Sometimes the power of validators is not enough. This happens mostly when you need to perform a complex validation that relies on context dependent variables. In that case, you can add a new method to your Action class named validateXXX() where XXX is the name of the action called by the form. If this method returns true, the executeXXX() method will be evaluated as usual. Otherwise, it's the handleErrorXXX() (if it exists), or the general handleError() method that is evaluated.
class ContactActions extends sfActions
{
...
public function executeIndex()
{
// display the form
...
}
public function validateSend()
{
// validate request parameters
$spammer = SpammersPeer::retrieveByName($this->getRequestParameter('name'));
if($spammer && $spammer->isBanned())
{
$this->getRequest()->setError('time', 'You are not allowed to post here anymore');
return false;
}
return true;
}
public function executeSend()
{
// handle the form submission
...
}
public function handleErrorSend()
{
$this->forward('contact', 'index');
}
}
Forms with array syntax
PHP allows you to use an array syntax for the form fields. When writing forms by yourself, or when using the ones generated by the Propel admin, you end up with HTML code looking like:
<label for="story[title]">Title:</label>
<input type="text" name="story[title]" id="story[title]" value="default value" size="45" />
The trouble is that using the input id as is (with brackets) in a validation file will push the YAML parser to its limits, and you will end up with errors. The solution here is to replace square brackets [] by curly brackets {} in the fields: section:
fields:
story{title}:
required: Yes
Symfony will do the translation automatically, and the validation will run as expected.
Execute validator on an empty field
You sometimes need to execute a validator on a field which is not required, on an empty value. This happens, for instance, with a form where the user can (but may not) want to change his password, and in this case a confirmation password must be entered:
fields:
password1: # Not required
password2: # Not required either
sfCompareValidator:
check: password1
compare_error: The passwords you entered do not match. Please try again.
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:
fields:
password1:
group: password_group
password2:
group: password_group
validators: sfCompareValidator
check: password1
compare_error: The passwords you entered do not match. Please try again.
Now, the validation occurs as follows:
If password1 == null and password2 == null
- The
required test passes
- Validators are not run
- The form is valid
If password1 == null and password2 == 'foo'
- The
required test passes
password2 is not null, so its validator is executed - and it fails
- An error message is thrown for
password2
If password1 == 'foo' and password2 == null
- The
required test passes
password1 is not null, so the validator for password2, which is in the same group, is executed - and it fails
- An error message is thrown for
password2
If password1 == 'foo' and password2 == 'foo'
- The
required test passes
password2 is not null, so its validator is executed - and it passes
- The form is valid
Client side validation
The client side validation is done by some javascript code. Symfony will soon provide built-in client-side validation features based on the YAML validation configuration files. Make sure you get the latest version !
|