forms

6 things to know about embedded forms in Symfony

When using symfony, if you use it long enough, pretty soon you will need to use embedded forms. The down side of this is, embedded forms can be problematic at best. Seeing as I have spent a lot of time getting embedded forms working just right, let me show you some of the things I have learned, so that maybe I can spare you from the same fate as our friend on the right.

1. save() is only called on the root form

That’s right! Only the root form has save() called. So if there’s other logic you want to run, you will want to override the saveEmbeddedForm method and call that code before. Oversimplification ahead: when you save a form with embedded forms, it calls $this->getObject()->save(), then it calls saveEmbeddedForms, which, for each embedded form, calls $form->saveEmbeddedForms() and then calls $form->getObject()->save(). This is critical to know, as it will save you a lot of headaches later on.

2. Extend classes for simplicity and security

When you’re working on embedding a form into a related object’s form, do yourself a favor and always extend your form to remove unneeded fields such as id, and the foreign key field. It will make your life much simpler.

For example, here’s the configure() command from my embedded version of the media registry form in my majaxDoctrineMediaPlugin:

The only reason I’m bringing the embedded form in is to be able to assign photos, videos, and audio clips to galleries, so all I really care about is $this[‘galleries_list’]. Here’s how I use it then, in the Video form:

You see there how we pass the instance that is related to our object to the embedded form? That’s why we can unset all of the foreign keys… because it already knows about the relation! Removing the foreign keys keeps it simpler, and improves security.

3. Embedded many-to-many relations are TRICKY

Ok, well, they are if you don’t know what I’m about to tell you. But then you will. Remember when we were talking about save() and saveEmbeddedForms() and how saveEmbeddedForms() doesn’t call the embedded form’s save() function? Well, this is why many-to-many relations break down in embedded forms. The function to save those relations are never called. Even better, because the embedded forms are never officially bound, they don’t even store the data to run those functions. To solve this, I use a two-prong attack.

First, I trick it into allowing us to access the values passed through bind:

Then, after being tricked into thinking it’s a real form, we close the deal by forcing it to run the function Doctrine built to save our many-to-many relationship:

4. I do it MY SELF

While watching the screens roll by in #symfony (on irc.freenode.org), I often see people having this problem or that problem with related forms. Invariably, they will be using the embedRelation command. To be honest… I don’t know what this does. I do know people seem to have lots of trouble with it though! I know, it saves time. I know, it makes it easy. I know, it’s a stock function, so you should use it, instead of expending more effort.

