Navigation

Improving the Forms

After my last post about simplifying symfony and especially the forms framework, I started diving into the forms framework to find out how we could improve it. I tried to find ways to improve the usability of this framework without reducing its mighty possibilities. Quite the contrary, I think that the forms can be made even mightier than they are 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

The Situation

In my last post I introduced you to the following situation: A form should allow to edit a user’s name, email address and password. The name should only be editable by administrators, while the password should be entered twice to make sure it was entered correctly. The maximum length of the name is 15 characters and the password should not be the same as the name.

We came up with the following class:

// 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->setWidgets(array(
      'name'           => new sfWidgetFormInput(array(), array(
        'max_length' => 15
      ),
      'email'          => new sfWidgetFormInput(),
      'password'       => new sfWidgetFormInputPassword(),
      'password_again' => new sfWidgetFormInputPassword(),
    ));
    $this->widgetSchema->setLabel('password_again', 'Password (again)');
 
    $this->setValidators(array(
      'name'           => new sfValidatorString(array(
        'max_length' => 15
      ), array(
        'max_length' => 'The name must be 15 characters or longer'
      ),
      'email'          => new sfValidatorEmail(array(), array(
        'required' => 'Please enter your email address',
      )),
      'password'       => new sfValidatorString(array(), array(
        'required' => 'Please enter a password',
      )),
      'password_again' => new sfValidatorPass(),
    ));
 
    // implement custom validation logic
    $this->validatorSchema->setPostValidator(new sfValidatorAnd(array(
      new sfValidatorSchemaCompare('password_again', '==', 'password', array(
        'invalid' => 'The passwords must be equal'
      )),
      new sfValidatorCallback(array(
        'callback' => array($this, 'comparePasswordAndName')
      )),
    )));
 
    // only administrators may enter the user name
    if (!$this->user->hasCredential('admin'))
    {
      unset($this['name']);
    }
  }
 
  public function comparePasswordAndName(sfValidator $validator, array $values)
  {
    // if the field "name" is not editable, it does not exist in $values
    if (array_key_exists('name', $values) && $values['name'] == $values['password'])
    {
      $error = new sfValidatorError($validator,
          'The password must not be the same as the name');
 
      // throw an error schema so the error appears at the field "password"
      throw new sfValidatorErrorSchema($validator, array('password', $error));
    }
  }
}

The Issues

The above form definition bears several issues:

  • It is not possible to abstract form fields that combine a widget, a validator and possible other settings. You might want to use a password field in several forms so it should be possible to extract that definition into a separate class.
  • To configure the form, you need to know about the widget schema and the validator schema. You also need to know where to call which method. For instance, you need to call setWidgets() on the form, setLabels() on the widget schema and setPostValidator() on the validator schema.
  • Nearly none of the default validator error messages like “Invalid.” or “Required.” are useful in production, thus we always have to configure them. Validators should provide sensible defaults for these errors to reduce the amount of configuration.
  • Some of the used classes have complex constructor signatures that are hard to remember. sfValidatorCallback, for instance is always called with a callback. It should be possible to pass the callback directly to its constructor without having to create addtional arrays.
  • If the user is not administrator, the field “name” is not available. Because of that we must implement additional checks everytime we access the form values to find out whether the name has been set. But, in fact, the name value should always be there. It just is not always editable.
  • Throwing a custom validation error and binding it to a specific field is tedious.

I was searching for ways to improve these issues and will explain them in more detail below.

Form Field Abstraction

It was very useful if you could abstract form fields in symfony that combine a validator, a widget and possible other settings. Let’s just imagine for this post that sfField is a class that allows bundling a widget and a validator.

I don’t think that including the “form” in the name is necessary (f.i. sfFormField). The “sf” prefix is reserved to symfony core classes and I can’t think of any other context in the core where an sfField class could serve useful. KISS.

To create our custom password field, we would just extend sfField and combine the logic inside.

// lib/form/fields/PasswordField.class.php
class PasswordField extends sfField
{
  public function configure()
  {
    $this->setWidget(new sfWidgetInputPassword());
    $this->setValidator(new sfValidatorString(array(), array(
      'required' => 'Please enter a password',
    ));
  }
}

Great! A neatly decoupled definition of a password field. But how should we add this field to a form? Probably with the most obvious method name there is for this task: addField().

// lib/form/doctrine/ProfileForm.class.php
class ProfileForm
{
  public function configure()
  {
    $this->addField('password', new PasswordField());
    ...
  }
}

Can it be easier?

You may ask yourself whether we can’t just bundle the second field for repeating our password as well. We certainly could, but I’ll leave the answer for this question to another post.

Widgets and Validators

As said before, we currently need to know a lot about the form’s internals to configure it successfully. We need to know about sfWidgetFormSchema, sfValidatorSchema and sfForm and their respective methods. Couldn’t we simplify that somewhat?

Let’s wait for a moment. In the previous section we introduced a new class sfField that bundles a widget, a validator and possible other settings. We also introduced a method addField() that adds a form field to the form.

Now we could return the added field from addField() and allow a modification of its widget, validator, label and other properties in a fluent coding style!

Let’s look at how the form’s configure() method looks like with these modifications:

