Please stop crapping on my carpet
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
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
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:
-
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 -
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.










David
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.
// 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
You're my hero. astonishing simple and beautiful.
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
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)
}
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 .
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!
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.
Thanks
-Ryan
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
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
Leave a reply