Embedding Relations in Forms with Symfony 1.3 and Doctrine
Par NiKo le dimanche 29 novembre 2009, 18:38 - Dev
- Lien permanent -
11 commentaires -
Tags :
Symfony 1.3 and 1.4 have been released some days ago in RC2 and you should really take a look at it, because they improve a lot the way you work with the framework, especially the forms one. It’s been a couple of days I started to implement a new bookmarks management feature on a project I’m working on currently, and the new model relationship embedding feature of 1.3’s forms framework just saved me a lot of days.
Here’s a quick and dirty example on how to setup a willingly simplistic bookmarks management system using it
The schema
Here’s the naive Doctrine schema I’ll use for this example:
# in config/doctrine/schema.yml
User:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
notnull: true
relations:
Bookmarks:
type: many
class: Bookmark
local: id
foreign: user_id
onDelete: CASCADE
Bookmark:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
notnull: true
url:
type: string(255)
notnull: true
user_id:
type: integer(4)
notnull: true
relations:
User:
type: one
local: user_id
foreign: id
As you can see, it’s really naive. Anyway.
Some fixtures
What would be a Doctrine schema without fixtures? Quite nothing I guess, so here we go:
# in data/fixtures/fixtures.yml
User:
niko:
name: niko
Bookmark:
niko_bookmark1:
User: niko
name: Slashdot
url: http://slashdot.org/
niko_bookmark2:
User: niko
name: Delicious
url: http://delicious.com/
niko_bookmark3:
User: niko
name: Digg
url: http://digg.com/
Building stuff
You guessed it, after having suited the config/databases.yml file to set up a database of our choice, we can now build the database, all the classes we need and load the previously defined fixtures:
$ ./symfony doctrine:build --all --and-load
A basic user’s bookmarks management form
What we want do do now is to manage one User’s bookmarks in a single form. Let’s do it now in a new UserBookmarksForm.class.php file where we’ll build a form extending the autogenerated UserForm one:
<?php // lib/form/doctrine/UserBookmarksForm.class.php class UserBookmarksForm extends UserForm { public function configure() { // We don't want to edit the User object unset($this['name']); // Existing bookmark forms $this->embedRelation('Bookmarks'); $this->widgetSchema->setNameFormat('user_bookmarks[%s]'); } }
All the magic is done by calling the embedRelation() method here: all BookmarkForm instances will be embedded and managed automatically in and by our form.
Let’s use a simple controller to manage the form[1]:
<?php class testActions extends sfActions { public function executeBookmarks(sfWebRequest $request) { $this->form = new UserBookmarksForm($user = Doctrine::getTable('User')->findOneByName('niko')); if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('user_bookmarks'))) { $this->getUser()->setFlash('notice', 'Bookmarks list updated'); $this->redirect('test/index'); } } }
And a basic template:
<form action="." method="post"> <?php echo $form->renderHiddenFields() ?> <?php echo $form->renderGlobalErrors() ?> <table> <?php echo $form ?> <tr> <td></td> <td><input type="submit"/></td> </tr> </table> </form>
You should see something like this:

The form is functional, you can edit several bookmarks at one. Neat? But wait, we can’t add a new one!
Embedding a bookmark creation form
Let’s enhance our form by embedding an empty BookmarkForm form instance in order to add a new bookmark:
<?php // lib/form/doctrine/UserBookmarksForm.class.php class UserBookmarksForm extends UserForm { public function configure() { unset($this['name']); // Bookmark creation form $newBookmarkForm = new BookmarkForm(); $newBookmarkForm->setDefault('user_id', $this->object->id); $this->embedForm('new', $newBookmarkForm); // Existing bookmark forms $this->embedRelation('Bookmarks'); $this->widgetSchema->setNameFormat('user_bookmarks[%s]'); } protected function doBind(array $values) { if ('' === trim($values['new']['name']) && '' === trim($values['new']['url'])) { unset($values['new'], $this['new']); } parent::doBind($values); } }
Here I just override the doBind() method to remove the bookmark creation embedded form if nothing has been submitted by the user in its fields; that’s probably because he just wants to edit existing bookmarks[2].
By refreshing the page you should now be able to see the new form:

