A while ago, I wrote a post entitled "Faux Form Serialization". It was an overly-complex solution to a very easy problem. Today, I am writing about a much better implementation for solving the same problem. What is this concept? You've heard of MongoDB right? CouchDB? BeullerDB...? Of course you have. You've been tired of writing migrations to add and remove columns that are constantly changing, and at some point have had the thought
"By golly, I should just slap an options field on this honkey!"
You're exactly right, and this is what you should do. Doctrine already has a lovely array field type. Similarly, it serializes an array into a longtext database column and unserializes upon hydration. Well that sounds useful! Exposing these fields can be tricky though. We have this lovely form framework with all its pretty widgets and validators, and the ability to embed them! Yet, since this isn't a true related object, we can't do this out of the box... or can we?!

Example Time

Your sfGuardUser object has a laundry list of silly auxiliary fields, including items such as "how often do you zumba?" and "what animal produces your favorite milk?". You refuse to write migrations for these things. Instead, you'll save them all to a single field of type array called extra_profile_fields. Good for you! Create a form:
// lib/form/sfGuardUserExtraProfileFieldsForm.class.php
class sfGuardUserExtraProfileFieldsForm extends BaseForm
{
   public function configure()
   {
      $this->setWidgets(array(
        'zumba_frequency' => new sfWidgetFormChoice(array('choices' => array('Not at all', 'Sometimes', 'Every Single Day', 'I NEVER STOP'))),
        'favorite_animal_milk' => new sfWidgetFormInput(),
      ));

      $this->setValidators(array(
        'zumba_frequency' => new sfValidatorChoice(...),
        'favorite_animal_milk' => new sfValidatorString(...),
      ));
   }
}
Now, use my very special sfWidgetFormEmbeddedForm and sfValidatorEmbeddedForm
// lib/form/doctrine/sfDoctrineGuardPlugin/sfGuardUserForm.class.php
class sfGuardUserForm extends PluginsfGuardUserForm
{
    public function configure()
    {
       ....
       $form = new sfGuardUserExtraProfileFieldsForm();
       $this->widgetSchema['extra_profile_fields']    = new sfWidgetFormEmbeddedForm(array('form' => $form));
       $this->validatorSchema['extra_profile_fields'] = new sfValidatorEmbeddedForm(array('form' => $form)); 
    }
Wow, is it that easy? YES IT IS. The form will render expectedly, with validators and all. However, make sure you pass the same form object to both the widget and the validator! If you, for instance, pass a new sfGuardUserExtraProfileFieldsForm instance to each, you'll have two separate forms, and errors in your form will not be rendered.

Extra Fun: Using Archivers

Let's say you wanted the options to save as YAML, JSON, or a PHP-serialized string. As long as you use the "array" column type in doctrine, none of this is necessary. But if you need this flexibility, I've got your back. The ArchiverInterface specifies a class with three methods: sleep, wake, and isAsleep. Essentially, it will convert from a certain array to a format and back again. Here are some sample archivers for your pleasure alone. Pass the class name as the "archiver" option to widget and validator, and you're all set! Hopefully you guys will enjoy this and use this as much as I have. And please don't hesitate to submit comments, suggestions, and questions.

6 comments

  • Sorin Neacsu - March 29, 2011

    Wouldn’t an EAV table be better than serialization into a single column? The resulting database is far from normalized and if you wanted to do something with that data at the DB level you’re out of luck …

  • Brent Shaffer - March 29, 2011

    The assumption here is this data that does not need to be normalized. Over-normalization, like over-architecting, can kill an application with complexity. Often times you just don’t need it.

    This post is also about a great way to visualize YAML, XML, or JSON configuration as a symfony form, utilizing the richness of the symfony widget and validator libraries.

  • Richtermeister - April 4, 2011

    Hey Brent,

    nice writeup – glad to see I’m not the only one doing this 🙂
    To Sorin’s point, I think what you’ve shown here is that the form and the options fields can be nicely decoupled, regardless of persistence strategy. So, whether the data goes into a serialized field or an EAV table, doesn’t really matter.

    Cheers,
    Daniel

  • Fabien - April 6, 2011

    Hi Brent, I see your point for all of this if you want to save your data as Yaml, Json and so on but otherwise for a lambda user it’s easier to use the default symfony stuff to serialize (with the doctrine array column type of course):
    $this->validatorSchema->setOption(‘allow_extra_fields’, true);
    $form = new sfGuardUserExtraProfileFieldsForm();
    $this->embedForm(‘extra_profile_fields’, $form);

  • Brent Shaffer - April 6, 2011

    That’s excellent! You’re correct, in my use-case I was saving as YAML. But for a Doctrine array column, using embedded forms like this is great.

    Embedded forms also allow you to iterate through fields and customize your HTML, which the widget method does not.

  • Symfony Central - May 23, 2012

    Hi Brent,

    This is a great little tutorial for Symfony 1.x, have you considered writing any for Symfony 2?

    We’re trying to build up a community around the new version of the framework, would love to hear you feedback / thoughts if you have any 🙂