sfForms: The Missing Component
Symfony 1.1 introduces a new Form subframework, combining Widgets and Validators to generate poweful form objects. Thatsquality.com explains how to use the new objects in a series of tutorial articles.
Symfony has always been about providing powerful and easy-to-use components to implement the best practices of web development and avoid long and repeating code. But there is a problem with the new subframework: it is incredibly powerful, but also incredibly not easy to use.
The Base Example
I won’t explain the whole thing here, but this is the main part: to define both the components and validation rules of an sfForm object, you must use the setWidgetSchema() and setValidatorSchema() methods of an sfForm extending object, in a configure() method.
Here is an example for a contact form:
class ContactForm extends sfForm
{
public function configure()
{
$this->setWidgetSchema(new sfWidgetFormSchema(array(
'email' => new sfWidgetFormInput(),
'name' => new sfWidgetFormInput(),
'body' => new sfWidgetFormTextarea(),
'captcha' => new sfWidgetFormReCaptcha(array('public_key' => sfConfig::get('app_recaptcha_public_key'))),
)));
$this->setValidatorSchema(new sfValidatorSchema(array(
'email' => new sfValidatorString(),
'name' => new sfValidatorAll(array(new sfValidatorString(array('required' => false), new sfValidatorEmail())), # not sure about the syntax there
'body' => new sfValidatorString(),
'captcha' => new sfValidatorReCaptcha(array('private_key' => sfConfig::get('app_recaptcha_private_key'))),
)));
$this->widgetSchema->setNameFormat('contact[%s]');
}
}
I won’t discuss the power of the system, it’s incredible and allows you for almost everything in no time.
Enters YAML
In no time? But what if the sfForm object had a configureFromArray() method accepting an associative array - or, in other terms, a YAML configuration? The same as above could be achieved with this YAML code:
contact: # This key is not necessary. Just an entry point for the hash
name_format: contact[%s]
fields:
email:
widget: { class: sfWidgetFormInput }
format: # 'format' is just so much faster to write than 'validator'
- { class: sfValidatorEmail }
- { class: sfValidatorString } # This is how I combine two validators with an AND
required: true
name:
widget: { type: input } # Easy to guess class from type, which is more familiar and faster to write
format: { type: string } # If only one validator, no need for an array
required: false
body:
widget: { type: textarea }
format: { type: string }
required: true
captcha:
widget: { type: reCaptcha, private_key: %APP_RECAPTCHA_PRIVATE_KEY% } # All keys except type and class are passed to the constructor
format: { type: reCaptcha, private_key: %APP_RECAPTCHA_PRIVATE_KEY% }
required: true
That’s longer than the PHP code, will you say. For now.
Convention over configuration
No need to be so verbose, symfony can have conventions for the 80% most common cases and do part of the job for you. So the configuration could reduce to:
contact:
name_format: contact[%s]
fields:
email:
widget: input # if only type, no need to use hash
format: [email, string]
# required: true # required is true by default
name:
# widget: input # widget is of type input by default
# format: string # format is of type string by default
required: false
body:
widget: textarea
# format: string # if widget is defined, format has sensible defaults (ex: input/textarea => string, date => date, etc )
# required: true
captcha:
widget: { type: reCaptcha, private_key: %APP_RECAPTCHA_PRIVATE_KEY% }
format: { type: reCaptcha, private_key: %APP_RECAPTCHA_PRIVATE_KEY% }
# required: true
Cleaning up
Once all the implied settings removed, the configuration could be shorter:
contact:
name_format: contact[%s]
fields:
email:
format: [email, string]
name:
required: false
body:
widget: textarea
captcha:
widget: { type: reCaptcha, private_key: %APP_RECAPTCHA_PRIVATE_KEY%, autoformat: true }
# if autoformat is true in the widget, then format takes the same params as widget (minus, of course, autoformat)
Or, if we use the inline YAML syntax:
contact:
name_format: contact[%s]
fields:
email: { format: [email, string] } # Wait a minute. Couldn't symfony guess this one for me ?
name: { required: false }
body: { widget: textarea } # And what about this one ?
captcha:
widget: { type: reCaptcha, private_key: %APP_RECAPTCHA_PRIVATE_KEY%, autoformat: true } # And this one ?
One step further
If symfony were really smart, all you should write to define the widgets and validation rules of a contact form would be:
contact:
name_format: contact[%s]
fields:
email: ~
name: { required: false }
body: ~
captcha: { private_key: %APP_RECAPTCHA_PRIVATE_KEY% }
THAT, my friend, is both powerful and very easy to use.
More use cases
The principle of YAML configuration works well on simple forms. But I believe it can be extended to most of the forms, leaving only 10% to 20% of the forms to be defined with setWidgetSchema() and setValidatorSchema(). Here are more examples:
my_form:
culture: fr
focus: message # Yep, that's for JavaScript focus()
fields:
id: ~ # let me guess: { widget: hidden, format: integer } ?
name: ~
email: ~
date_of_birth:
label: Date of birth # I know it belongs to the widget, but it's just more natural like that
widget: date
format: { type: date, min: 1900/01/01, max: 2005/31/12 }
message:
widget: textarea
format: string or number # Why not use the wonderful text validators here ?
want_to_be_spammed:
widget: { type: checkbox, default: true, tabindex: 3, class: foo } # How do you define default values with the widgets?
subject:
widget: { type: select, choices: [administrative, legal, commercial], default: administratif }
recipient:
widget: { type: selectMany, populator: [this, getPossibleRecipients] } # Populator expects a callable to populate choices
title:
type: input
format: not in Article::title # That's a text validator for the sfPropelUniqueValidator
What do you think? Should symfony implement this system by default?
Comments(22)