Navigation

Today I want to follow up on my last post about improving the forms in symfony. David Hermann wrote a quite interesting reply on his blog. I want to take some of his ideas and enhance them even further. The goal is to be able to reuse as much code as possible when creating applications with lots of different forms.

This post will be different though. While I only made assumptions in my last post without any code verifying that my ideas are implementable, I do have a prototype implementation now.

The concept presented in this article series is not implemented in symfony. It has been developed together with a few other people in the community. With these articles, I would like to receive feedback about how you like the concept and how it can be improved.

  1. Improving the Forms
  2. Improving the Forms: Field Groups
  3. Improving the Forms: Layouts and Formatters

Where Did We Stop?

In my last post, we left with the following form:

Basic Profile Form

The idea was to have a user profile form where we can enter the name of the user, his email address and his password and are able to validate a few special conditions. I added a few help texts to enhance the look and usability of the form. Here is the code for the form:

// lib/form/doctrine/ProfileForm.class.php
class ProfileForm extends sfFormDoctrine
{
  protected $user = null;
 
  public function __construct(sfUser $user)
  {
    $this->user = $user;
  }
 
  public function configure()
  {
    $this->addField('name')
         ->setWidgetAttributes(array('max_length' => 15))
         ->setValidatorOptions(array('max_length' => 15))
         ->setHelp('Your full name, f.i. "John Travolta"');
 
    $this->addField('email')
         ->setValidator(new sfValidatorEmail())
         ->setHelp('A valid email address, f.i. "john.travolta@gmail.com"');
 
    $this->addField('password', new PasswordField())
         ->setHelp('Must not be "pulpfiction"');
 
    $this->addField('password_again')
         ->setWidget(new sfWidgetPassword())
         ->setValidator(new sfValidatorPass())
         ->setLabel('Password (again)');
 
    $this->setFinalValidator(new sfValidatorAnd(
      new sfValidatorCompare('password_again', '==', 'password',
          'The passwords must be equal'),
      new sfValidatorCallback($this, 'comparePasswordAndName')
    ));
 
    if (!$this->user->hasCredential('admin'))
    {
      $this->getField('name')->setEditable(false);
    }
  }
 
  public function comparePasswordAndName(sfValidator $validator, array $values)
  {
    if ($values['name'] == $values['password'])
    {
      throw new sfValidatorError('The password must not be the same as the name',
          'password');
    }
  }
}

I introduced you to the concept of form fields that bundle a widget and a validator. If you have not read the post explaining this concept, I recommend you to read it now, because I will build up on it today.

Convention Over Configuration

Many forms in symfony applications share the same type of fields. An email address, for example, is in 95% of the cases a normal input widget with an email validator. Redefining this definition in different forms leads to code duplication, so we should extract it into a separate field type. If we do this for the most common field types, we come up with a nice little collection of fields that should be bundled with symfony by default:

  • sfTextField (sfWidgetInput, sfValidatorString)
  • sfTextareaField (sfWidgetTextarea, sfValidatorString)
  • sfPasswordField (sfWidgetInputPassword, sfValidatorString)
  • sfEmailField (sfWidgetInput, sfValidatorEmail)
  • sfCheckboxField (sfWidgetCheckbox, sfValidatorBoolean)
  • sfCountryField (sfWidgetI18nSelectCountry, sfValidatorI18nSelectCountry)
  • sfLanguageField (sfWidgetI18nSelectLanguage, sfValidatorI18nSelectLanguage)
  • sfSelectField (sfWidgetSelect, sfValidatorChoice)

I might have forgotten quite a few now, but you see where I am heading. With a decent collection of predefined fields you will only need to manually define widgets and validators in edge cases. This is especially great for beginners who do not have to learn about widgets and validators to create simple forms!