So now you’re able to add a new Bookmark and/or edit existing instances within the same simple form. Neat? Wait, we can’t delete existing bookmarks!
Deleting existing bookmarks
So you would like to have a checkbox next to every existing bookmark embedded form to schedule its deletion on saving the form? Wow, that’s getting tricky, I like that.
The Symfony forms framework has evolved a lot, but what we’re trying to achieve here will involve some fine-tunning. Don’t be too much afraid seeing the code 
First, we’ll have to add the checkbox widget to the BookmarkForm form, but only if an existing Bookmark object has been bound to it:
<?php // lib/form/doctrine/BookmarkForm.class.php class BookmarkForm extends BaseBookmarkForm { public function configure() { $this->widgetSchema['user_id'] = new sfWidgetFormInputHidden(); $this->validatorSchema['url'] = new sfValidatorAnd(array( new sfValidatorString(array('max_length' => 255)), new sfValidatorUrl(), )); if ($this->object->exists()) { $this->widgetSchema['delete'] = new sfWidgetFormInputCheckbox(); $this->validatorSchema['delete'] = new sfValidatorPass(); } } }
Refreshing the form page should display something like that:

The UserBookmarksForm now holds this code:
<?php // lib/form/doctrine/UserBookmarksForm.class.php class UserBookmarksForm extends UserForm { /** * Bookmarks scheduled for deletion * @var array */ protected $scheduledForDeletion = array(); /** * Configures the form * */ public function configure() { unset($this['name']); // Bookmark creation form $newBookmarkForm = new BookmarkForm(); $newBookmarkForm->setDefault('user_id', $this->object->id); $this->embedForm('new', $newBookmarkForm); // Existing bookmark forms $this->embedRelation('Bookmarks'); $this->widgetSchema->setNameFormat('user_bookmarks[%s]'); } /** * Here we just drop the bookmark embedded creation form if no value has been * provided for it (it somewhat simulates a non-required embedded form) * * @see sfForm::doBind() */ protected function doBind(array $values) { if ('' === trim($values['new']['name']) && '' === trim($values['new']['url'])) { unset($values['new'], $this['new']); } if (isset($values['Bookmarks'])) { foreach ($values['Bookmarks'] as $i => $bookmarkValues) { if (isset($bookmarkValues['delete']) && $bookmarkValues['id']) { $this->scheduledForDeletion[$i] = $bookmarkValues['id']; } } } parent::doBind($values); } /** * Updates object with provided values, dealing with evantual relation deletion * * @see sfFormDoctrine::doUpdateObject() */ protected function doUpdateObject($values) { if (count($this->scheduledForDeletion)) { foreach ($this->scheduledForDeletion as $index => $id) { unset($values['Bookmarks'][$index]); unset($this->object['Bookmarks'][$index]); Doctrine::getTable('Bookmark')->findOneById($id)->delete(); } } $this->getObject()->fromArray($values); } /** * Saves embedded form objects. * * @param mixed $con An optional connection object * @param array $forms An array of forms */ public function saveEmbeddedForms($con = null, $forms = null) { if (null === $con) { $con = $this->getConnection(); } if (null === $forms) { $forms = $this->embeddedForms; } foreach ($forms as $form) { if ($form instanceof sfFormObject) { if (!in_array($form->getObject()->getId(), $this->scheduledForDeletion)) { $form->saveEmbeddedForms($con); $form->getObject()->save($con); } } else { $this->saveEmbeddedForms($con, $form->getEmbeddedForms()); } } } }
The code should be self-explanatory… or not. But anyway, you should now be able to add a new bookmark and delete existing ones as well, in a single form and process, and we’re still using the very same controller from the beginning!
Okay, let’s admit it, we’re reaching the limits of Symfony and probably those of my knowledge of it as well, but… IT WORKS.
But, what about the templating?
As of now we’ve used the convenient <?php echo $form ?> trick, but we now want to have full fine-grained control over the way our embedded form fields will look like.
Let’s update the form template accordingly:
<form action="." method="post"> <?php echo $form->renderHiddenFields() ?> <?php echo $form->renderGlobalErrors() ?> <!-- Embedded new bookmark form --> <fieldset> <legend><?php echo __('Create a new bookmark') ?></legend> <?php echo $form['new']->renderError() ?> <div class="form-row"> <?php echo $form['new']['name']->renderLabel() ?> <div class="form-field"> <?php echo $form['new']['name']->render() ?> <?php echo $form['new']['name']->renderError() ?> </div> </div> <div class="form-row"> <?php echo $form['new']['url']->renderLabel() ?> <div class="form-field"> <?php echo $form['new']['url']->render() ?> <?php echo $form['new']['url']->renderError() ?> </div> </div> </fieldset> <!-- /Embedded new bookmark form --> <!-- Embedded existing bookmark forms --> <?php foreach ($form['Bookmarks'] as $i => $eForm): ?> <fieldset> <legend><?php echo sprintf(__('Edit bookmark #%d'), $i+1) ?></legend> <?php echo $eForm->renderError() ?> <div class="form-row"> <?php echo $eForm['name']->renderLabel() ?> <div class="form-field"> <?php echo $eForm['name']->render() ?> <?php echo $eForm['name']->renderError() ?> </div> </div> <div class="form-row"> <?php echo $eForm['url']->renderLabel() ?> <div class="form-field"> <?php echo $eForm['url']->render() ?> <?php echo $eForm['url']->renderError() ?> </div> </div> <div class="form-row"> <div class="form-field"> <?php echo $eForm['delete']->render() ?> <?php echo $eForm['delete']->renderError() ?> <?php echo $eForm['delete']->renderLabel(__('Delete this bookmark')) ?> </div> </div> </fieldset> <?php endforeach; ?> <!-- /Embedded existing bookmark forms --> <input type="submit" value="<?php echo __('Update your bookmarks') ?>"/> </form>
Now you can really control the semantics of the HTML code of the form, field by field.
As always, if you have better solutions in mind, feel free to post them in the comments.
11 commentaires (Ajouter un commentaire)
Testé, et approuvé ;-).
T'as oublié un ) à cette ligne:
$this->form = new UserBookmarksForm($user = Doctrine::getTable('User')->findOneByName()
Ok je chipotte je chipotte... Super post !!! THX !
Trop bien ! Merci N1K0 pour le billet
CpNForTehWin> C'est corrigé, merci
Thanks n1k0 ! It will save some time ...
Just a typo : Let’s do it now in a new UserBookmarks.class.php => Let’s do it now in a new UserBookmarksForm.class.php
@vjousse: effictivement, c'est corrigé merci
To avoid an error if parent form is not valid you have to modify a little the
doBind()function:<?php protected function doBind(array $values) { parent::doBind($values); if ($this->isValid() && '' === trim($values['new']['name']) && '' === trim($values['new']['url'])) { unset($values['new'], $this['new']); } if (isset($values['Bookmarks'])) { foreach ($values['Bookmarks'] as $i => $bookmarkValues) { if (isset($bookmarkValues['delete']) && $bookmarkValues['id']) { $this->scheduledForDeletion[$i] = $bookmarkValues['id']; } } } }nice post as usual. i just don't get a thing: why do you first bind the delete checkbox values to the form object and then you unset them in doUpdateObject ? cannot these values be unset inside the doBind loop before calling parent::doBind ?
gpilotino> Unfortunately, you cannot unset an embedded form once the form itself has been bound (whereas you can still unset normal fields)
Lovely post! On a *slightly* different note, I may have missed this in previous symfony discussions, but what is the relevance of using integer(4) for primary keys? I notice that it's used in sfGuard too.
Tom> I have absolutely no idea... Actually I used integer(4) because in my real world use case the User model was extending sfGuardUser (you must define the exact same type in the foreign keys), and I just copied/pasted the pk yaml code for the Bookmark model...
Feel free to ask on the symfony-user mailing-list, I'd be also interested to have an explanation from Jon
La discussion continue ailleurs
Social comments and analytics for this post
This post was mentioned on Twitter by n1k0: [blog] Embedding Relations in Forms with Symfony 1.3 and Doctrine http://n1k.li/embedrelations...