I also know that my forms work! I’m a huge fan of Doing What Works(tm) and Getting The Job Done(tm). It may take an extra 3 minutes to extend your form class (point #2), and hack it to make your many-to-many relations work (point 3), but it’s a whole heck of a lot better than slamming your head into the wall (or keyboard) repeatedly.

5. Post validators only fire on the root form

Another reason to simply consider your embedded form a different beast than the form on it’s own, is that any post validators you have set to run in the embedded form will not be run! However, you can access your embedded form’s data in the root form’s post validator.

Here’s an example of how to make such a validator:

6. embedMergeForm

This is something picked up from Roland Tapken. It’s really quite beautiful. Before embedMergeForm, you had two options when it came to bringing two forms together. On one hand, you have mergeForm, which looks nice, but for all intents and purposes, without a lot of extra work, doesn’t work. On the other, you have embedForm, which works for many situations, but in some circumstances (the admin generator) it will produce horrible results visually. embedMergeForm is the key! It combines the beauty of mergeForm with the mostly functional setup of embedForm.

Just to make it real easy, here’s the code I use. Place this in your lib/form/BaseFormDoctrine.class.php:

  /**
   * Embeds a form like "mergeForm" does, but will still
   * save the input data.
   */
  public function embedMergeForm($name, sfForm $form)
  {
    // This starts like sfForm::embedForm
    $name = (string) $name;
    if (true === $this->isBound() || true === $form->isBound())
    {
      throw new LogicException('A bound form cannot be merged');
    }
    $this->embeddedForms[$name] = $form;
 
    $form = clone $form;
    unset($form[self::$CSRFFieldName]);
 
    // But now, copy each widget instead of the while form into the current
    // form. Each widget ist named "formname|fieldname".
    foreach ($form->getWidgetSchema()->getFields() as $field => $widget)
    {
      $widgetName = "$name-$field";
      if (isset($this->widgetSchema[$widgetName]))
      {
        throw new LogicException("The forms cannot be merged. A field name '$widgetName' already exists.");
      }
 
      $this->widgetSchema[$widgetName] = $widget;                           // Copy widget
      $this->validatorSchema[$widgetName] = $form->validatorSchema[$field]; // Copy schema
      $this->setDefault($widgetName, $form->getDefault($field));            // Copy default value
 
      if (!$widget->getLabel())
      {
        // Re-create label if not set (otherwise it would be named 'ucfirst($widgetName)')
        $label = $form->getWidgetSchema()->getFormFormatter()->generateLabelName($field);
        $this->getWidgetSchema()->setLabel($widgetName, $label);
      }
    }
 
    // And this is like in sfForm::embedForm
    $this->resetFormFields();
  }
 
  /**
   * Override sfFormDoctrine to prepare the
   * values: FORMNAME|FIELDNAME has to be transformed
   * to FORMNAME[FIELDNAME]
   */
  public function updateObject($values = null)
  {
    if (is_null($values))
    {
      $values = $this->values;
      foreach ($this->embeddedForms AS $name => $form)
      {
        foreach ($form AS $field => $f)
        {
          if (isset($values["$name-$field"]))
          {
            // Re-rename the form field and remove
            // the original field
            $values[$name][$field] = $values["$name-$field"];
            unset($values["$name-$field"]);
          }
        }
      }
    }
 
    // Give the request to the original method
    parent::updateObject($values);
  }

Software Updates

I just wanted to make a quick post to update everyone on the progress I’ve been making on my symfony plugins.

majaxDoctrineMediaPlugin (github repository and symfony plugin)

A number of things have happened here:

  1. It’s now in the symfony plugin repository!
  2. All of it’s dependencies have been properly configured in the pear package.xml
  3. It gained concurrency support to keep from wasting CPU and mangling the files it processes.

majaxPheanstalk (github repository and symfony plugin)

It is now in the symfony plugin repository!

majaxJqueryPlugin (github repository and symfony plugin)

It is now in the symfony plugin repository!

majaxMarkdownPlugin (github repository and symfony plugin)

It is now in the symfony plugin repository!

A simple example on how to use post validators in symfony

Someone came into the #symfony channel on freenode asking how to add conditional validators that would require a text box to be filled out when a checkbox was ticked. As I realize this is a common question (though the circumstances may change), I wanted to put this example up here as well, for others.

Here’s the example:

class TestForm
{
  public function configure()
  {
    // ... snip ...
 
    $this->validatorSchema->setPostValidator(
      new sfValidatorCallback(array('callback' => array($this, 'contextualChecks')))
    );
  }
 
  public function contextualChecks($validator, $values)
  {
    if ($values['checkbox_1'] == 1)
    {
      if ($values['textbox_for_checkbox_1'] == '')
      {
        $error = new sfValidatorError($validator, 'Field is required when Checkbox 1 is checked.');
        $ves = new sfValidatorErrorSchema($validator, array('textbox_for_checkbox_1' => $error));
        throw $ves;
      }
    }
    return $values;
  }
}

Note: I completely left out returning $values in the initial draft. I apologize!

How to build a better date widget in symfony with jQuery

We’re going to step things up a notch with this one, so hang on for the ride, because for those of you not already well versed in symfony and jQuery, there will be a lot of new information, and it will go pretty quickly, but I will try to make sure to keep everyone caught up. This is another post I hinted towards back when I first wrote about symfony widgets. Right, so, on to the fun!

First things first, let’s talk about requirements. My only real requirement is that it must be seamless. We must be able to only implement the widget, and have everything else work exactly as intended. For a date widget, this means when the user clicks that submit button, an array with ‘month’, ‘day’, and ‘year’ must be sent back. Not a string, not JSON, just that basic array.

It’s for the above reason that we will need to make a widget on top of the basic jQuery UI Datepicker Widget. The Datepicker alone would just return a string when we submitted the form, which would then require at least a custom validator, if not much more. So to simplify things later, we’ll put in more effort now.

There are four things we will be doing in this tutorial to make this happen. Here’s a quick overview:

  1. Set up majaxJqueryPlugin to give us our jQuery/jQuery UI base
  2. Set up a new module for us to use with the widget tutorials
  3. Create our widget class
  4. Create our jQuery UI widget that goes along with our widget class

1. Setup majaxJqueryPlugin

We’re using my plugin for two reasons:

  1. It’s my blog, so of course, I’m using my plugin
  2. We need jQuery UI, both the JavaScript and the CSS, and majaxJqueryPlugin provides that right away

Setting it up is easy:

cd plugins
wget -O majaxJqueryPlugin.zip http://jmather.com/wp-content/plugins/download-monitor/download.php?id=4

Now we add the plugin to our Project Configuration. Open up your config/ProjectConfiguration.class.php file, and place this line somewhere within your setup() function:

    $this->enablePlugins('majaxJqueryPlugin');

And one last step, we need to publish the assets:

./symfony plugin:publish-assets

Alright, now we’re ready to move on to the good stuff.

2. Setting up our plugin

Now we need to make a new plugin, and set up a few directories for future use. It’s pretty self-explanatory so we’ll cut right to the code, and come back afterwards to talk about some of the extras.

./symfony generate:plugin majaxWidgetPlugin
mkdir -p plugins/majaxWidgetPlugin/web/js
mkdir -p plugins/majaxWidgetPlugin/lib/widget

We made our plugin, then we made the directory our jQuery plugins will go in, and the directory where we will store our widgets. The -p flag means “no error if existing, make parent directories as needed” which saved us a few mkdir commands.

3. Making our symfony widget

Now open up plugins/majaxWidgetPlugin/lib/widget/majaxWidgetFormDate.class.php and here’s our code:

<?php
 
class majaxWidgetFormDate extends sfWidgetFormDate
{
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $sfContext = sfContext::getInstance();
    $resp = $sfContext->getResponse();
    $resp->addJavascript('/majaxWidgetsPlugin/js/jquery.majax.dateselector.js');
 
    $id = $this->generateId($name);
 
    $txt = ($this->getOption('can_be_empty') == true) ? 'true' : 'false';
 
    $display = '<div id="'.$id.'"></div>';
    $display .= '<div id="'.$id.'_ctrls">';
    $display .= parent::render($name, $value, $attributes, $errors);
    $display .= '</div>';
    $display .= '
<script type="text/javascript">
$(function() {
  $(\'#'.$id.'\').majaxdateselector({can_be_empty: '.$txt.'});
});
</script>
';
    return $display;
  }
}

So, to start out, we extend sfWidgetFormDate, as that will make the controls we need to manage a date submission. The only change we need to do, is to override the render function, which will let us add some custom code around the standard output, to allow us to more easily control the display.

The first thing we do in the render function is to include the required JavaScript. Some will say the widget shouldn’t know about the JavaScript, but I say, if the widget shouldn’t know… who should? We want to make this as transparent as possible, so why should we have to also remember to add a JavaScript include to use this widget?

Next, we grab our generated ID, and figure out our options (i.e. can the date be null) that we can pass along to Datepicker. After that we wrap the whole deal in a containing div, and then wrap the native controls in another specific div. You will see why we did this in a minute. Lastly, we write the JavaScript to activate the widget.

4. Making our jQuery Plugin

As is the standard, I’ll show you the code, then highlight portions that are interesting. This file goes in plugins/majaxWidgetPlugin/web/js/jquery.majax.dateselector.js

(function($) {
        $.widget('ui.majaxdateselector', {
                version: '1.0.0',
                eventPrefix: 'majax.dateselector',
                options: {
                        can_be_empty: false,
                        datepicker_opts: {
 
                        }
                },
                _create: function() {
                        this.options['id'] = $(this.element).attr('id');
                        this._hide_real_ctrls();
                        this._build_facade();
                        return this;
                },
                _build_facade: function() {
                        $(this.element).html('<input size="10" type="text" id="'+this.options['id']+'_display" />');
                        var tfDisplayUpdate = function(widget) {
                                return function() {
                                        widget._update_ctrls(this.value);
                                }
                        }
 
                        $('#'+this.options['id']+'_display').change(tfDisplayUpdate(this));
 
                        var m, d, y;
                        m = $('#'+this.options['id']+'_month').val();
                        d = $('#'+this.options['id']+'_day').val();
                        y = $('#'+this.options['id']+'_year').val();
                        if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
                        {
                                $('#'+this.options['id']+'_display').val(this._zero_pad(m, 2)+'/'+this._zero_pad(d, 2)+'/'+y);
                        }
                        $('#'+this.options['id']+'_display').datepicker(this.options['datepicker_opts']);
                        if (this.options['can_be_empty'])
                        {
                                $('#'+this.options['id']).append(' <input type="button" id="'+this.options['id']+'_empty" value="Clear" />');
                                $('#'+this.options['id']+'_empty').button();
                                var tfClear = function(widget) {
                                        return function() {
                                                widget._clear_display();
                                                return false;
                                        }
                                }
                                $('#'+this.options['id']+'_empty').click(tfClear(this));
                        }
                },
                _zero_pad: function(num,count)
                {
                        var numZeropad = num + '';
                        while(numZeropad.length < count) {
                                numZeropad = "0" + numZeropad;
                        }
                        return numZeropad;
                },
                _clear_display: function() {
                        $('#'+this.options['id']+'_display').val('');
                        $('#'+this.options['id']+'_month').val('');
                        $('#'+this.options['id']+'_day').val('');
                        $('#'+this.options['id']+'_year').val('');
                },
                _update_ctrls: function(val) {
                        var vals = val.split('/');
                        if ((val == '' || vals.length != 3) && this.options['can_be_empty'])
                        {
                                $('#'+this.options['id']+'_month').val('');
                                $('#'+this.options['id']+'_day').val('');
                                $('#'+this.options['id']+'_year').val('');
                        }
 
                        var m, d, y;
                        m = vals[0];
                        d = vals[1];
                        y = vals[2];
 
                        if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
                        {
                                $('#'+this.options['id']+'_month').val(parseInt(m));
                                $('#'+this.options['id']+'_day').val(parseInt(d));
                                $('#'+this.options['id']+'_year').val(parseInt(y));
                        }
                },
                _hide_real_ctrls: function() {
                        $('#'+this.options['id']+'_ctrls').css('display', 'none');
                },
                _show_real_ctrls: function() {
                        $('#'+this.options['id']+'_ctrls').css('display', null);
                },
                destroy: function() {
                        this._show_real_ctrls();
                        ('#'+this.options['id']).html('');
                        $.Widget.prototype.destroy.call(this);
                        return this;
                }
        });
})(jQuery);

You know, looking over, it’s pretty clear, I feel, what most parts do. The functions “_hide_real_ctrls”, and “_show_real_ctrls” hide and show that wrapping div we built around the original controls, so we can keep them in play, but not have to worry about controlling them. The “_build_facade” function builds our fake interactive Datepicker object, and optionally our ‘Clear’ button, if our date is allowed to be empty. Functions “_clear_display” and “_update_ctrls” to exactly as you would expect. Cure functions “_create” and “destroy” are from the jQuery UI Widget framework, and are called … can you guess when? 🙂

We’re done!

Once you’re done, you can replace any sfWidgetFormDate instance with a majaxWidgetFormDate instance, and everything else is handled. A little bit of effort up front, and many rewards down the road!

How to build a basic contact form in symfony

Forms in symfony bring some fun to the table. They’re very, very powerful, but you have to know how to ask them, and figuring that out isn’t always the easiest thing. I have worked with forms extensively for the past several weeks, and primed with this knowledge, I believe I can show you a pretty solid way to start building customized forms.

We’re going to have six pieces building our contact form:

  1. Setup — We make our module
  2. Preparation — I have a helpful function for you to add to your BaseForm class that makes things easier later on
  3. Form — We build our form
  4. Action — We build the action to handle our form
  5. Templates — We build two templates to handle our contact form, the form page, and the thank you page.
  6. Routing — All this hard work, let’s make it look nice!

So with that being said, let’s get on the road to enlightenment!

1. Setup

At this point, I assume the following:

  • You have a working symfony install
  • You have an app named ‘frontend’

If you haven’t made a module before, you’re about to expand your horizons!

./symfony generate:module frontend contact_form

This will create apps/frontend/modules/contact_form, and related subdirectories.

2. Preparation

I promised you a helpful addition to your BaseForm class, and along with that, I will offer an explanation.

The sfForm class (and it’s children) will allow you to easily get only hidden fields, but it doesn’t offer you a simple way to allow you to easily get only visible fields. This little function solves that inequity.

  public function getVisibleFields()
  {
    $hidden_fields = $this->getFormFieldSchema()->getHiddenFields();
    $fields = array();
    foreach($this as $name => $field)
    {
      if (!in_array($field, $hidden_fields))
        $fields[$name] = $field;
    }
    return $fields;
  }

If you have nothing else added to your BaseForm.class.php file (lib/form/BaseForm.class.php), it will look like:

<?php
 
/**
 * Base project form.
 *
 * @package    your-package
 * @subpackage form
 * @author     YourNameHere
 * @version    SVN: $Id: BaseForm.class.php 20147 2009-07-13 11:46:57Z FabianLange $
 */
class BaseForm extends sfFormSymfony
{
  public function getVisibleFields()
  {
    $hidden_fields = $this->getFormFieldSchema()->getHiddenFields();
    $fields = array();
    foreach($this as $name => $field)
    {
      if (!in_array($field, $hidden_fields))
        $fields[$name] = $field;
    }
    return $fields;
  }
}

3. Form

For our contact form, we’ll have Name, Email, and Comments. Name and Email will be input fields, and Comments will be a text area. All fields will be required. Let’s take a look at what that form looks like:

lib/form/PublicContactForm.class.php

<?php
 
class PublicContactForm extends BaseForm
{
  public function setup()
  {
    // this is required, don't forget this!
    $this->widgetSchema->setNameFormat('contact_form[%s]');
 
    $this->setWidget('name', new sfWidgetFormInput());
    $this->setValidator('name', new sfValidatorString(array('required' => true)));
 
    $this->setWidget('email', new sfWidgetFormInput());
    $this->setValidator('email', new sfValidatorEmail(array('required' => true)));
 
    $this->setWidget('comments', new sfWidgetFormTextarea());
    $this->setValidator('comments', new sfValidatorString(array('required' => true)));
 
    parent::setup();
  }
}

Why did I use Public in the name? It reminds me that it’s a public facing form, most likely using a custom-ish template, which I may need to update if I change the form much.

4. The Action

Here’s a hearty chunk of where we’re going to end up. Let’s start by taking a look at the final form of the file, and then we’ll highlight portions.

apps/frontend/modules/contact_form/action/action.class.php

<?php
 
/**
 * contact_form actions.
 *
 * @package    your-project
 * @subpackage contact_form
 * @author     Jacob Mather
 * @version    SVN: $Id: actions.class.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
 */
class contact_formActions extends sfActions
{
 /**
  * Executes index action
  *
  * @param sfRequest $request A request object
  */
  public function executeIndex(sfWebRequest $request)
  {
    $this->form = new PublicContactForm();
    if ($request->hasParameter('contact_form'))
    {
      // we're getting the parameters for the form, let's bind and see what happens
      $this->form->bind($request->getParameter('contact_form'));
 
      if ($this->form->isValid())
      {
        $values = $this->form->getValues();
        $text_msg = 'You have received a message!'."\r\n\r\n";
        $html_msg = 'You have received a message!<br /><br />';
        foreach($values as $name => $value)
        {
          $text_msg .= $name.':'."\r\n".$value."\r\n\r\n";
          $html_msg .= $name.':<br />'.$value.'<br /><br />';
        }
        $text_msg .= 'Thanks!';
        $html_msg .= 'Thanks!';
 
        $from = array('my-app@example.com' => 'My Application');
        $to = array('replace-with-your-email@example.com');
 
        $message = $this->getMailer()->compose($from, $to, 'Contact Form Submission');
        $message->setBody($html_msg, 'text/html');
        $message->addPart($text_msg, 'text/plain');
        $this->getMailer()->send($message);
 
        $this->getResponse()->setTitle('Thanks for contacting me');
        return 'Thanks';
      }
    }
    $this->getResponse()->setTitle('Contact Me');
    return 'Form';
  }
}

I think you’ll find the form pretty self explanatory, though I’ll cover a couple things.

The first thing to point out, I didn’t return sfView::SUCCESS, or sfView::ERROR. Why not? Simple. I wanted to keep the names of the files within the context of what they’re doing. We have two views we will use, the form, and the thank you page, so we will use two templates, indexForm.php, and indexThanks.php. I think that keeps things pretty obvious, how about you?

The next thing to point out, is we’re just using a single action! Why? Well, why do we need more than one action? We need the form object to display an error, and we can easily detect if someone has submitted the form (that is what $request->hasParameter(‘contact_form’) does), so why not just roll it into one compact bit.

Lastly, yes, I put a lot of effort into the outgoing emails. I always send an HTML and a plaintext part. It’s my personal preference, but it takes so little effort to deliver a professional solution even in a simple example, it feels sloppy not to.

5. The Templates

As I mentioned in the last section, we need two templates. indexForm.php and indexThanks.php.

apps/frontend/modules/contact_form/templates/indexForm.php

<style>
  .contact-form li { list-style: none; margin-bottom: 10px; }
  .error_list { padding: 0px; }
  .error_list li { background-color: red; color: white; font-weight: bold; padding: 3px;}
</style>
<?php echo $form->renderFormTag(url_for('contact_form/index')); ?>
<?php echo $form->renderGlobalErrors(); ?>
<?php echo $form->renderHiddenFields(); ?>
<ul class="contact-form form">
<?php foreach($form->getVisibleFields() as $name => $field): ?>
  <li>
    <?php echo $field->renderError(); ?>
    <?php echo $field->renderLabel(); ?>
    <div class="value"><?php echo $field->render(); ?>
    <span class="help"><?php echo $field->renderHelp(); ?></span></div>
  </li>
<?php endforeach; ?>
  <li><input type="submit" value="Submit Contact Form" /></li>
</ul>
</form>

apps/frontend/modules/contact_form/templates/indexThanks.php

<p>Thanks for contacting me!</p>

As you can see, it’s a pretty simple setup. I did some inline styles for the form, though in a larger project you would want those to be in a stylesheet include. The indexForm.php should serve you well as a base for further forms, as it includes support for a few things we don’t use in this form (help, global errors), which should help you make things ‘just work’ in the future.

6. Routing

Ok, so we’ve made everything, set it up, made it look visually nice enough to not want to stab our eyes out too much, now it’s time to put the icing on the cake: the route.

This is real simple. Open apps/frontend/config/routing.yml

contact_form:
  url: /contact-me
  param: { module: contact_form, action: index }

And you’re all set! Clear your cache, and you’re ready to go:

./symfony cc

Now navigate to your /contact-me page, and you will see your contact form!

Let’s try something a little new, and show a screencast of the finished product! Let me know how you like it!

twitter