back

HTML5 Forms with Symfony 1.4

HTML5 forms are an exquisite gift from the almighty spaghetti monster, and, as a bonus, they have a practical use: If you use an <input type="number">, i- and other -pad uses will get the numeric keyboard when they go to write something in it.

This is really not something negotiatable; good user experience demands it. But how to get it done without going through every single form and keeping the types right? It is practically guaranteed that there will be bugs with every change, because changing a simple data type in your schema requires changing a form and changing the template. It's a maintenance nightmare.

If you really need to just quickly override one form, you can do it like so:

<!--This is a horrible idea for anything more than a quick temporary fix for a field or two-->
<?php echo $form['myFieldName']->render(['type' => 'number']);

Luckily, the creators of symfony have enough experience with maintenance nightmares to have baked in a solution: The much hated (by beginners) and calmly appreciated (by intermediates) form framework. And lo and behold, it even comes with a plugin that has all the required HTML5 form elements readily available. Thanks, dear symfony creators, for making a framework that even while aging attracted enough attention in its time for the most common problems to be already solved! So here is how to get HTML5 forms into the framework:

# in the symfony project root
wget http://plugins.symfony-project.org/get/sfHtml5FormPlugin/sfHtml5FormPlugin-0.4.9.tgz
tar -xvzf sfHtml5FormPlugin-0.4.9.tgz
mv sfHtml5FormPlugin-0.4.9 plugins/sfHtml5FormPlugin
rm package.xml

Then add sfHtml5FormPlugin to the array in $this->enablePlugins in config/ProjectConfiguration.php.

Yay! The form framework now has HTML5 form widgets, making it possible to cleanly use HTML5 forms without resorting to ugly unmaintainable hacks like overriding the type in the templates. We have saved ourselves from changing each and every template where a form is used. But we still have to change every form:

<?php
// This is somewhat acceptable but still way too much manual labor for comfort
class myForm extends BaseFormDoctrine {
  configure() {
    // $this->widgetSchema['myFormField'] = new sfWidgetFormInputText();  // removed
    $this->widgetSchema['myFormField'] = new sfWidgetFormInputNumber();   // added
  }
}

Maintaining every form is much easier than maintaining every template everywhere where said form might be used, but it still a huge waste of precious thought power to avoid mistakes in doing so.

Luckily, apparently the symfony creators thought so too, which is why they baked generating forms from the database schema right into their framework. They didn't make it super obvious to end users, but poking (well, rather grepping) around in the source code identifies a neat function that assigns form widgets to data types during form generation. And, lo and behold, it can be overridden*. Observe:

<?php
// lib/task/myDoctrineBuildFormsTask.class.php

// This creates a new form build class with a different name

class myDoctrineBuildFormsTask extends sfDoctrineBuildFormsTask
{
  protected function configure()
  {

    // This is an exact copy of the configure task in sfDoctrineBuildFormsTask,
    // the only change is $this->name to something unique, and the default generator class

    $this->addOptions(array(
      new sfCommandOption('application', null, sfCommandOption::PARAMETER_OPTIONAL, 'The application name', true),
      new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environment', 'dev'),
      new sfCommandOption('model-dir-name', null, sfCommandOption::PARAMETER_REQUIRED, 'The model dir name', 'model'),
      new sfCommandOption('form-dir-name', null, sfCommandOption::PARAMETER_REQUIRED, 'The form dir name', 'form'),
      new sfCommandOption('generator-class', null, sfCommandOption::PARAMETER_REQUIRED, 'The generator class', 'myDoctrineFormGenerator'),
    ));

    $this->namespace = 'doctrine';
    $this->name = 'build-forms-html5';
    $this->briefDescription = 'Creates form classes for the current model';

    $this->detailedDescription = <<<EOF

Extends the normal doctrine build forms task with HTML5

EOF;
  }
}


<?php
// lib/generator/myFormGenerator.class.php

// This overrides the widget class assignment

class myDoctrineFormGenerator extends sfDoctrineFormGenerator {

  public function getWidgetClassForColumn($column)
  {
    switch ($column->getDoctrineType())
    {
      case 'string':
        $widgetSubclass = null === $column->getLength() || $column->getLength() > 255 ? 'Textarea' : 'InputText';
        break;
      case 'boolean':
        $widgetSubclass = 'InputCheckbox';
        break;
      case 'blob':
      case 'clob':
        $widgetSubclass = 'Textarea';
        break;
      case 'date':
        $widgetSubclass = 'Date';
        break;
      case 'time':
        $widgetSubclass = 'Time';
        break;
      case 'timestamp':
        $widgetSubclass = 'DateTime';
        break;
      case 'enum':
        $widgetSubclass = 'Choice';
        break;

      // This case block was added
      case 'integer':
      case 'float':
      case 'decimal':
        $widgetSubclass = 'InputNumber';
        break;
      default:
        $widgetSubclass = 'InputText';
    }

    if ($column->isPrimaryKey())
    {
      $widgetSubclass = 'InputHidden';
    }
    else if ($column->isForeignKey())
    {
      $widgetSubclass = 'DoctrineChoice';
    }

    return sprintf('sfWidgetForm%s', $widgetSubclass);
  }

}

*If you even thought about hacking the symfony source, the flying spaghetti monster will surely suffocate such an unworthy transgressor in its meat balls.

And here we are! In our entire app, no matter how large, we have now switched all form element to the correct HTML5 variant with no further effort or risk of breakage. This, my friends, is maintainability. We do need to make one mental note, which I would have preferred to avoid, but doing so would probably have been overkill. We need to build our forms with

./symfony doctrine:build-forms-html5

instead of the standard ./symfony doctrine:build-forms or ./symfony doctrine:build --all-classes. So build-forms-html5 und build-model should probably be called seperately in some kind of build script instead of just using the catch-all ./symfony doctrine:build --all. But changing or creating a build script is a one time thing, and fiddling with templates is forever.

Note that in both class overrides, the entire method is copied from the symfony source and edited, instead of calling the parent and then doing our thing. This would normally be considered gratuitious code duplication and a maintenance nightmare of its own, but since the symfony 1.4 source is essentially static it is acceptable in this case. But it would still be good to port bug fixes from the source into the overridden method in the unlikely event that there would be a change.