02.02.09

Can the Symfony Forms Framework be Domesticated? A Simple Todo List

Posted by ryan in php, symfony


Please stop crapping on my carpet

graywolf.jpg
you can trust me...
Granted, it's an old article (completed before Symfony 1.1 was beta), but this piece of work (yes, written by me) is a great example of a simple problem with an embarrassingly complex solution. A lot has changed since then, but I still routinely find myself in situations where the form framework feels like trying to use a bear trap to catch a mouse: in many cases, I just end up hurting myself. Like a puppy that craps on your carpet before you train him, so to has the framework crapped on me.

The modest task that is our charge

Today we're going to create a very simple todo list. I've purposefully left out all the bells and whistles. I need to crawl before I walk. Notice that I always give you 3 boxes for new todo items. I choose that arbitrarily and to keep things simple.

Our simple todo list
todo-forms-simple.gif

The Very Simple Setup

Simple: our module is called "todo" and we have the following schema. All of this code is common to almost any situation.

schema.yml:

propel:
  todo:
    _attributes:    { phpName: Todo }
    id:
    task:           varchar(255)
actions.class.php

class todoActions extends sfActions
{
  public function executeSimple(sfWebRequest $request)
  {
    $this->form = new simpleTodoForm();
  }
  
  public function executeSimpleProcess(sfWebRequest $request)
  {
    $this->form = new simpleTodoForm();
    
    if($request->isMethod('post'))
    {
      $this->form->bind($request->getParameter('todo_list'));
      if($this->form->isValid())
      {
        $this->form->save();
        $this->redirect('todo/index');
      }
    }
    
    $this->setTemplate('simple');
  }
}
TodoForm.class.php

class TodoForm extends BaseTodoForm
{
  public function configure()
  {
    unset($this['id']);
  }
}

Background - Symfony's embedForm method

Let me talk for a moment about Symfony's mysterious, powerful and aggravating embedForm method. As it sounds, the embedForm method allows you to essentially combine many forms into one form. More accurately, the embedForm method creates a hierarchy inside your form that functions almost like a multi-level array. It does this all very nicely, but causes some confusion along the way.

The Forms

