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
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.
unset($this['id']);
What bad thing would happen if that line was not there?
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).
"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~~~
-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```
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.
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.
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.
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.
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)...
Any ideas what might be causing this?
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
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
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
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
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
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
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
James
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??
@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.
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...
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?
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.
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.
should i do saveEmbeddedFoms before save, or in the save method i should save my embedded form before the principal forms ¿?
The link to the attachment is throwing a 404. Any chance we can get an updated link to the example files.
-Ryan
How and where should I delete my empty embedded forms?
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...
but i still do not understand why it doesn't save my embedded form
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
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.
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
is this for technical specific reason or your personal tastes?
you could ignore the container form, right?
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.