  public function configure()
  {
    $this->addField('name', new sfField())
         ->setWidget(new sfWidgetInput(array(), array('max_length' => 15)))
         ->setValidator(new sfValidatorString(array('max_length' => 15)));
 
    $this->addField('email', new sfField())
         ->setWidget(new sfWidgetInput())
         ->setValidator(new sfValidatorEmail());
 
    $this->addField('password', new PasswordField());
 
    $this->addField('password_again', new sfField())
         ->setWidget(new sfWidgetInputPassword())
         ->setValidator(new sfValidatorPass())
         ->setLabel('Password (again)');
    ...
  }

Doesn’t look too bad. To reduce the amount of code, addField() could always add an instance of sfField, if no field is given, and preconfigure it with sfWidgetInput and sfValidatorString. Then we could introduce new methods sfField::setWidgetOptions(), sfField::setWidgetAttributes() and sfField::setValidatorOptions() that allow a fluent modification of these properties.

  public function configure()
  {
    $this->addField('name') // implicitly sfWidgetInput and sfValidatorString
         ->setWidgetAttributes(array('max_length' => 15))
         ->setValidatorOptions(array('max_length' => 15));
 
    $this->addField('email') // implicitly sfWidgetInput
         ->setValidator(new sfValidatorEmail(array(), array(
           'required' => 'Please enter your email address',
         )));
 
    $this->addField('password', new PasswordField());
 
    $this->addField('password_again')
         ->setWidget(new sfWidgetInputPassword())
         ->setValidator(new sfValidatorPass())
         ->setLabel('Password (again)');
    ...
  }

Again our code became a bit easier to read and write. For reference, here is how the configuration of these settings was like before:

  // before
  public function configure()
  {
    $this->setWidgets(array(
      'name'           => new sfWidgetFormInput(array(), array(
        'max_length' => 15
      ),
      'email'          => new sfWidgetFormInput(),
      'password'       => new sfWidgetFormInputPassword(),
      'password_again' => new sfWidgetFormInputPassword(),
    ));
    $this->widgetSchema->setLabel('password_again', 'Password (again)');
 
    $this->setValidators(array(
      'name'           => new sfValidatorString(array(
        'max_length' => 15
      ), array(
        'max_length' => 'The name must be 15 characters or longer'
      ),
      'email'          => new sfValidatorEmail(array(), array(
        'required' => 'Please enter your email address',
      )),
      'password'       => new sfValidatorString(array(), array(
        'required' => 'Please enter a password',
      )),
      'password_again' => new sfValidatorPass(),
    ));
    ...
  }

Sensible Default Error Messages

Nearly all default error messages of the validators are absolutely unusable in a production environment. If you listen to the big usability gurus like Jakob Nielsen (see his Error Messages Guidelines), error messages should give “Constructive advice on how to fix the problem”. “Required.” and “Invalid.”, unfortunately, do not.

I think that all validators should be preconfigured with sensible error messages. Examples for the “required” and “invalid” errors of sfValidatorEmail are “Please enter an email address” and “The given email address is not valid”.

With sensible default messages, we can yet again reduce the complexity of our code:

  public function configure()
  {
    ...
    $this->addField('email')
         ->setValidator(new sfValidatorEmail());
    ...
  }

Compared to before:

  // before
  public function configure()
  {
    ...
    $this->addField('email')
         ->setValidator(new sfValidatorEmail(array(), array(
           'required' => 'Please enter your email address',
         )));
    ...
  }

The Post Validator

Now I’ll look at how to improve the post validator of our profile form. Basically I think the name is a bit weird. I am familiar with the latin expression “post”, but wouldn’t the english expression “final” fit in better?

Let’s imagine we live in an ideal world where setPostValidator() can be moved to sfForm and renamed to setFinalValidator().

$this->setFinalValidator(...);

Okay. Next step: sfValidatorAnd and its contained validators. First of all, I think we could extend sfValidatorAnd to accept an infinite number of arguments or an array containing these arguments. In Java we would achieve this by overloading the method. In PHP we cannot overload methods, but due to the weak typing we can still simulate this behaviour with one method.

There’s another candidate for simplification: sfValidatorCallback. Let’s just recall the current usage of this class:

new sfValidatorCallback(array('callback' => array($this, 'comparePasswordAndName')));

Why is this so complicated? It’s obvious that we want to pass a callback to this class. A callback can be either a single string or an array with two entries. We can again “overload” the constructor to accept either a single argument (a function name) or two arguments (a class or object method).

new sfValidatorCallback($this, 'comparePasswordAndName');

Much better. Last step: sfValidatorSchemaCompare. I think the usage of its constructor is pretty straight-forward. Because I can never remember its name, this validator should be renamed to sfValidatorCompare (KISS). Furthermore it is always necessary to pass an error message to this class, so why not make it the fourth argument?

new sfValidatorCompare('password_repeat', '==', 'password',
    'The passwords must match');

Now we can draw the whole picture of our final validator:

    $this->setFinalValidator(new sfValidatorAnd(
      new sfValidatorCompare('password_repeat', '==', 'password',
          'The passwords must match'),
      new sfValidatorCallback($this, 'comparePasswordAndName')
    ));

Isn’t this much nicer to read? Here’s the old definition again:

    // before
    $this->validatorSchema->setPostValidator(new sfValidatorAnd(array(
      new sfValidatorSchemaCompare('password_again', '==', 'password', array(
        'invalid' => 'The passwords must be equal'
      )),
      new sfValidatorCallback(array(
        'callback' => array($this, 'comparePasswordAndName')
      )),
    )));

Editable vs. Non-Editable

In forms it is very common to display some fields as editable and some fields as non-editable depending on the user’s rights. Just think of a common article form. The administrator may be able to change the author of an article, while the editor may only be allowed to see the author.

In the past, I have read several times that people would like to set fields to read-only in the admin generator. One such situation was the discussion about the admin generator for symfony 1.2. Since the admin generator is now based on the forms framework, this would be essentially the same feature.

The idea is to extend the current sfWidgetForm class to have two different states: “editable” and “non-editable”. Dependent on the state, each widget renders different HTML. Let’s look at sfWidgetFormInputDate for example. If this widget is editable, it displays the three select fields that we all know. But if the widget is not editable, it renders the date as plain text in the configured culture.

Do you notice something? If such a widget is implemented, it is essentially not a form widget anymore, but just “a widget”. In this case we could remove the “Form” from its name and reuse the widget in other components, such as sfGrid ;-).

Let’s look at the configuration of our “name” field with this new widget concept. Instead of removing the field when the user is not an administrator, we just disable it.

    if (!$this->user->hasCredential('admin'))
    {
      $this->getField('name')->setEditable(false);
    }

That’s all! Compared to before:

    // before
    if (!$this->user->hasCredential('admin'))
    {
      unset($this['name']);
    }

The form should still populate the “name” value after submitting so that we do not have to care about whether the field was editable or not. The submitted value should just be ignored then.

Throwing Custom Validation Errors

Let’s be honest. Throwing custom validation errors is a pain in the ass. You must remember to pass the validator itself to the error, and then you need to construct an sfValidatorErrorSchema just to associate the error with a field.

In my opinion this should be much easier. The only reason why sfValidatorError requires a passed validator is to populate the error message internally. Why don’t we just pass the error message?

Second, sfValidatorError‘s constructor could simply accept an optional parameter that specifies the field the validator belongs to.

How does that all look like? Let’s look at our refactored method comparePasswordAndName():

  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');
    }
  }

Now that was easy! Here’s the old definition:

  // before
  public function comparePasswordAndName(sfValidator $validator, array $values)
  {
    // if the field "name" is not editable, it does not exist in $values
    if (array_key_exists('name', $values) && $values['name'] == $values['password'])
    {
      $error = new sfValidatorError($validator,
          'The password must not be the same as the name');
 
      // throw an error schema so the error appears at the field "password"
      throw new sfValidatorErrorSchema($validator, array('password', $error));
    }
  }

Conclusion

I think we don’t need to drop object oriented concepts to facilitate the use of the forms. The interface of this framework can be easy while remaining completely well-designed and flexible.

Here you can see the complete new form definition. Especially note the shorter widget names.

// 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));
 
    $this->addField('email')
         ->setValidator(new sfValidatorEmail());
 
    $this->addField('password', new PasswordField());
 
    $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');
    }
  }
}

The above ideas are only very rough and conceptual. I was thinking about how the interface would be ideal from the user’s point of view without verifying whether it is implementable and, if yes, how. But that’s a whole different story anyway. Usually, usability should drive implementation, not the other way round.