As you can see in the above code, we are using a form called simpleTodoForm. Create this form from scratch (simpleTodoForm.class.php) and place it somewhere appropriate (e.g., create a form directory inside your application's lib folder and place it in there). Here's where we start

simpleTodoForm.class.php
  
class simpleTodoForm extends sfForm
{
  public function configure()
  {    
    $todoWrapperForm = new sfForm();
    foreach(TodoPeer::doSelect(new Criteria()) as $todo)
    {
      $todoWrapperForm->embedForm($todo->getId(), new TodoForm($todo));
    }
    // add 3 blank todos (the #3 was chosen arbitrarily)
    $todoWrapperForm->embedForm('new_1', new TodoForm());
    $todoWrapperForm->embedForm('new_2', new TodoForm());
    $todoWrapperForm->embedForm('new_3', new TodoForm());
    
    $this->embedForm('todos', $todoWrapperForm);
    
    $this->widgetSchema->setNameFormat('todo_list[%s]');
  }
}

Visualizing our Forms

On the surface: Our form displays multiple "todo" text boxes
Under the hood: Our form embeds multiple TodoForm forms

(left) Our 3 levels of forms      (right) The output of those forms
20090202-simple-todo-form-organization.gif

Back to the code

Keeping in mind the above model, the configure() code above makes more sense. First, I create a new sfForm called $todoWrapperForm. This intermediate form is here only for organization and serves no other purpose. I could have embedded my TodoForm forms directly to simpleTodoForm, but (as you'll see) my method makes life easier down the road.

One more quick comment. The first argument in the embedForm method is the name to give the form, and is used in the actual name attribute of the resulting form elements. In this case, the names don't matter too much - they'll just be used as keys later to retrieve the data and save the Todo objects. To prevent any weirdness (like if concurrent users were adding/removing todos), I use the primary key of the existing Todo items as their names. For the new todo items, I simply give them the arbitrarily chosen names new_1, new_2, and new_3. You'll see later how I use this to save our todo items.

The View

<form action="<?php echo url_for('todo/simpleProcess'); ?>" method="post">
  <?php echo $form->renderGlobalErrors(); ?>
    
  <?php foreach($form['todos'] as $fieldSchema): ?>
    <?php echo $fieldSchema['task']->render(); ?>
    <?php echo $fieldSchema['task']->renderError(); ?>
  <?php endforeach; ?>
    
  <?php echo $form['_csrf_token']; ?>
  <input type="submit" value="save" />
</form>

This rings of the simplicity that I've been looking for. With the exception of the foreach statement in the middle, most forms will have these essential parts.

Remember that our embedded forms work like a multilayered array. In fact, our form functions exactly like the array I've constructed below. Notice how each of our 3 forms is represented as an array here:

array(
  '_csrf_token' => new sfWidgetInput(...),
  'todos' => array(
    'new_1' => array(
      'task' => new sfWidgetInput(...),
    ),
    'new_2' => array(
      'task' => new sfWidgetInput(...),
    ),
    'new_3' => array(
      'task' => new sfWidgetInput(...),
    ),
  ),
)

It's with this in mind that we render our form in the manner above. By creating a foreach loop through the $form['todos'] elements, we're actually going through each of our embedded TodoForm forms one by one. We then output the elements of our TodoForm form just like we normally would if it were a standalone form. In this (unique) case, our TodoForm forms only have one widget, called task. So, all we have to do is output our task widget. I omitted the common renderRow() command here, instead using render() and renderError(). I did this simply for aesthetics: I didn't want to use the element's label tag in this case.

Saving all the objects

If you were to comment out the $this->form->save() call in the simpleProcess action, you'd find that you have a multilayered form that binds and validates as you'd expect. In fact, it should do everything except actually save your new Todo objects. Add this:

class simpleTodoForm extends sfForm
{  
  ...
  public function save()
  {
    $values = $this->getValues();
    
    foreach($this->embeddedForms['todos']->getEmbeddedForms() as $key=>$todoForm)
    {
      if ($values['todos'][$key]['task'])
      {
        // only save todos that aren't blank
        $todoForm->updateObject($values['todos'][$key]);
        $todoForm->getObject()->save();
      } elseif(!$todoForm->getObject()->isNew()) {
        // delete any existing todos that are now blank
        $todoForm->getObject()->delete();
      }
    }
  }
}

The world is 2 worlds

We've entered the twilight zone of saving embedded forms. Although you've probably never realized the division, here are the 2 very separate worlds of embedded forms:

  1. Parent form: Propel/Doctrine form
    Embedded forms: Propel/Doctrine forms related to the parent form by some foreign key
    To save: Simply call the parent's save() method, which automatically saves all the embedded forms
  2. Parent form: some normal sfForm form that holds everything together
    Embedded forms: A group of Propel/Doctrine forms (e.g., you're adding many Todo objects at once)
    To save: Since the parent form has no save() method, you must cycle through all the subforms, update their Propel/Doctrine objects, and save those objects

Obviously, we fall into world #2. The foreach statement is designed to cycle through each of our original TodoForm forms in very much the same way that we used a foreach statement to cycle through them in our view. The difference in those foreach statements is due to some underlying mechanics of the embedForm method. If you're interested, keep reading below.

Each $todoForm in our foreach must first have its object updated (updateObject()) then saved (getObject()->save()). These two statements have really nothing to do with the Forms world: they simply update each Todo object with the input data. The extra if/then statement used here is just to make things work a little better: I don't want to save new Todo objects if their task is blank and I want to delete any existing Todo items whose task has been deleted (made blank).

Summary

Creating a simple todo form really can be simple once you get the hang of it. Forms, which act just like arrays, can be used to help organize your overall form through the embedForm method. The most confusing thing about embedForm is exactly how to then output your embedded forms and save them. Using the above as a guide, you should be in good shape. If you ever get confused, remember to view your forms as an array - they truly function in this way.

One stumbling block is the fact that, to access your embedded forms, you must use a different foreach statement in your view than in the form itself. Specifically, when you need to access an embedded form's widgets (for the purpose of outputting them), you should use the method used in the view. However, when you need to access the embedded form object itself, you should use the method seen in the form's save() method. There's a very good reason for this, but it confused the hell out of me. If you want to learn me, see my explanation below.

There's black magic in those forms

Behind the scenes of this embedForm business, some black magic is hiding, and it's a bit difficult to explain. The underlying weirdness, as eluded to in the above paragraph, is that you must use a different foreach statement depending on whether you want to get at the embedded form's widgets or the form object itself. Another place of weirdness is why I used updateObject() and getObject()->save() to save my embedded forms instead of using their prebuilt save() methods. What gives?

What really happens when you embed a form

The explanation lies in what really happens when you embed a form. To be straightforward, if you use embedForm to embed form B into form A, the Widget schema and Validator schema of form B are copied into form A. That's it. The embedded form itself (form B) isn't actually used. Instead, the widgets and validators for form B become part of form A. Most importantly, when data is bound to form A, that data is passed through to all its validators, which now include the validators originally from form B. Very importantly, however, is the fact that form B itself is never bound to the data. As far as validation and errors are concerned, everything works perfectly. Since all of form B's validators have become part of form A, they all throw their validation errors correctly. The added complication occurs when you try to save the embedded form B.

Let's go back to our save() method and propose a new piece of (incorrect) code:

// Original code to save the embedded Todo forms:
  $todoForm->updateObject($values['todos'][$key]);
  $todoForm->getObject()->save();

// My incorrect what-if proposed code to save the embedded Todo forms:
  $todoForm->save()

Remember that the $todoForm was embedded into our simpleTodoForm form (through an intermediate form - there were 3 layers in total). That means that its widgets and validators became a part of simpleTodoForm. It also means that $todoForm was never actually bound. So, if you try to save $todoForm, you'll get a nasty error because that form hasn't actually been bound with the input data. There is a disconnect between embedded form objects and the data bound to their parent form. Because of this fact, we must update then save the embedded form's object.

Some of you my be wondering, as I did, if there is a security concern with simply updating then saving the objects on these unbound, embedded forms. Fear not. Even though the form objects themselves are unbound, the forms' validators have still been passed the input data and thrown validation errors as necessary (because a bound form's validators become part of the main form).

Going back to my 2 worlds of embedded forms above, some of you may be wondering how Symfony handles everything so smoothly in world #1. The answer: Symfony uses the exact method I propose above for world #2 - it just does it all behind the scenes.

Final Words

Can the Symfony forms framework be domesticated and become helpful in 100% of the situations? I answer a resounding yes. To begin, it HAS to work - the old system is terribly inferior to this one, even if this system has some learning curves. I hope that this article, despite its complex details, will show that the forms framework can approach these types of situations without choking. Like any great system, we've just gotta figure out how to use it right.

Happy embedding, good luck and good night.

Attachments

Simple and Advanced Todo Module (todo.tar.gz)
Thanks for the shares!
  • StumbleUpon
  • Sphinn
  • del.icio.us
  • Facebook
  • TwitThis
  • Google
  • Reddit
  • Digg
  • MisterWong
Posted by David Arthur on 2009-02-04
Well written article, ive been hit with this confusion myself but never took the time to write about it. You've done an excellent job! I'll pass this on to anyone i see in need of some clarification regarding embeding forms!
David
Posted by Nei Rauni Santos on 2009-02-20
How can i validate a group of embedded forms?

by example:

I have a system of todo.

each item has a time to do.

How can validate to not allow that a sum of all times < 12 hours??

Thanks.
Posted by Ryan on 2009-02-20
@Nei - You'll need to use a custom post validator. If each embedded form represents one todo item (as it does in my example), then you'll need to add a post validator to your main form. Below, I'm assuming I've added a widget to my TodoForm called 'hours':

// simpleTodoForm::configure
$this->validatorSchema->setPostValidator(new sfValidatorCallback(array('callback'=>array($this, 'checkHours'))))


// simpleTodoForm add callback validator
public function checkHours($validator, $values, $args)
{
$totalHours = 0;
foreach($values['todos']['hours'] as $hours)
{
$totalHours += $hours;
}

if ($hours>12)
{
throw new sfValidatorError($validator, 'invalid');
}

return $values;
}

Try that out - it may not be perfect, but should work. If it doesn't work at all, I'd like to know.

-Ryan
Posted by JoeZ99 on 2009-02-25
Ryan.
You're my hero. astonishing simple and beautiful.

Posted by Arnold on 2009-04-09
Hello,

I tried to "domesticate" the forms, but it seems that I have failed. :(
Excellently written article by the way, but i was wondering if you could help me with an issue i encounter with embedded forms and file upload.

I have posted on the google group (http://groups.google.com/group/symfony-users/browse_thread/thread/772396cf0f4971be), but no answer yes.

Your help would be appreciated a lot.
Thannk you
Posted by Ryan on 2009-04-09
@Arnold - Check out my response, I think you just left some code commented that you didn't mean to - you're in good shape.
Posted by Arnold on 2009-04-09
Hello, Thank you for your response, the sfValidatorString was a typo, replaced it but i still have this error: Fatal error: Call to a member function getExtension() on a non-object in /home/ispa/workspace/timnews/lib/form/GeneralImageForm.class.php on line 41 because instead of receiving an sfValidatedFile the updateImagefileColumn receives an array :(

array(5) {
["name"]=>
string(32) "article_big_1-1235590741.jpg"
["type"]=>
string(10) "image/jpeg"
["tmp_name"]=>
string(14) "/tmp/phpvnybEb"
["error"]=>
int(0)
["size"]=>
int(267713)
}
Posted by Alex on 2009-04-10
Great article.

I am having an issue saving data with my own embedded forms. My setup is the same for your article - a main sfForm containing many embedded Propel forms.

The form functions properly until I go to submit the data. It successfully passes validation and then goes through the save() loop but only one record gets inserted into the database. Looking at the logfile shows that for all other iterations of the save() loop generate an UPDATE query instead of an INSERT.

Any ideas that can point me in the right direction? I am using symfony 1.2.5 .
Posted by Ryan on 2009-04-10
@Alex

Well that's a new problem. My guess is that your update statements all use the primary key from the one successful insert statement. If that's true, it would lead me to believe that your embedded forms are all, somehow, sharing the same Propel object (so the first save inserts the object, but only udpates after that). This would probably be the symptom also if you were embedding the same new form instead of separate new forms:

public function configure()
{
...
// this is bad
$new_form = new myForm();
$this->embedForm('new_1', $new_form);
$this->embedForm('new_1', $new_form);
...
}

That's all I can say - haven't seen this problem yet!
Posted by Alex on 2009-04-10
I sent an email about a secondary problem I was having about passing a variable to the configure() function of the form.

As for the embeded forms, they are all unique - generated by an embedFormForEach() which is creates the embeded forms based off the id numbers for the cell they make up. When I do a var_dump of '$values['cavities'][$key] the data is properly looped through. The first record is the only one inserted. All the other update queries are not even writing over the initial insert, but trying to update records that do not exist - according to the logfile, they are using the correct data, but are the incorrect query type. Very frustrating, heh.
Posted by xdreucker on 2009-07-05
Can you help me figure out what to do? Why is it that the fields still displayed on the embeded form, I already unset some of the fields on that embeded form configure method.

Thanks
Posted by Ryan on 2009-07-05
@xdreucker - The fields should only display if you actually output them in your template. What does your template file look like? Shoot me an email.

-Ryan
Posted by Symbiat on 2009-07-20
Say I had an arbritrary number of embedded forms (could be 0, could be 7 but they are from a query). How do I test for those in my template and only output ones that exist?
Posted by Eleazar on 2009-08-12
Thank you for this wonderful tutorial! I have one problem and wonder if you could help me. There are some required fields in my embedded forms and instead of deleting empty form, it throws an required error. Do you know how can I solve this? I made only one change in your code - what you have in save() method, I've put to the saveEmbeddedForms().
Posted by Ryan on 2009-08-12
@Eleazar - unless I'm reading your problem incorrectly, you should just be able to change the 'required' option on the offending fields from inside the embedded form class itself. Lemme know if I've got your situation all wrong though!
Posted by Eleazar on 2009-08-14
Thanks for your concern. I turned off 'required' option and I'm checking it with JS. Not the best solution.
I think you should try to figure this out - if the embedded form is emty (or some predefined fields - required in my case), it will be deleted, if not, validate it. I hope it's not too confusing :-D
Posted by Rob Graham on 2010-02-24
Finally, I've found someone else who has spotted this! I've just started really delving into the forms of 1.1 and I'm horrified, I don't think they are going to work for me. The implementation of embedded forms and the way it binds and them makes any overriding of bind or isValid etc in your subform completely useless!

I wanted to have a couple of embedded forms held by a wrapper and that wrapper form would be considered valid if EITHER of it's subforms were valid (i.e. this wrapper was going to be a composite type pattern). That would involve me overriding the isValid on the wrapper form to return true if either of it's subforms isValid methods returned true. But as you say, due to the fact the subforms are essentially discarded it renders the whole thing useless. I may be able to write the subforms as widget schema's instead, but like you say it's harder than it should be. Still looks to be the same in symfony 1.4 too. Grrrrrr
Posted by Javi on 2010-03-03
Thanks God someone explain what the heck is going on under the hood in embeddedForms. Thanks a million!!!
Posted by kaore on 2010-04-15
I prepared this post on the same subject. Let me know what you think and thanks for your post
Posted by London on 2010-06-17
It really depends on your definition of 'domesticated'. Symfony forms involve a lot of overhead. You cant create a 'simple' form in symfony - even the most basic form involves making classes.
Posted by jaycreation on 2010-06-29
J'ai beau faire j'ai toujour du mal avec les formulaire imbriqués... J'espere que Symfony va améliorer ça !
Posted by 894 on 2011-03-21


We are a professional manufacturer specializing in clean room products: about airborne particle
counter named Dust
counter, Biological air sampler named Bacteria tester, horizontal & vertical laminar flow bench, FFU, Filters,Air shower, Pass box as well as some other
relative meters such asAnemometer, hood,Digital Sound level meter, Humidity/temperature meter, Differential pressure meter and Luxmeterand so on.