Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

symfony validate database content with sfValidatorCallback

Scene from "The three Musketeers" by Stephen Herek (1993)

In my last project, I needed to create a custom validation feature that would check if there are some specific records in database tables. You can read a lot about symfony validators at the official documentation, but I didn't found an exact solution to my problem there.


There are some customers with their names and their security codes and there are some offers that are bound to customers. There is a offer submission form in the frontend application, where the customer's name and security code have to be given. I found none of the simple validators useful. In the sfDoctrineGuardPlugin there is a sfGuardValidatorUser which is used to validate passwords in the signin form:


$this->setWidgets(array(
  'username' => new sfWidgetFormInputText(),
  'password' => new sfWidgetFormInputPassword(array('type' => 'password')),
  'remember' => new sfWidgetFormInputCheckbox(),
));
 
$this->setValidators(array(
  'username' => new sfValidatorString(),
  'password' => new sfValidatorString(),
  'remember' => new sfValidatorBoolean(),
));
 
$this->validatorSchema->setPostValidator(new sfGuardValidatorUser());

Take a look at the validator's class here. In short, the validator retrieves the user object and compares its password stored in the database with the one accessible from the request. As I mentioned, no simple validator can help, so a post validator has to be used on the entire validator schema:


$this->validatorSchema->setPostValidator(new sfGuardValidatorUser());

However, you can have the same effect without creating a custom validator. Symfony framework provides a callback validator:


$this->validatorSchema->setPostValidator(
  new sfValidatorCallback(array
    ('callback' => array($this, 'customer_code_callback'))));

The customer_code_callback has to be created now:


public function customer_code_callback($validator, $values)
{
  $customer_count = Doctrine_Query::create()
    ->from('Customer c')
    ->where('c.name = ?', $values['customer'])
    ->andWhere('c.code = ?', $values['code'])
    ->count();
 
  if (!$customer_count)
  {
    throw new sfValidatorError($validator, 'Niepoprawny kod lub firma');
  }
 
  return $values;
}

Following the principle the simplest is the best, I'm doing almost the same stuff as the sfGuardValidatorUser, but with less work. Of course, it is up to you to decide whether you prefer to create a simple callback function or a reusable, entire validator class (which is more complicated, more elegant but provides the same functionality). So this is enough for the form to validate the customer's security code, before the offer file can be uploaded.


Of course, apart from the post validator, all form widgets can be validated with simple validators:


$this->setWidgets(array(
  'customer' => new sfWidgetFormInputText(),
  'code' => new sfWidgetFormInputText(),
  'file' => new sfWidgetFormInputFile(),
));
 
$this->setValidators(array(
  'customer' => new sfValidatorString(array(
    'required' => true,
    'trim' => true
  ), array(
    'required' => 'Podaj nazwę firmy',
  )),
  'code' => new sfValidatorString(array(
    'required' => true,
    'trim' => true
  ), array(
    'required' => 'Podaj kod weryfikacyjny',
  )),
  'file' => new sfValidatorFile(array(
    'required' => true,
    'path'     => sfConfig::get('sf_upload_dir').'/offer/',
  ), array(
    'required' => 'Wybierz załącznik',
  ))
));

further usage


The example I gave seems quite similar to password check. But as you can see, you can just replace the example Doctrine query with any other queries, e.g.

  • check number of offers submitted by a customer, which are still being processed (not finished), if their total count exceeds 10, the validation shall fail
  • check if there is any free machine/employee that can handle a service, requested by the user; for example, all machines are busy, the validation shows an error you have to wait 36 minutes to wait for the first free machine and, optionally, store the IP to be handled as the first in the queue