Archive for December, 2007

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?

How to learn symfony?

Last week, I gave another symfony workshop with Fabien Potencier and Marc Hugon in Paris. It was a great pleasure and a great experience, especially given the quality of the audience (this last sentence should bring me some kind comments).

We had thirty attendees, and that’s a lot of people to train! Even if it was already the fourth time that I gave this particular training program, I still learned a lot from people’s reactions. Given the feedback we got, it looks like we managed to pass some parts of our knowledge and experience. But the amount of things to learn is just too much, so I think we should cut on some feature descriptions. The problem is, that every person comes with some special expectations about i18n, Ajax, security, form validation, etc. If we try to satisfy them all, three days is clearly not enough.

Symfony workshops try to focus on the practice rather than the theory. In order to avoid endless talks about features that people generally forget the minute the slide disappears, we make the attendees work. Everyone is supposed to bring a laptop with a LAMP stack already installed (and that never happens). The workshop starts by transforming a classical PHP application into a symfony one. I think this first exercise is the most satisfactory, because in about four hours people know all the basics about symfony applications.

Practice also makes the “why” of things clearer. Why do partial names start with an underscore? Why are fixture files in YAML? Why does the plugin system uses PEAR? Why sfGuard is a plugin and not a core feature? The answers to these questions come naturally after a few hours of coding symfony applications.

But there are many concepts behind symfony that need explanation, and practice cannot suffice. The problem is that once you start explaining ORM, MVC, DRY, and KISS, you need to perform really well to keep having people’s attention. I think that’s the hardest part of giving a training: not losing the audience when you have to explain something that requires at least ten minutes of talking. Especially when your voice vanishes from too much talking…

Anyway, this session helped us realize how to further improve the training material, and how to get people more ready for what they will learn. No doubt that the next sessions will be even better!

That being said, what do you think is the best way to learn symfony?

Rails getting inspiration from symfony

Rails 2.0 improvements on fixtures look a lot like the standard symfony 1.0 fixtures features - apart from the many-to-many syntax. Take a look at this screencast in Railscast to see how revolutionary fixtures are in the latest Rails release.

I like looking at Rails, Django, Zend Framework and other good open-source projects to take inspiration. It doesn’t prevent me from thinking on my own, but it also prevents me from reinventing the wheel. It is nice to see that the Rails developers do the same and don’t keep on thinking that they invented the Best Thing Around.