Symfony 1.1 Forms and Validation

Okay, so you want the quick rundown of how forms and validation work in symfony 1.1?
  • First, download the demo project - http://notjosh.com/labs/sf11forms.zip
  • Extract the project to somewhere on your http://localhost/ (the project should run as a sandbox project)
  • (Probably only necessary for *nix/Mac) On the command line, navigate to the directory of the project, and run ./symfony project:permissions
  • Point your favourite browser to http://localhost/<your project path>/web/frontend_dev.php/


You'll be presented with a pretty basic form, with some ridiculous validation rules. The scenario we're going for is that this is a user registration form. The validation rules are mostly just so you can see how flexible they are and how to use them.

That's the quickstart. For an explanation of how it all works, read on!

Okay, so let's have a look at the file structure of the project. It's very much what you're used to in symfony 1.0. Knowing that, let's head into some code.

Open your favourite file-browser/IDE to /apps/frontend/modules/user.

  • In our actions file, we have one action called 'register'.
  • We have two templates - a form, and a page that will just echo the form values once a successful submission is received
  • In our module's lib/ directory, we have three folders, each with a single class in them. (Note: this is just my convention, you don't need to have the classes in the folders)


Let's start with the things we're used to. Let's have a look in the action:


1 public function executeRegister()
2 {
3 $form = new RegisterForm();
4
5 if ($this->getRequest()->isMethod('post'))
6 {
7 $form->bind(
8 array(
9 'username' => $this->getRequest()
->getParameter('username'),
10 'password' => $this->getRequest()->getParameter('password'),
11 'email' => $this->getRequest()->getParameter('email'),
12 )
13 );
14
15
16 if ($form->isValid())
17 {
18 $values = $form->getValues();
19
20 $this->setTemplate('registered');
21
22 $this->values = $values;
23 return sfView::SUCCESS;
24 }
25 }
26
27 $this->form = $form;
28 }



So, a summary:
  • We're creating a form.
  • We're checking if it's a POST.
    • If so:
      • We'll bind the parameters from the form back to the form object
      • We'll check the form passes validation
      • If it's valid, pass the values to a new 'registered' template
    • If not, we'll assign the form variable to the template


And how does the default template look?


1 <form action="<?php echo url_for('user/register') ?>" method="post">
2 <table>
3 <?php echo $form ?>
4 <tr><td colspan="2"><input type="submit" /></td></tr>
5 </table>
6 </form>


So, we define a form, echo our form variable and give it a submit button. Easy! (You can modify the presentation of the form and add nested forms in code, but I'll cover that another day..)

The downside is that it's using tables for presentation. It's my understanding that you can plug in different form templates, but I haven't got that far yet.

And the other template? Well that looks like:


1 <p>Form successful! Here are the values:</p>
2
3 <?php foreach ($values as $id => $value): ?>
4 <p>
5 <strong><?php echo $id ?></strong>:
6 <?php echo $value ?>
7 </p>
8 <?php endforeach ?>


Simple, just a foreach over the values. We're outputting the POSTs, basically.

So far, so good.

Now we'll get into the nitty-gritty a little more. We'll have a look at how the form object (RegisterForm) is created.

So, let's take a look at RegisterForm:


1 class RegisterForm extends sfForm
2 {

3 public function configure()
4 {
5 $this->setValidatorSchema(new RegisterValidatorSchema());
6 $this->setWidgetSchema(new RegisterWidgetFormSchema());
7 }
8
9 public function bind($taintedValues)
10 {
11 $request = sfContext::getInstance()->getRequest();
12 if ($request->hasParameter(self::$CSRFFieldName))
13 {
14 $taintedValues[self::$CSRFFieldName] = $request->getParameter(self::$CSRFFieldName);
15 }
16
17 parent::bind($taintedValues);
18 }
19 }


Take a look at the configure() method, it's the important one. We're assigning the form with a widget schema (a set of form widgets), and a validation schema (the new version of validation YAML). Really simple stuff, definitely.

Note: I don't know if there's a better way than overriding the bind method. Overriding the bind method is just to transparently manage the CSRF protection. You can ignore that for now - the easy way is to add the extra array option in the action where you call ->bind on the other POST vars.

Let's look at the widget schema we're using (RegisterWidgetFormSchema):


1 class RegisterWidgetFormSchema extends sfWidgetFormSchema
2 {

3 public function __construct($options = array(), $attributes = array(), $labels = array(), $helps = array())
4 {
5 parent::__construct(
6 array(
7 'username' => new sfWidgetFormInput()
,
8 'password' => new sfWidgetFormInputPassword(),
9 'email' => new sfWidgetFormInput(),
10 ),
11 $options,
12 $attributes,
13 $labels,
14 $helps
15 );
16 }
17 }


I'm not certain this is the best way to do this, but it seems to work just fine. I'm overriding the constructor, and passing in the values I know the 'register' form should have (in this case, a 'username', a 'password' and an 'email'). The sfWidgetFormInput is a simple input element (type=text), and predictably the sfWidgetFormInputPassword is just a password input element.

So there we have it - now we can render a form, and have no validation on it.

We can do more complex things in here, like nesting different forms and having groups of elements (I think, anyway). But I'll leave that for now.

So, let's look at our validation rules in RegisterWidgetFormSchema:


1 class RegisterValidatorSchema extends sfValidatorSchema
2 {

3 public function __construct($options = array(), $messages = array())
4 {
5 parent::__construct(
6 array(
7 'username' => new sfValidatorString()
,
8 'password' => new sfValidatorAny(
9 array(
10 new sfValidatorString(array('min_length' => 10))
,
11 new sfValidatorRegex(array('pattern' => '/^\d{2}/')),
12 )
13 ),
14 'email' => new sfValidatorAll(
15 array(
16 new sfValidatorString(array('min_length' => 10))
,
17 new sfValidatorEmail(),
18 )
19 )
20 ),
21 $options,
22 $messages
23 );
24 }
25 }

Similar to above, I'm just overriding the constructor as passing in validators (making sure the array keys match the inputs defined in the widget schema).

Let's take a closer look at our (wildly stupid) validation rules:

  • username: simple string validator. This should be fairly familiar to symfony 1.0 kind of functionality.
  • password: this gets a bit more complex, our validator is 'sfValidatorAny'. This validator takes an array of parameters (which are other validators). It's like a boolean 'OR' that says "we must match any of the following validators". So what are the possible validators we can match?
    • sfValidatorString, min_length = 10
    • sfValidatorRegex, patterm = /^\d{2}/ (for the regex challenged, we must have two numbers at the start of the string)
    So, in human terms, we must either have a password at least 10 chars long, or we must start our password with two numbers. So, 'abcdefgh' is an invalid password, but 'abcdefghij' is valid, and so is '12abc'.
  • email: similar to password, our validator is 'sfValidatorAll'. In the way that sfValidatorAny was 'OR', this is like 'AND'. It says that all of the validators must pass. So what are the validators?
    • sfValidatorString, min_length = 10
    • sfValidatorEmail
    Both of these should be pretty obvious to you - it must be an email that's at least 10 characters long. 'a@b.com' will fail validation, but 'abcde@fghij.com' will pass!


That pretty much wraps it up. It's pretty basic, but it'll give you enough to start building some basic forms and validating them. If I get a chance, I'll run through some more complex stuff another day? Hrm, maybe.

Best of luck! Happy sf1.1'ing!

Monday, November 26. 2007

1 Comment