Let’s look at how we can reduce the above form definition even more by using these predefined fields:

  public function configure()
  {
    $this->addField('name', new sfTextField(15))
         ->setHelp('Your full name, f.i. "John Travolta"');
    $this->addField('email', new sfEmailField())
         ->setHelp('A valid email address, f.i. "john.travolta@gmail.com"');
    $this->addField('password', new sfPasswordField())
         ->setHelp('Must not be "pulpfiction"');
    $this->addField('password_again', new sfPasswordField())
         ->setValidator(new sfValidatorPass())
         ->setLabel('Password (again)');
 
    $this->setFinalValidator(new sfValidatorAnd(
      new sfValidatorCompare('password_again', '==', 'password',
          'The passwords must be equal'),
      new sfValidatorCallback($this, 'comparePasswordAndName')
    ));

Field Groups

So far, I have left the question open about how we can bundle the two password fields together, so you can just drop them into any form. The idea to solve this problem is to create field groups as proposed by David.

A field group is simply a collection of fields that logically belong together. These fields may share a common initial and final validator, a common label, a common help text and a few other things. You will see that field groups are very similar to embedded form as they exist right now.

In fact, a form is a field group with a few extra abilities such as CSRF protection, form tag generation etc.

Normal fields and field groups share a common interface. In practice, this means that you can simply add groups by calling addField() and nest groups inside each other. It does also mean that the developer doesn’t necessarily need to know whether a field is a “normal” field or a field group, which is exactly what we will do with the password field.

To understand the implementation, we will look at the common interface sfFormFieldInterface first:

interface sfFormFieldInterface
{
  public function bind($taintedValue, $taintedFile);
  public function getData();
  public function getId();
  public function getLabel();
  public function getHelp();
  public function render();
  ...
}

You may wonder what the methods bind() and getData() do in a form field. To use the real power of object orientation, the bound values are delegated from the form to all nested fields and field groups, where they are validated. The form can collect the cleaned values again by calling getData() on all fields and field groups. Because embedded forms are nothing more than field groups, this implementation also solves the common problem that bind() is not called on embedded forms.

Now we can finally implement our combined password field. The combined field extends sfFormFieldGroup which already implements sfFormFieldInterface, so we only need to configure the field:

class VerifiedPasswordField extends sfFormFieldGroup
{
  public function configure()
  {
    $this->addField('first', new sfPasswordField());
    $this->addField('second', new sfPasswordField())
         ->setValidator(new sfValidatorPass())
         ->setLabel('Password (again)');
 
    $this->setFinalValidator(
      new sfValidatorCompare('second', '==', 'first',
          'The passwords must be equal')
    );
  }
}

Now we can simply drop our password field into the form:

  public function configure()
  {
    $this->addField('password', new VerifiedPasswordField())
         ->setHelp('Must not be "pulpfiction"');
  }

Alert readers might have noticed that the password fields are now named “first” and “second”. Do you wonder how that will be translated into form field names? It works just the same way as it does now for embedded forms: The name of the internal field is appended to the name of the external field in squared brackets (f.i. “password[first]”).

Now you probably think that this implementation makes things harder than they were! When processing the values after a successful validation, we need to know that the password is stored in “password[first]” instead of simply “password”!

Calm down old boy, help is on the way. To find our solution, let’s look at the output of the method VerifiedPasswordField::getData(), which returns the cleaned values of the field group:

// VerifiedPasswordField::getData()
array(
  'first' => 'The password',
  'second' => 'The password',
)

Hm, any idea? Let’s also look at the output of ProfileForm::getData().

// ProfileForm::getData()
array(
  'name' => 'The name',
  'email' => 'thename@gmail.com',
  'password' => array(
    'first' => 'The password',
    'second' => 'The password',
  ),
)

Did you already find the solution? I’ll tell you: It’s fairly easy. We just need to modify getData() in VerifiedPasswordField to just return a single password instead of an array.

  public function getData()
  {
    $data = parent::getData();
 
    return $data['first'];
  }

Now you probably understand why getData() isn’t named getValues(), like it is in the forms framework right now. The problem is that getData() can return single values (in simple fields) or arrays (in field groups). Naming the method getValues() would implicate that it always returns an array.

Time To Relax

If you made it until here, the hardest part of this post is over. Field groups allow us to easily implement and reuse a set of fields. As you have seen, we can even use field groups as if they were a single field. Especially in large applications, this architecture can hugely simplify your form definitions and reduce duplicate code.

You can, of course, create field groups on the fly, if you want to logically group fields together:

  public function configure()
  {
    $group = $this->addGroup('personal')
          ->setLabel('Personal Information')
          ->setHelp('Please enter your personal information here so we can sell it');
    $group->addField('first_name')
          ->setLabel('First name');
    $group->addField('surname');
  }
Calling

$form->addGroup('groupname')

without explicitly passing an sfFormFieldGroup instance has the same effect as calling

$form->addGroup('groupname', new sfFormFieldGroup())

or

$form->addField('groupname', new sfFormFieldGroup())

In my next post, I will speak about how formatters and group layouts can help you to reduce duplicate code in your form templates and help you to sustain a uniform look in applications with many forms. You will learn that the structural information stored in field groups can easily be used to generate a visually appealing presentation.

How do you like the field groups? Are there any use cases that cannot be implemented with this architecture?

Posted Sunday, April 19th, 2009 at 11:56
Written by: | Filed Under Category: Thinking Ahead
You can leave a response, or trackback from your own site.

8

Responses to “Improving the Forms: Field Groups”

That’s even more than what I had in mind when thinking about form groups, and I really like the concept. The only thing not yet mentioned is how this can be used with formatters, but I think the whole formatter topic is yet something to be discussed in detail.

dereck

I think it’s worth having a look at the way Zend Framework handles this tasks http://framework.zend.com/manual/en/zend.form.html

Marijn

I really like your ideas, but… Isn’t this currently handled by using embedForm? As far as I know this can all be done already with a slightly different interface..

Well anyway great to see some community action around sfForm:-)