I did focus this post on this specific ProfileForm. There are several issues with the form framework that remain to be discussed. One example is how to bundle the two password fields and their post validator to be able to just drop them into any form. Another example is the internal processing of embedded forms. But these topics belong entirely to a new post.

How do you like this interface? Has it flaws and how can it be improved?

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

18

Responses to “Improving the Forms”

I love it! Escpecially the chaining of methods is great. This would be a big step forward for the symfony forms framework and could persuade lots of users to switch from 1.0 to 1.2.

One flaw in the current form framework that bugs me most is that you can’t pass options to the field formatters which in the end always makes you show all the fields by hand in the template. Have you already thought of a possible solution to this problem? I think the whole field formatter thing is nice but unusable in the end.
What about passing formatters as objects to each form widget? This could be done in the method chain and would allow to use as many different formatters as you need (so far 1 formatter was hardly enough in any form I created). There would of course be a default formatter for all fields based on what exists now!

naholyr

Very neat. The current form framework is really uncomplete in my opinion, and articles like this one tend to comfort me in this idea, it seems like it has been done by astronauts for astronauts (you know, the guys who are more interested in how things are made and structured, than how we could use it to ease our life).

Your propositions are very good, do you have a patch to propose this in the core ? If not I could provide help about this, ’cause it’s far from hard to be done.

About the renderer thing, I think it’s a bit too complicated for the moment, rendering a field is nothing more than calling a function which returns a string, so I’m not sure it wouldn’t be good to just pass a callback ?
sfFormWidget::setRenderer(callback), and sfField::setRenderer(callback) which is just a shortcut to sfField::getWidget()->setRenderer(callback).

The callback will take a sfFormWidget objet as argument, and returns a string. This way it could be a generic static method (which uses the parameter) or a dedicated sfField or sfFormWidget instance method (which will ignore the parameter and use $this).

Then it would integrate just like a charm and allow to see even more as a package this sfField class :

class PasswordField extends sfField
{

public function configure()
{
$this->setWidget(new sfWidgetInputPassword());
$this->setValidator(new sfValidatorString(array(), array(
‘required’ => ‘Please enter a password’,
));

// Defines renderer : could be automatically done when a “render” method exists in the sfField instance.
$this->setRenderer($this, ‘render’);
}

public function render()
{
// stupid example
return sprintf(”, $this->getWidget()->getName());
}

}

What do you think ?

Bernhard

@David: I would be very grateful if you could send me some specific use cases affected by your problem. Then it is much easier to think of a solution.

@naholyr: No, as I’ve written I don’t have a patch yet. I want to discuss my ideas with the community first before going to implement anything. Maybe there are some major flaws in my above proposition that prevent an implementation like that? I don’t know. Yet.

What exactly is the use case behind the field renderer? Isn’t it enough to extend the widget overriding the render() method?

naholyr

That’s the point : the idea behind the renderer as a callback is not to have to extend the widget class, to write less code.

We can then see \sfField\ as a \package\ which assembles a widget, its validator(s), and the way it should be rendered.

Of course as I see it, it’s just a possible use and you can pass either a callback or an instance of sfWidgetRenderer.

Two more ideas :

– setWidget() could take a string as first parameter so the library can guess the class name.
– same idea for setValidator().
– setValidator() could take ‘and’ and ‘or’ as parameters to build group of validators.

Example :
$this->addField(‘mail’)
->setWidget(‘mail’) // new sfWidgetFormMail()
->setValidator(new sfValidatorString(), ‘and’, ‘mail’) // new sfValidatorAnd(array(new sfValidatorString(), new sfValidatorMail()))

Well, that’s just a few ideas ;)

@Bernhard: I think since the renderer/formatter thing has become very important in my projects I’ll publish a little summary about how I use the form fields.

Altogether let’s not forget that a render() method in the widget class exactly opposes MVC and the templating, because it mixes template specifics with the widget definition. Always keep in mind that you might use the same widget in various forms with various formatter requirements.

Bernhard

@naholyr: I still don’t see the point of the renderer. The main point of the widget being there in the first point is … to provide a render method. That’s all the widget does. So how does the renderer differ from the widget?

I personally don’t like to introduce too much magic in the function calls (letting the library guess the class name etc.). The problem here is that the developer doesn’t really know what’s going on behind the scenes and you loose a lot of flexibility.

A better approach in my opinion is to make class names as short as possible, so it takes not much more characters to instantiate the class than to write the string.

[...] reading Bernhard’s great article about how the symfony forms framework could be enhanced I sat back and thought: how would I like the forms to be? What could be improved to increase my [...]

Henrik Bjornskov

