Today I want to demonstrate how to make interactive forms with sfForm and Doctrine. The target is to create an interface which edits an object, edits related objects and allows you to add or remove related objects. See the screenshot below for the end-result:
Preparation
- To illustrate my way of installing I added a full history of what I did. The tutorial will start in the next paragraph. *
I started with a fresh 1.2beta project with something like
symfony generate:project sandbox. After the project setup I ran:
1) ./symfony generate:app frontend to create the frontend app
2) Enable Doctrine: config/ProjectConfiguration.class.php
<?php require_once dirname(__FILE__).'/../../lib/symfony/1.2/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); class ProjectConfiguration extends sfProjectConfiguration { public function setup() { $this->enablePlugins(array('sfDoctrinePlugin')); $this->disablePlugins(array('sfPropelPlugin')); } }
3) I installed a SQLite db: config/databases.yml
all:
doctrine:
class: sfDoctrineDatabase
param:
dsn: sqlite:///<?php echo dirname(__FILE__); ?>/../log/sqlite.db4) I made the schema.yml
---
Author:
columns:
name: string(255)
description: string
Book:
columns:
author_id: integer(20)
title: string(255)
released: integer(20)
relations:
Author:
foreignAlias: Books5) I put some sample data in /data/fixtures/fixtures.yml. There’s a link to the file at the end of this post.
6) Run some fun stuff at the command line: ./symfony doctrine:build-all-load followed by ./symfony cc and to setup the right permissions sudo ./symfony project:permissions.
7) Now I create a default doctrine module to edit the Author table: ./symfony doctrine:generate-module frontend author Author. Everything should be up and running now, visiting http://your_host/frontend_dev.php/author should give you something like:
The Author form is still very basic:
Automatic templates
First step is to remove the strict formatting in the template and enable automattic formatting. As I’m lazy I want sfForm to do the layout work for me. Open up the /apps/frontend/modules/author/templates/_form.php partial and remove some code. The following template will display sfForm using it’s own __toString() rendering system:
<?php include_stylesheets_for_form($form) ?> <?php include_javascripts_for_form($form) ?> <form action="<?php echo url_for('author/'.($form->getObject()->isNew() ? 'create' : 'update').(!$form->getObject()->isNew() ? '?id='.$form->getObject()->getid() : '')) ?>" method="POST" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>> <?php if (!$form->getObject()->isNew()): ?> <input type="hidden" name="sf_method" value="PUT" /> <?php endif; ?> <table> <tfoot> <tr> <td colspan="2"> <a href="<?php echo url_for('author/index') ?>">Cancel</a> <?php if (!$form->getObject()->isNew()): ?> <?php echo link_to('Delete', 'author/delete?id='.$form->getObject()->getid(), array('post' => true, 'confirm' => 'Are you sure?')) ?> <?php endif; ?> <input type="submit" value="Save" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>
Don’t be impressed here; I only removed the code in the <tbody></tbody> section and removed the call to $form->renderHiddenFields(). Your form should still look exactly the same as in the screenshot supplied before.
Embed the forms
Now we can add the forms for all the books an Author has written to the form. Open /lib/form/doctrine/AuthorForm.class.php and add some intelligence:
class AuthorForm extends BaseAuthorForm { public function configure() { foreach ($this->object['Books'] as $index=>$book) { $this->embedForm('book'.$index, new BookForm($book)); } } }
This works, now you can edit all books:
Adding interactivity
The form works well and saves all values, but it is not possible to remove a book or add a new one yet. To achieve this we can add some more submit buttons, for each book one. This will inform the server what the user wants to delete or insert. I also restructured the code a bit to make it easier to understand.
public function configure() { foreach ($this->object['Books'] as $index=>$book) { $fieldName = 'book_'.$book['id']; $form = new BookForm($book); unset($form['author_id']); $this->embedForm($fieldName, $form); $label = '<input type="submit" name="submit" value="delete_'.$book['id'].'">'; if (count($this->object['Books']) -1 == $index) { // this is the last book $label.= '<input type="submit" name="submit" value="insert">'; } $label.= ' Book '.($index+1); $this->widgetSchema->setLabel($fieldName, $label); } }
I admit, it’s quite ugly on the screen. But in the action we will now have a parameter submit which is set to insert when the insert-button is clicked. It’s set to delete_* when one of the delete-buttons is clicked (* is the id of the related book), or it’s not set, then the normal ‘Save’ button is clicked.
Let’s work on /apps/frontend/modules/author/actions/actions.class.php to enable the form to react to these new parameters:
public function executeUpdate(sfWebRequest $request) { $this->forward404Unless($request->isMethod('post') || $request->isMethod('put')); $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id'))); //get the submit value from the request, and split it into an array of command, id $submit = ($request->hasParameter('submit') ? $request->getParameter('submit') : '_'); $submit = explode('_',$submit); switch ($submit[0]) { case 'insert': $author['Books'][] = new Book(); break; case 'delete': $this->forward404Unless($book = Doctrine::getTable('Book')->find($submit[1])); $book->delete(); break; } $this->form = new AuthorForm($author); $this->processForm($request, $this->form); $this->setTemplate('edit'); }
This code works! After pressing one time on the insert button we can see that a new book is added to the form. Thanks to Doctrine the newly created book know’s it’s written by the Author. This is because the record defaults are automatically used as form defaults. According to the image below the only thing we need to do is some styling:
Styling the submit buttons
Luckily styling will be very easy. The old man at the wc3 will help a little, as they have once defined an <input type='image'>. This input looks like an image, but clicking it will submit the form. Let’s update the AuthorForm.class.php again:
public function configure() { foreach ($this->object['Books'] as $index=>$book) { $fieldName = 'book_'.$book['id']; $form = new BookForm($book); unset($form['author_id']); $this->embedForm($fieldName, $form); $label = '<input type="image" src="/images/delete.gif" name="submit" value="delete_'.$book['id'].'">'; if (count($this->object['Books']) -1 == $index) { // this is the last book $label.= '<input type="image" src="/images/add.gif" name="submit" value="insert">'; } else { $label.= '<img src="/images/empty.gif">'; } $label.= ' Book '.($index+1); $this->widgetSchema->setLabel($fieldName, $label); } }
Now our form looks and works the way we want:
Some more things to consider
Deleting a book will give you an Unexpected extra form field named "book2". error. This can be undone if you unset this data from the request. It needs some more work as the default generated processForm function has to be changed:
protected function processForm($data, sfForm $form) { $form->bind($data); if ($form->isValid()) { $author = $form->save(); $this->redirect('author/edit?id='.$author['id']); } } public function executeUpdate(sfWebRequest $request) { $this->forward404Unless($request->isMethod('post') || $request->isMethod('put')); $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id'))); $data = $request->getParameter('author'); //get the submit value from the request, and split it into an array of command, id $submit = ($request->hasParameter('submit') ? $request->getParameter('submit') : '_'); $submit = explode('_',$submit); switch ($submit[0]) { case 'insert': $author['Books'][] = new Book(); break; case 'delete': $this->forward404Unless($book = Doctrine::getTable('Book')->find($submit[1])); unset($data['book'.$book['id']]); $book->delete(); break; } $this->form = new AuthorForm($author); $this->processForm($data, $this->form); $this->setTemplate('edit'); }
Also make sure to always check the $request->getParameter('submit') for validity. It’s not secured by the sfForm security system so you have to use it cautiously.
That’s it for today, thanks for reading!
Full source of this project (Make sure you change /config/ProjectConfiguration.class.php to point to the right location of the Symfony 1.2 Library)






great tutorial. thats really usefull
Just great tutorial.
I think a widget should be created to populate some kind of HTML table that will just allow the user to add a line or update existing data.
For example, you access the author form and in a separate tab, you get the books he wrote and you can add another one or delete a line. The widget will rely on the author_id to filter the data.
Options will include a list of fields we want to be able to populate as well as a second list to tell symfony to render the different fields as plain text or input boxes.
I do not know of it is possible to do it but I think it should be.
Thanks for taking the time to post. However, I could not get this to work under sf1.2 beta2. I made the needed updates to the code such as passing the $request parameter to processForm() instead of the data.
However, this is the closest I’ve gotten to 1-m relationship working in the new form framework. When I try this the new Book record saves without the title or release information.
FredLab: I think this would be a great idea for a widget as well. As soon as I can get 1-m relationships to work I’ll make one.
@Steven: Strange it didn’t work. I made the example in sf1.2 beta as well, so that shouldn’t be the reason. To test some more I added my full source to the post. @all: Thanks for your replies
@Steven: I was thinking about your problem. There’s a small change I didn’t describe in the post. In the BookForm I changed the naming scheme for the widgets from $fieldName = ‘book’.$index; to $fieldName = ‘book_’.$book['id']; to make the name easier to reproduce; this was a typo in the post. Good luck
With the new updated code it now works. I’m glad after much searching I can finally get a 1-m relationship to work, so THANK YOU SO MUCH.
The only thing I don’t like about this code is if the user clicks the plus sign and abandons the form there will be a blank new record. By embedding the forms this way http://trac.symfony-project.org/ticket/4906 you would not have to add the record before hand. Instead when the array is passed in Doctrine would automatically save the new foreign record. But sadly this does not work correctly with validation.
Thanks for the great post.
Another problem, which I don’t know if you would have a solution to is that you can’t have a form for a new author which allows you to add books to it, because the Author object has to be saved before it has a book.
Right now i”m trying to create form where for example a new user would be given a form with 2 book objects to fill out. This can’t be done without saving the author and the two books, which means if the user abandons the form all of that will be blank.
[...] Interactive embedded forms [...]
Thank you so much for this post. Saved my day…
I think it would be great if you could get a link onto the symfony 1.2 Documentation page, as there is only a very limited amount of information out for embedded forms, that I could find at least!
Nice tutorial, but I use a different approach, I think that inserting HTML direct input from the class completely negate the whole MVC pattern.
Take a look at the way I handle the same problem in a different manner http://sandbox-ws.com/how-to-embed-forms-in-symfony-12-admin-generator-part-2, even though it is not perfect
Great tutorial, just one question, what about if we had:
authors books authors_books
this meaning that a book could be made by two diferent authors ?
Im trying a similar thing without success
portfolio tags portfolio_tags
Any tips ? Joao
I hate the new form of symfony.
I have to write much code to make things very simple.
Hi,
A great tutorial
There is a little mistake in the function executeUpdate(…)
replace unset($data['book'.$book['id']]); by unset($data['book_'.$book['id']]);
to don’t have the “Unexpected extra form field” message
Olivier
How can I validate a group of embedded forms, so I can restrict a minimum of two books per author.
How to change the line:
$author['Books'][] = new Book();
from Doctrine into Propel ? Any ideas ?
I’ve incorporated this tutorial and shown the code needed to load more embedded forms via AJAX/jQuery
How to Embed AJAX(!) Forms in Symfony 1.2 Admin Generator http://israelwebdev.wordpress.com/2009/05/04/how-to-embed-ajax-forms-in-symfony-12-admin-generator/
Why with doctrine and not propel?
Great tutorial thank you!
All works well except if I add a new book and then save it!
I get a Unexpected extra form field named “book_” error when it tries to save the new book.. the others get saved fine if I don’t add a new book!
Thanks!
I have the same problem. If I install your sandbox, then everything is okay. But if I create own project, I get an error “Unexpected extra form field”
Pleeease help us, thank you!
What about doing the child table/form, and it working.
How about a form that displays a list of books and the authors are shown as the __tostring() value instead of the ‘author_id’? How is that easisly done?
One of the key parts to this whole thing, you make a joke of, “. . . don’t be impressed . . . ” Swapping out the strict formatting in ‘_form.php’ actually ENABLES the viewing of the forms that are embedded, (don’t know if that’s true for MERGED forms). I just thought it was no big deal, just for looks, but IT’S CRITICAL to this working.
PS, ’swapping out strict formatting’ and substituting in the block is what does it.
It erased my code. {{{}}}
Hi,
I made something quite similar, but when I modify some values in an embedded form and click on “save”, it doesn’t modify the values in my database.
Is it normal ?
I would like to see my new solution for this problem:
class AuthorForm extends BaseAuthorForm { public function configure() { $books = $this->getObject()->getBooks();
} }
[...] How to Embed Forms in Symfony 1.2 Admin Generator Interactive embedded forms [...]
Hi folks ! Good news : i found out how to get at least one first “book” (i am doing this for articles for newsletters). So you just override the “save” method of your ‘Author’ class :
public function save(Doctrine_Connection $conn = null) {
//You need to keep the state because when the parent::save() method will be called, it will not be new anymore.
//Now you save it, and you keep the answer to return it after
//If this was new, you create a new book and save it }
//And you return the answer for symfony to perform it’s job. }
Comments : this is still not perfect : you have an empty book by default, you still can delete it but then you will have to add one manually. But when you create an author, it’s because you have at least one book he wrote no ?
Hope this will help you, i took some time to find out how to do this (and now I realise i forgot something in my model…)
Bye
Hi, is it possible that the image tags don’t work in IE8? Implemented your solution, which is perfect, but in IE nothing really happens, when I click on one of the images. Also there a no values transfered.
Did anyone else experience something like that?
What is the equivalent of the following piece of code to Propel:
$author['Books'][] = new Book();
I did find the propel equivalent, BUT when clicking the insert button nothing happen:
$author->addBook(new Book());
wonderful! thanks a lot.