02.13.09

Embedding Child forms with sfFormPropel - A Practical Example

Posted by ryan in php, symfony


As it turns out, when it comes to creating rich Symfony forms using embedForm, the solution depends greatly on the answer to this question: Does your main form extend sfFormPropel or sfForm? In two previous articles, I created a simple todo list and extended it to make it more useful. In those articles, my main form extended sfForm. Today, I'm going to cover a similar (and probably more useful) example where my main form extends sfFormPropel.

The Goal

Our goal is to create a simple interface to manage Author information and their Publications. As you may have guessed, each Author can have many Publications. These will be our Propel objects. Similar to my advanced todo list, we'll be adding AJAX to make the interface more user-friendly.

Play around with our Author-Publication Form
author-form.gif

The setup

Simple. Our schema is a parent "Author" model and a child "Publication" model. I won't display the actions and template code here, because they're almost identical to my previous article. The point I want to make here is how embedding forms into sfFormPropel differs from sfForm. You can download the full source to this project at the bottom of the article.

//schema.yml
propel:
  author:
    _attributes:   { phpName: Author }
    id:
    first_name:    varchar(255)
    last_name:     varchar(255)
  publication:
    _attributes:   { phpName: Publication }
    id:
    author_id:     { type: integer, foreignTable: author, foreignReference: id, onDelete: cascade, onUpdate: cascade, required: true }
    title:         varchar(255)
    year:          integer

The Meat: The Forms

Our main form (the one we actually render in the template and initialize in the actions) is called myAuthorForm and it extends the propel-generated AuthorForm. This form is configured below. Notice that there is no real difference between the configuration here and the one used in my previous todo list article.

class myAuthorForm extends AuthorForm
{
  
  public function configure()
  {
    parent::configure();
    unset($this['id']);
    
    $this->validatorSchema['first_name']->setOption('required', true);
    $this->validatorSchema['last_name']->setOption('required', true);
    
    $publicationsForm = new sfForm();
    foreach($this->getObject()->getPublications() as $publication)
    {
      $publicationsForm->embedForm('publication_'.$publication->getId(), new myPublicationForm($publication));
    }
    // add one blank publication form
    $publicationsForm->embedForm('new_1', new myPublicationForm());
    // embed the publication form
    $this->embedForm('publications', $publicationsForm);
    
    $this->widgetSchema->setNameFormat('author[%s]');
  }
  ...
}
class myPublicationForm extends PublicationForm
{
  public function configure()
  {
    parent::configure();
    
    unset(
      $this['author_id'],
      $this['id']
    );
  }
}

Your Main Form: sfFormPropel vs sfForm

Here it is:: sfFormPropel has the ability to automatically save the embedded forms, while sfForm does not. In this example, our main form (myAuthorForm) extends sfFormPropel, which actually makes our job a bit easier. Specifically, with sfForm we must add a save() method to manually save our underlying embedded forms. With sfPropelForm, we don't have to do anything - the save method is built in.
Saving embedded forms:

  • sfForm Manually add a save() method
  • sfFormPropel Do nothing, the save() method is built in

One little detail - making sure the publications have the right author

As I just explained, if your main form is an sfFormPropel form, then you need not do any extra work to save your embedded forms: it happens automatically.

However, in its current form, the Publication objects are being saved without an author_id (actually, an error is being thrown since that field is required in my schema). Somehow, we need to be able to set the author_id field of our publications before they are saved (but AFTER the author is saved, to ensure that it has an id). Here's the trick:

  /**
   * Update the author_id of the publications after the author has been saved
   */
  public function saveEmbeddedForms($con = null, $forms = null)
  {    
    foreach($this->embeddedForms['publications']->getEmbeddedForms() as $publicationForm)
    {
      if (!$publicationForm->getObject()->getAuthorId())
      {
        $publicationForm->getObject()->setAuthorId($this->getObject()->getId());
      }
    }
    
    parent::saveEmbeddedForms($con, $forms);
  }

The saveEmbeddedForms method is an sfFormPropel method that is called AFTER the object of the main form has been saved. It's very useful in this case because this guarantees that the author object has an id to use here.

