The definitive guide of Symfony 1.0

10.4. 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.

10.4.1. 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!

10.4.2. 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

10.4.3. 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 and password2 == null:
    • The required test passes.
    • Validators are not run.
    • The form is valid.
  • If password2 == null while password1 is not null:
    • The required test passes.
    • Validators are not run.
    • The form is valid.

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 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.