Displaying Symfony form fields recursively

The most significant change in the Symfony 1.1 is the new form framework. Almost every aspect of working with forms is moved to a separate class. Form fields, validators etc. even handling of saving Propel objects is handled by a custom class that extends sfForm class.

Form framework is extremely powerful, but comes at a price of a learning curve. Along with Symfony's form book another must read is a series of articles on Symfony forms  by Thatsquality.com, but to really understand the way Symfony forms work you will often have to peruse the API and even look up source code.

The easiest way to output form fields is this: <?php echo $form; ?>

While being extremely easy this method gives you no flexibility in customizing form view beyond that of the sfForm. Let's illustrate this with an example.

Let's say you want to build a form where there are several nested fieldsets that are accessed with JavaScript tabs. This way of displaying a form is useful when working with internationalized Propel objects where each i18n form is in a separate tab. Displaying such a form cannot be achieved by echoing the form object. The logical way here would be iterating through the field schema and outputting rows one by one. The problem is that Symfony so far does not provide built in iteration mechanism for form objects.

In this post we are going to discuss displaying form fields recursively.

Let's start with Propel objects


<table name="page" isI18N="true" i18nTable="page_i18n">
  <column name="id" type="INTEGER" required="true" autoIncrement="true" primaryKey="true" />
  <column name="url" type="VARCHAR" size="100" required="true" default="" />

<table name="page_i18n">
  <column name="id" type="INTEGER" required="true" primaryKey="true" />
  <column name="culture" type="VARCHAR" size="5" required="true" isCulture="true"  primaryKey="true" />
  <column name="title" type="LONGVARCHAR" required="true" />
  <column name="content" type="LONGVARCHAR" required="true" />
  <foreign-key foreignTable="page" onDelete="cascade">
    <reference local="id" foreign="id"/>

We are going to be using form classes generated by Symfony. Add list decorator to form objects:

$decorator = new myWidgetFormSchemaFormatterList($this->widgetSchema);
  $this->widgetSchema->addFormFormatter('list', $decorator);

Embed I18n into PageForm class:

$this->embedI18n(array('en_US', 'fr_FR', 'ru_RU'));

I18n objects are all represented by a row in sfFormFieldSchema where the key is the culture.

We are going to use Control Tabs for our JavaScript tabs.

Displaying the form would look like this:

<?php $schema = $form->getFormFieldSchema(); ?>
<?php $languages = array('en_US', 'fr_FR', 'ru_RU'); ?>
<?php $tabs = false; ?>

<?php while ($schema->offsetExists($schema->key())): ?>

  <?php if (in_array($schema->key(), $languages)): ?>
    <?php if (!$tabs): ?>       
      <?php $tabs = true; ?>
      <ul id="language_tabs">
      <?php foreach ($languages as $language): ?>
        <li><a href="#<?php echo $language; ?>"><?php echo $language; ?></a></li>
      <?php endforeach; ?>
      <div style="clear:both;"></div>         
    <?php endif; ?>
    <div id="<?php echo $schema->key(); ?>" class="language">
      <h2><?php echo $schema->key(); ?></h2>
      <?php echo $schema->current()->render(); ?>
  <?php elseif($schema->key() !== 'hidden'): ?>
    <?php echo $schema->current()->isHidden() ? $schema->current()->render() : $schema->current()->renderRow(); ?>
  <?php endif; ?>
  <?php $schema->next(); ?>
<?php endwhile; ?>

  new Control.Tabs('language_tabs');

Let's analyze the code.

$tabs variable will tell us whether we have reached the embedded i18n forms, because then we will have to output the actual tab markup.

<?php while ($schema->offsetExists($schema->key())): ?>

Currently the internal pointer is on the first form field and we need to iterate through all the fields until none are left. We get current field key with key() method and check if the row exists for this key with offsetExists() method.

<?php if (in_array($schema->key(), $languages)): ?>

In this line we check if the field row we are about to display is an embedded i18n form.

<?php echo $schema->current()->render(); ?>

This line renders the actual decorated form row.

<?php elseif($schema->key() !== 'hidden'): ?>
        <?php echo $schema->current()->isHidden() ? $schema->current()->render() : $schema->current()->renderRow(); ?>
      <?php endif; ?>

These lines are needed because Symfony places hidden rows twice in the form object -- once as a row that has a key value of "hidden" and one more time as a normal row with the appropriate key. For example in our forms there is the "id" field that is a hidden field. Id field has it's own row with "id" as a key and is also present in the row with "hidden" as a key. When echoing a form this situation is worked out automatically so it is only a problem here.

Basically what we do here is we ignore the row with key that equals to "hidden" and display the hidden input where the appropriate row would have been.

That's it. I think this solution lets you have the flexibility of displaying the field rows the way you want them without having to code each field row by hand. Sort of like to have the cake and eat it too. I hope you find this solution useful.



© 2003 — 2017 Akinas
All rights reserved