Will I ever stop writing Symfony form articles???

Another post about the Symfony form framework: even I'm feeling a little exhausted. Still, through the expansion of the framework and books, blogs, and other articles, I'm now able to use the framework to easily do some pretty flexible and innovative tasks. So, I feel like we're getting somewhere.

But what else is there? What other problems and solutions have you run into? If the framework can't be used in all the most flexible ways, then it has failed. So far, it's doing pretty well imo.

Attachments

Author & Publication Symfony Example (author-publication.tar.gz)
Thanks for the shares!
  • StumbleUpon
  • Sphinn
  • del.icio.us
  • Facebook
  • TwitThis
  • Google
  • Reddit
  • Digg
  • MisterWong
Posted by Lawrence Krubner on 2009-02-15
I am confused by this line in the configure() method:

unset($this['id']);

What bad thing would happen if that line was not there?
Posted by Fredlab on 2009-02-16
What Else ? Internalization of forms with the admin generator. The extract task does not work. How could we extend it to include the extraction of "label" from the cache ?
Posted by Ryan on 2009-02-16
Lawrence - Great Question - I pondered heartily for awhile Here are my thoughts.

Without the unset($this['id']) in myAuthorForm::configure(), nothing bad would happen. But, you'd then have a redundancy. Specifically, your myAuthorForm is constructed with an Author object that was initialized via the id parameter sent in the url. If you included the id as a hidden field, the id field of the object would then be re-set when you bound the form. You'd really be setting the id on an object twice.

It could also open up a security hole. Assume that only certain users have rights to modify certain Authors. When you initialize your Author from the url parameter, you check to make sure that the current user has access to modify that Author. This would open up the possibility to add a publication to an author that you don't have access to modify (by changing the id of the hidden id field).
Posted by Attacker on 2009-03-01
Thanks for your sharing ``` but I did all like the examples and get the error like this:
"Warning: sfAutoload::require(C:/www/piano/apps/frontend/modules/piano/lib/myPianoForm.class.php) [function.sfAutoload-require]: failed to open stream: No such file or directory in C:\xampp\php\PEAR\symfony\autoload\sfAutoload.class.php on line 165

Fatal error: sfAutoload::require() [function.require]: Failed opening required 'C:/www/piano/apps/frontend/modules/piano/lib/myPianoForm.class.php' (include_path='C:\xampp\php\PEAR\symfony\plugins\sfPropelPlugin\lib\vendor;C:\www\piano;C:\xampp\php\PEAR\symfony;.;C:\xampp\php\pear\') in C:\xampp\php\PEAR\symfony\autoload\sfAutoload.class.php on line 165",and what's the problem with me `?Hope your reply```Regards~~~
Posted by Ryan on 2009-03-02
Most likely you need to clear you cache. It looks like you may have moved around some of your files, and the cache is trying to autoload a file that doesn't exist.

-Ryan
Posted by Attacker on 2009-03-02
Hi``Ryan```
I did all like the example,but it didn't work still```There seems to be some problems,
1:the bind() method in myXXXForm.
2:the ajax part,when I click the "Add another pianopicture" link,there wasn't any change on my page but added a '#' in the url```
I worked with this problem several days and not find any solution s for this ```Please help ``Be appreciate```
Posted by Denis on 2009-03-03
Hi! I just wanted to thank you for a nice article :)
Posted by DiSalvoTech on 2009-03-06
Thanks for the article Ryan. I'm searching for an easy way to embed forms. Have not found one yet. I created a test app using your supplied files and it works like dream. I'm trying to embed the sfGuardUserProfile form in the sfGuardUserForm. Not having much success with it. Symfony 1.2 is poorly supported as far as documentation and examples for embedding forms.
Posted by Sergey Rogin on 2009-03-07