Some of your ideas are indeed nice, and could be implemented but againg too simple is also a problem, and changing the way things are named in symfony is just a too big of a thing to do.

But some of it i am all for :)

Very interesting approach. I definitely look forward to an early release of any code, even at prototype stage. I could also contribute.

What about yaml form configuration ?

I am not arguing for a 100% coverage of form config via yaml — that’s possibly impossible and definitely a wast of effort.

However, for most forms, I’m pretty sure that the degree of customization is mostly a matter of reconfiguring the order of fields, possibly re-arranging them in groups, etc. All of which is simply a recombination of existing framework constructs: fields, formatters, layouts, widgets, etc. A yaml config file would then be perfectly suitable. More complex form customization could of course still be handled by extending classes, etc.

I would think therefore that allowing form definition via a yaml config file would be beneficial in quite a large number of situations. While this feature is not definitely not a must-have initially, I tend to think that its feasibility would also provide a strong validation of your concepts. If it’s easy enough to configure in the code, then a yaml configuration should really just be a layer above that logic.

sfField could have generic options. Example: setting the max_length of your “name” field isn’t DRY.

It could look like this:
$this->addField(‘name’, new sfFieldString())
->setOption(‘max_length’, 15)
;

sfFieldString would then parse the generic option and set the appropriate Options for widget and string.

Pete BD

Really great ideas. You are absolutely right that symfony needs to have a self-evident class library. There should be little or no need to consult a reference text to work out how to do something. This is especially true of PHP type languages where auto-completion is not completely reliable.

Two really simple (and aesthetic) suggestions:

1) Personally I prefer to use “readonly=true” rather than “editable=false”. It just reads :-) better.

2) I find the “final” in setFinalValidator no better than “post”. It still makes me think. Why not realise that what you are saying is that there are two kinds of validators: field validators that are specific to individual fields and form validators that are for the form as a whole? The only reason one needs to have “post” or “final” on the front of this method call is because at the moment all the validators are conceptually stored in the form. With your new sfField objects holding their own validators you can just have a setValidator method on the sfForm class or maybe setFormValidator if you must.

Bernhard

@Pete: Ad 2: This is actually a great idea! Thank you for your feedback.

Pete BD

@klemens: You could actually make this shorter still with

this->addField(’name’, new sfFieldString())
->setMaxLength(15);

Since addField returns the new sfFieldString() object you can call methods on it that are specific to its purpose.

Pete BD

@Bernard: It makes even more sense now that I read about the sfFormFieldGroup class. You can drop the setFormValidator altogether and just have setValidator in the sfFormFieldInterface
using it either at the field level, the field group level or the form level.

Indra

The flexibity provided by symfony forms are great but the problem is exposure to too much internal detail of forms and, at times, somewhat incoherent API.

My first set of recommendations will be as follows:
1)Make sure user of the framework can’t access widgetSchema or validatorSchema from sfForm.
2)Add methods, that user will call on widgetSchema or validatorSchema, to the form class itself. Examples are setLabel, setLabels, setOption, setOptions, setAttribute, setAttributes etc.
3)May be we should not even have a setPostValidator method. Its syntax is unnecessarily complex. Instead add a method to the form class named postValidate.
4)Add addField method to sfForm which takes the name of the field and returns an sfFormField which can be customized. See example below.

If we create a new class everytime we want to customize a form field, we might end up creating many small classes in our application. This may or may not be a problem depending on the number of classes you will have to create and maintain. However, I see that method chaining is a great idea. I would propose something like this which I have used in other (nonPHP) frameworks:

$this->addField( ‘first_name’ )
->setWidget( new sfWidgetInput() )
->setOption( ‘required’, true )
->setOption( ‘max_length’, 50 )
->setAttribute( ‘columns’, 20 )
->setAttribute( ‘style’, ‘background:grey’ )
->addValidator( … )
->addValidator( … );

$this->addField( ‘last_name’ )
->setWidget( new sfWidgetInput() )
->setOptions( array( ‘required’=> false, ‘max_length’ => 20) )
->setAttributes( array( ‘columns’ => 20, ‘style’ => ‘background:grey’ ) )
->addValidators( … );

– Indra

One of the most interesting symfony articles i’ve seen in awhile.
Glad you joined the core team ! Very good idea’s & input !

[...] заняла более двух лет, хотя думать над ним я начал еще где-то в 2009-ом году или даже раньше. С каждой новой версией этот компонент [...]

[...] заняла более двух лет, хотя думать над ним я начал еще где-то в 2009-ом году или даже раньше. С каждой новой версией этот компонент [...]

Leave a Reply

 

Additional Resources