Bernhard

@Marijn: You couldn’t really do the above things with embedded forms, because embedded forms are not integrated in the whole value binding and validation process.

This is great. I’ve tinkered with the Django framework and liked the readability of the the form configuration code (which, if I remember rightly, is part of the schema definition). I’ve not really seen this done better, not that I’ve spent time looking or anything.

Symfony forms are awesome but I do feel an improved interface would simplify real-world projects, where (maybe due to my own inadequacies) I often spend most time tweaking with and/or fixing forms.

I’d definitely use this and encourage you to continue!

[…] Web Mozarts » Blog Archive » Improving the Forms: Field Groups […]

Rubino

An interesting proposal but wouldn’t it be better to completely delegate form generation to something yml based. Forms are generally static definitions associated with actions or components – we already use yml for the admin side – the implementation could be reused and enhanced in a more general way. If something dynamic is needed then you can hand-craft – an exception rather than the rule if it’s implemented well.

Allow for proper type definition of form elements (including types, defaults, validation), include field level security attributes and with clever use of the translation engine allow better default error messages & tooltips. Field grouping would be possible follows the same pattern we have in the generator.yml.

The use of the translation engine would allow text from resource files to be used in this scenario. The main changes needed to translation would be to implement a form of inherited lookup – best illustrated with an example:

all:
name:
label: Name
tooltip: Enter the name
validationError: You need to enter the name

registration:
name:
label: Login Name
tooltip: Enter the login name you’d like to use
validationError:
default: You need to register with a valid name
validationFail: That name already exists, try with another.

In the context of registration – the specific details would be used for the field – otherwise the site default would be used.

Generally what you’ve outlined is interesting – but while it’s simpler syntax – it’s much the same as we have already. I’d like to see something more flexible generally – where form fields are more component based and where the project structure defines more the defaults.

Something like:
lib\form\fields contains:
input.class.php [for the generic input field]
apps\thisApp\lib\form\fields:
input.class.php [overriding it for this application]
apps\thisApp\modules\thisMoule\form\fields:
input.class.php [overriding it for this module]

Pete BD

@Rubino: The YAML layer you describe could be built on top of this programmatic interface easily. But one will always need to have the option of programmatic (i.e. PHP) declaration and modification of forms.

Here is an idea: you could use YAML to generate the base form classes in the same way that Doctrine generates model classes. Then you could easily derive a custom class from this base class to fiddle with at run-time.

Leave a Reply

 

Additional Resources