abstract class sfFormPropel extends sfForm
{
//....
protected function doSave($con = null)
{
//....
//action 1 !!!!!! All is true
$this->object->save($con);

//action 2 !!!!!! All is true
$this->saveEmbeddedForms($con);
}


public function saveEmbeddedForms($con = null, $forms = null)
{
//....
if ($form instanceof sfFormPropel)
{
//ERROR !!!!!!!!! This action should be carried out after $form->getObject()->save($con);
$form->saveEmbeddedForms($con);
$form->getObject()->save($con);

}
//....


if fix this bug, that the problem of use sfFormPropel will disappear.
Posted by Ryan on 2009-03-08
@DiSalvoTech - you're very welcome. In case you haven't checked it out (or for anybody else here), the still-unfinished-yet-very-informative Symfony Forms book is a great resource (http://www.symfony-project.org/book/forms/1_2/). I don't think they yet cover embedding forms, but it's still a strong resource.
Posted by Ryan on 2009-03-08
@Sergey Rogin - I'm not sure what you're getting at, but I do see that you've highlighted something strange. The base object saves its object first, and then its embedded forms. This makes sense since you may have embedded forms that rely on the saved id of the main object (foreign keys). Why then does the reverse happen inside the saveEmbeddedForms method? I'm not sure. What were you getting at exactly with this Sergey Rogin? Have you had trouble with this exact situation?
Posted by Sergey Rogin on 2009-03-09
Ryan, this bug is only in sfFormPropel.class.php.
Look a method saveEmbeddedForms in sfFormDoctrine.class.php.
Probably in Doctrine for this bug have corrected, and in Propel have forgotten.

I use three embedded forms.
The order contains some goods, and the goods contain some option.
As forms in symfony are very heavy, I consider, that it is better to not use sfForm as the container for sfPropelForm, and to write at once

PropelForm->embeddedForm ('p' . $ i, PropelForm($object))

I have made this conclusion having seen the list of unnecessary calls saveEmbeddedForms in symfony stack trace at display of page with the form.
Posted by Ryan on 2009-03-09
@Sergey Rogin - Some people are reporting related issues, but I can't find anyone who's laid it out this clearly.

http://trac.symfony-project.org/ticket/5867

The above addresses the issue, but not exactly (they're additionally concerned with the fact that the embedded forms themselves aren't saved via $form->save()).

Regarding NOT using sfForm as a container for embedded forms, I wonder exactly how heavy sfForm is. How big of a performance difference does it actually make? I like using the container because it helps organize things - I haven't looked at it from a performance standpoint.
Posted by malas on 2009-03-09
i have used your article to make Item 1->N Image relationship.

dynamic new Image addition works perfectly well, however old Images editing does not work. It looks like the main Item form loses the embedded Image forms it had before..

Any comments on where to search for the problem would be more than appreciated

I used the source you attached to this article.
Posted by Ryan on 2009-03-25
@malas - hmm, I have no idea. Obviously, you'd need to modify the bind method slightly to handle files (using the taintedFiles array), but that shouldn't make a difference between new and existing objects. From here, I'd have to see the code to say for sure.
Posted by ChrisM on 2009-04-07
Hi Ryan,

Very nice article! Do you have any knowledge how to mange n:m relationships which have additional fields in the mapping table with this method? Embedding the forms kinda works, but only with keys that are already existant (and not necessarily linked to the embedding object)...
Posted by Clive on 2009-04-16
You know what guys this is awesome
Posted by JimD on 2009-04-20
When I try to implement this app on my system, the publication form is being rendered escaped. Symfony rev: 1.2.4, php rev: 5.2.8.

Any ideas what might be causing this?


Posted by Ryan on 2009-04-20
@JimD

Huh, not sure what you mean exactly - the elements themselves are being output as escaped? In general, output escaping is handled in settings.yml with the keys escaping_strategy and escaping_method. Even still, I don't think that should be causing you problems...

-Ryan
Posted by JimD on 2009-04-21
Sorry I should have been more clear. Yes the form elements themselves are being output as escaped.

For example, rather than seeing the text element for the publication title, I see:


This occurs only for the publications part of the form. The author part of the form is OK.

When creating the app, I used the --escaping-strategy=on option, which I do all the time. This is my first attempt at doing anything with embedded forms, although I did not think that would make a difference.

The following is the content of the settings.yml re escaping:
escaping_strategy: 'on'
escaping_method: ESC_SPECIALCHARS

I downloaded and used your attachment. The only change I made was replacing
Posted by JimD on 2009-04-21
Let's try a bit of that last post again.

For example, rather than seeing the text element for the publication title, I see:
<input type="text" name="author[publications][new_1][title]" id="author_publications_new_1_title" />

On the above line I pasted the page source rather than what was displayed in the browser which I pasted in the previous post and did not get displayed. Since there is no preview, I'm not sure how this will render

Thanks

Jim

Posted by Ryan on 2009-04-21
@JimD

Sorry about the no preview - I have limited time, but the commenting on here definitely needs work.

The escaped form doesn't make a lot of sense. I've modified my settings.yml to match yours - still no problem. Assuming you're using the renderRow method on the embedded form fields like I do in the code, does the rest of your form row render correctly (i.e. the label & the help)? Are you using a non-default form-formatter? I'm just trying to think through some things.

-Ryan
Posted by JimD on 2009-04-21
The escaped form makes no sense to me either. None of the form renders correctly.

I generated the app as described above, created a module using the options:
--with-show
--non-verbose-templates

I dropped in the files from your archive and the only change I made was replacing
Posted by JimD on 2009-04-21
Sorry - I just noticed something else I typed in that got filtered.

The end of the previous post I meant to say the only change I made was replacing the php echo short form tag with the full tag.

Jim
Posted by Ryan on 2009-04-21
Hmm, I'm afraid I can't help then - this doesn't make sense to me. It may very well be a legitimate issue that my code should address - but I just can't recreate the problem on my end. Has anybody else had problems with any part of their form improperly escaping?
Posted by JimD on 2009-04-21
I found a solution that was posted in the symfony forum late last night for someone having a similar problem. I replaced the contents of the _publication_field partial with the php command:
echo htmlspecialchars_decode($formField)

I understand what this is doing. What I don't understand is where the escaping is taking place and why it is taking place on my system and not Ryan's.

Oh well, at least I can play with form embedding which was the point in the first place.

Thanks

Jim
Posted by James on 2009-04-25
Great post Ryan! How would you go about making a delete link for each publication? Thanks,

James
Posted by germana on 2009-05-06
Hi!!!

Im trying to follow this to embed my forms.. but i just got this error:

Call to undefined method BaseDenuncia::getDenunciado

When i try to embed my Denunciado Form into Denuncia Form....

Can you help me??
Posted by Ryan on 2009-05-09
@James - It's a good idea and a commonly asked question, but I haven't done it yet. It should be pretty straightforward though - just an ajax link to an action that deletes the particular publication. You shouldn't even need to return any new ajax content - just remove the old content that represented the deleted publication. If anybody actually follows through with this, I'd be happy to post the code here.

@germana - I sent you an email. Looks like a problem with your schema & foreign keys. Also, if you're using Doctrine, that could be the culprit as well.
Posted by german on 2009-05-19
Hi!! i did not receive your message maybe it lost in my spam :S---

But i have a more explicit question, Can i, for example, embed the author form into the publications form ??? i mean, the opposite of your example...
Posted by Tom Haskins-Vaughan on 2009-05-21
Hi Ryan,

Just a quickie: I see you embed the publications in to a publications form then embed that into the author form. Is it necessary to do that rather than just embedding each publication directly into the author form?
Posted by Ryan on 2009-05-22
@german-

Yes, but would need to work a little differently than this example. For instance, it only makes sense to embed the author form into a publications form if you're editing ONE publication on a page and also want to be able to edit it's author information from that page. You also have to be careful because the Author object would need to be saved before the Publication object. That may require some custom save code - but I'm not entirely sure. Report back if you end up figuring out how that should happen.
Posted by Ryan on 2009-05-22
@Tom Haskins-Vaughan

Check this outhttp://www.thatsquality.com/articles/can-the-symfony-forms-framework-be-domesticated-a-simple-todo-list#the-forms:
http://www.thatsquality.com/articles/can-the-symfony-forms-framework-be-domesticated-a-simple-todo-list#the-forms

That form is there simply for organization. I've heard the argument that this adds unnecessary overhead (which is technically true), but I haven't tested exactly how MUCH overhead it adds - my guess is that it's negligible.
Posted by germana on 2009-05-22
Yes. i imagine that is the solution, but the thing is i dont know how to start.

should i do saveEmbeddedFoms before save, or in the save method i should save my embedded form before the principal forms ¿?
Posted by Rafael Hernandez on 2009-06-02
I have error in function getAuthorForm() line 36 "Invalid text representation: 7 ERROR: invalid input syntax for integer: ""]" when I'll trye create a new one
Posted by Rafael Hernandez on 2009-06-02
I'll have a error when add a publication a save error is "ERROR: null value in column "author_id" in function function saveEmbeddedForms of myAuthorForn
Posted by Hardeep Khehra on 2009-06-13
Hi Ryan,

The link to the attachment is throwing a 404. Any chance we can get an updated link to the example files.
Posted by Ryan on 2009-06-16
@Hardeep - thanks for the head's up - the link is fixed now - let me know if I forgot anything in the new archive file.

-Ryan
Posted by Hardeep Khehra on 2009-07-06
@Ryan - Thanks for the files. Had another question for you.

How and where should I delete my empty embedded forms?
Posted by M on 2009-08-10
Hi, Ryan fantastic article, made me learn a lot of things!

i wanted to ask a question though:

the publications form is not a propel form, so how does it save its embedded forms?
i'm having problem in this...
Posted by M on 2009-08-10
no , wait, i noticed what exactly the saveembeddedforms method does.

but i still do not understand why it doesn't save my embedded form
Posted by Ryan on 2009-08-10
@M - Great question actually. The idea of a form saving its embedded forms automatically only occurs when your main form is sfFormPropel/sfFormDoctrine. If your main form is one of those types, it'll cascade the save down onto its children. If your main form is just an sfForm, the cascading will NOT occur, even though the embedded forms may be sfFormPropel/sfFormDoctrine.

I know that may seems weird, but sfForm proper is unaware of any "saving" since it's ORM agnostic. sfForm itself doesn't even have a save() method, and so therefore also no saveEmbeddedForms method.

If you're able to have your main form be sfFormPropel/sfFormDoctrine, life is certainly easier. I cover that here: http://www.thatsquality.com/articles/embedding-child-forms-with-sfformpropel-a-practical-example
Posted by Dan on 2009-08-21
I have followed your code and modified it a bit to work with my Doctrine forms. However, although I can successfully display embedded forms every time I try to save I get:

500 | Internal Server Error | InvalidArgumentException
Widget "userform" does not exist.

"userform" is the equivalent of your "publications" embedded form.

I have tried to resolve this in many ways but have failed. Do you have any suggestions for what is wrong?

Thanks for your time.
Posted by Ryan on 2009-08-21
Hey Dan-

Ok, your error is being thrown when you try to access the userform "field" on your form (e.g., $form['userform']). This could actually be happening in a variety of places, but most commonly this occurs when outputting the form in the view.

Either way, the bottom line is that your userform isn't being properly embedded when you submit your form. Either your code that you use to embed the form is not used for the action that you submit to, OR you're trying to access userform before you embed your forms.

Hope that gets you rolling again
Posted by Jason on 2009-09-03
Man this rocks!! I changed it for doctrine and after a tweak or two, I was in business.
Posted by M on 2009-09-10
why did you embed all forms inside another form embedded in the main one?

is this for technical specific reason or your personal tastes?
you could ignore the container form, right?
Posted by Ryan on 2009-09-10
@M-

Yep, completely for organization/personal taste. Somehow you need to be able to foreach through all of your embedded forms in the view. This is one method of organization so that you can do this.
Posted by Dennis Gearon on 2009-11-03
Jason, could you send me your doctrine version? I would be very grateful. gearond AT sbcglobal DOT net
Posted by Legado Lince on 2009-11-07
Hi, just one question, in which class does the saveEmbeddedForms goes?? I suppose that is in the myAuthorForm, but if I want to use the AuthorForm directly?
Posted by Pedro Casado on 2010-04-06
Thanks a lot! I spent 24hrs looking for it!
Posted by Data Recovery on 2010-06-25
Thanks for the important article. I'm searching for an easy way to embed forms.