How to Embed Forms in Symfony 1.2 Admin Generator Part 2

In part one of this article we saw how to embed forms in a one-to-one relationship. But I often run into a situation where I want to edit a parent model, it’s children models, and maybe add a new child model while I’m at it, greedy I know, but isn’t that the reason why we use the admin generator. Anyway in this article we will examine the one-to-many relationship.

Before we get started here’s a sneak peak of the CategoryForm after the modifications to be done.
embed_2

Files need for this article

Ok, lets get started, consider the following schema:

category:
    _attributes: { phpName: Category }
    id: { type: INTEGER, size: '11', primaryKey: true, autoIncrement: true, required: true }
    name: { type: VARCHAR, size: '255', required: true }
    created_at: { type: TIMESTAMP, required: false }
    updated_at: { type: TIMESTAMP, required: false }
subcategory:
    _attributes: { phpName: Subcategory }
    id: { type: INTEGER, size: '11', primaryKey: true, autoIncrement: true, required: true }
    name: { type: VARCHAR, size: '255', required: true }
    category_id: { type: INTEGER, size: '11', required: true, foreignTable: category, foreignReference: id, onDelete: CASCADE, onUpdate: RESTRICT }
    created_at: { type: TIMESTAMP, required: false }
    updated_at: { type: TIMESTAMP, required: false }

Steps needed to achieve the required result

  1. Modify the CategoryForm to include embedded forms for all available subcategories of the current category.
  2. Modify the widget of the name field in the subcategory
  3. Modify the CategoryForm to add a new blank subcategory form
  4. Override the bind method of the sfForm class to skip saving and validating the new subcategory form if the name field was left blank
  5. Remove fields from SubcategoryForm

Embedded forms for all available subcategories of the current category [step 1, 2, and 3]

// lib/forms/CategoryForm.class.php
public function configure() {

  // remove timestamps
  unset($this['created_at'], $this['updated_at']);

  // embed forms only when editing
  if (!$this->isNew()) {

    // embed all subcategory forms
    foreach ($this->getObject()->getSubcategorys() as $subcategory) {

    // create a new subcategory form for the current subcategory model object
    $subcategory_form = new SubcategoryForm($subcategory);

    // embed the subcategory form in the main category form
    $this->embedForm('subcategory'.$subcategory->getId(), $subcategory_form);

    // set a custom label for the embedded form
    $this->widgetSchema['subcategory'.$subcategory->getId()]->setLabel('Subcategory: '.$subcategory->getName());

    // change the name widget to sfWidgetFormInputDelete
    $this->widgetSchema['subcategory'.$subcategory->getId()]['name'] = new sfWidgetFormInputDelete(array(
    'url' => 'category/deleteSubcategory',      // required
    'model_id' => $subcategory->getId(),        // required
    'confirm' => 'Sure???',                     // optional
    ));

  }

  // create a new subcategory form for a new subcategory model object
  $subcategory_form = new SubcategoryForm();

  // embed the subcategory form in the main category form
  $this->embedForm('subcategory', $subcategory_form);

  // set a custom label for the embedded form
  $this->widgetSchema['subcategory']->setLabel('New Subcategory');

  }
}

Override the bind method

public function bind(array $taintedValues = null, array $taintedFiles = null) {

	// remove the embedded new form if the name field was not provided
	if (is_null($taintedValues['subcategory']['name']) || strlen($taintedValues['subcategory']['name']) === 0 ) {

		unset($this->embeddedForms['subcategory'], $taintedValues['subcategory']);

		// pass the new form validations
		$this->validatorSchema['subcategory'] = new sfValidatorPass();

	} else {

		// set the category of the new subcategory form object
		$this->embeddedForms['subcategory']->getObject()->
                setCategory($this->getObject());

	}

	// call parent bind method
	parent::bind($taintedValues, $taintedFiles);

}

Remove fields from SubcategoryForm

public function configure(){
  unset($this['created_at'], $this['updated_at'], $this['category_id']);
}

Now we create the action deleteSubcategory in category module

// apps/backend/modules/category/actions/actions.class.php
public function executeDeleteSubcategory(sfWebRequest $request) {
  $sub_category = SubcategoryPeer::retrieveByPk($request->getParameter('id'));
  $sub_category->delete();
  $this->redirect('@category_edit?id='.$sub_category->getCategory()->getId());
}

That’s it, I hop that you enjoyed this article.

Spread the word

50 Comments So Far

Avatar

jukea | January 7th, 2009 at 6:25 pm

Great work! Your code is clean and concise.

I like it. however, I’d suggest you made it possible to edit subcategories even when the object is new (isNew()==true).

To push the enveloppe even more, I’d like to be able to add multiple embeded forms, ideally using AJAX (why not jquery?).

And to wrap this in some way (plugin ? widget ?) would be a great benefit for the symfony community. But I’m asking for a lot I guess :D

Avatar

Ahmed El.Hussaini | January 7th, 2009 at 9:01 pm

@jukea: thanks for you kind comment, and no it’s not too much to ask, I was planning on extending the admin generator to be 100% Ajax. Anyway, when it’s ready you’ll be the first to know.

Avatar

nvidhive | January 7th, 2009 at 9:27 pm

Thank you for your post.

I have gone over the steps of the tutorial and I encounter this error when attempting to delete a Subcategory:

Action “category/deleteSubcategory” does not exist.

I have placed the sfWidgetFormInputDelete class file in /lib/form

Is there something I am doing wrong or missing?

Avatar

Ahmed El.Hussaini | January 7th, 2009 at 10:30 pm

@nvidhive: No you didn’t do anything wrong, I thought I’d leave the creation of the action for the developers to delete the subcategory. Anyway I’ll will update the post within the hour. Sorry for any inconvenience.

Avatar

Ahmed El.Hussaini | January 7th, 2009 at 10:32 pm

@nvidhive: I forgot to mention, it’s better to place the widget in any lib folder, but not in lib/form folder.

Avatar

nvidhive | January 7th, 2009 at 11:36 pm

Wow!

Thank you so much for your quick response. One final thing:

I am obviously missing something from my routing.yml

How would you suggest handling this?

error is as follows:

The “/category/:id/edit.:sf_format” route has some missing mandatory parameters (:id)

I realize you are probably writing this article from a perspective that the reader is much more knowledgeable, but perhaps this final tidbit will help others as well.

Thanks again!

Avatar

Ahmed El.Hussaini | January 8th, 2009 at 12:47 am

@nvidhive: I’m really sorry, it’s my mistake, I fixed the typo, please check the deleteSubcategory function above.

Avatar

jukea | January 8th, 2009 at 12:51 am

wow, great! I’m looking forward for your next version ! Now, you just have to switch to doctrine, and you’ll be my hero ;)

Avatar

Ahmed El.Hussaini | January 8th, 2009 at 12:56 am

@jukea: You know what, I’m having serious thoughts about changing to Doctrine, I’m tired of building complex criteria using Propel. Don’t get me wrong, Propel is a good ORM, but having a prior background and experience in Hibernate makes Doctrine like Pink Floyd music to my ears.

Maybe soon I’ll get to be your hero. :)

Avatar

Malte Blättermann | January 9th, 2009 at 5:28 pm

Hello,

Is this method compatible with 1.1, too?

That would be great.

Thx, Malte

Avatar

Ahmed El.Hussaini | January 9th, 2009 at 6:13 pm

@Malte Blättermann: I’ve not tried it with symfony 1.1 but as far as I know embedding forms is a new feature in symfony 1.2, so I guess it can’t be implemented in symfony 1.1, sorry.

Avatar

nvidhive | January 9th, 2009 at 7:05 pm

Thank you again for your update.
For anyone interested, with the help of the community I was able to convert this to work with Doctrine. Just the schema change and 1 line in 2 files.

//config/doctrine/schema.yml
---
Category:
  tableName: category
  actAs:
    Timestampable: ~
  columns:
    name:
      type: string(255)
      notnull: true
Subcategory:
  tableName: subcategory
  actAs:
    Timestampable: ~
  columns:
    name:
      type: string(255)
      notnull: true
    category_id:
      type: integer
      notnull: true
  relations:
    Category:
      foreignAlias: Subcategorys
      local: category_id
      foreign: id
      type: one
      onDelete: CASCADE
      onUpdate: RESTRICT

//lib/sfWidgetFormInputDelete.class.php 

*Replace*
$this->setOption('icon', sprintf('http://%s%s/sfPropelPlugin/images/delete.png',
$request->getHost(), $request->getRelativeUrlRoot()));

*With*
$this->setOption('icon', sprintf('http://%s%s/sfDoctrinePlugin/images/delete.png',
$request->getHost(), $request->getRelativeUrlRoot()));

//apps/backend/modules/category/actions/actions.class.php

*Replace*
$sub_category = SubcategoryPeer::retrieveByPk($request->getParameter('id'));

*With*
$sub_category = Doctrine::getTable('Subcategory')->find($request->getParameter('id'));

It works perfectly!

Avatar

Ahmed El.Hussaini | January 9th, 2009 at 7:12 pm

@nvidhive: great job, thank you very very much.

Avatar

Waff | January 17th, 2009 at 2:34 pm

Great article, thanks!
I’ve tried to extend this example whith I18N behavior:


Subcategory:
tableName: subcategory
actAs:
Timestampable: ~
I18n:
fields: [desc]
columns:
name:
type: string(255)
notnull: true
desc:
type: string(255)
notnull: true

in SubcategoryForm:

public function configure()
{
$this->embedI18n(array(’hu’, ‘en’));
}

when i save the form(Category) symfony throws an exception:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column ‘id’ cannot be null

‘INSERT INTO subcategory_translation (id, lang, desc) VALUES (?, ?, ?)’, array(null, ‘en’, ‘w’)

symfony try to insert the translation table first not subcategory and there is no id yet.
what is the workaround? i’ve try to put some code to the bind method but not works.

thanks for your help!

Avatar

Ahmed El.Hussaini | January 17th, 2009 at 11:55 pm

@Waff: First of all thanks for your kind comment, second of all as I see from the schema example in your comment you’re using Doctrine, unfortunately I’ve tested this method only using Propel, but I’ve not actually tried adding I18N support to it, I’ll look into it ASAP and update the method to support I18N, but I’m afraid it will also be in Propel but I’m sure you can manage to import it to Doctrine.

Avatar

Paul | January 19th, 2009 at 3:18 pm

Thank’s for the wonderfull guide!
I’ve a question: how can I sort the fields with embed forms? for example in your example, I want see the fields in this order:
-Name
-New Subcategory
-Section

Avatar

Ahmed El.Hussaini | January 23rd, 2009 at 1:48 pm

@Paul: I think you can do the sorting in the form class.

Avatar

juro | February 4th, 2009 at 1:13 am

Hi,
when I “embed all subcategory forms” using the foreach loop, how can I create a widget, so that I can place it in the generator.yml?

As soon as I want to customise my form using the generator.yml, I loose the list of subcategories.

Thanx for your input

juro

Avatar

juro | February 4th, 2009 at 2:24 am

Answering my own question (thanks to Jon Wage):

$subcategories = new sfForm();

foreach ($this->getObject()->getSubcategorys() as $subcategory) {
$subcategory_form = new SubcategoryForm($subcategory);
$subcategories->embedForm(’subcategory’.$subcategory->getId(), $subcategory_form);
$subcategories->widgetSchema['subcategory'.$subcategory->getId()]->setLabel(’Subcategory: ‘.$subcategory->getName());
$subcategories->widgetSchema['subcategory'.$subcategory->getId()]['name'] = new sfWidgetFormInputDelete(array(
‘url’ => ‘category/deleteSubcategory’, // required
‘model_id’ => $subcategory->getId(), // required
‘confirm’ => ‘Sure???’, // optional
));
}

$this->embed(’subcategories’, $subcaterories);

In generator.yml, I can place ’subcategories’ anywhere I want to.

Avatar

all inclusive | February 16th, 2009 at 2:15 pm

hey people this is perfect exactly what i have been looking for a long time here … thanks so much everyone! just great!

Avatar

Sander | February 19th, 2009 at 11:35 am

Thanks for the article, really helped me a lot :)

But now i’m trying to get this to work with a file upload in for example the subcategory.

When you create a new subcategory, the file is ignored. The form doesn’t even try to upload it.

The file only uploads after the subcategory is created. And you upload the file for a second time

Anyone has an idea how to get the file to upload the first time? :)

Avatar

refptr | March 13th, 2009 at 6:13 am

@Ahmed: Great Works, thanks a lot.

@Sander: #5667 (In generated forms, Doctrine 1.1 does not save model after linking relations.)

Avatar

spl13 | April 6th, 2009 at 4:35 am

I’m trying to override the bind method so that the embedded forms aren’t saved. The problem is that they’re saved before they get to bind. Where should I be removing them?

Avatar

murkein | May 13th, 2009 at 9:44 pm

it’s why propel?

Avatar

Ahmed El.Hussaini | May 14th, 2009 at 9:47 am

@murkein: what do you mean exactly. Did you mean why I’m using Propel instead of Doctrine ?

Avatar

leyan | May 26th, 2009 at 8:01 pm

hi i get a embedded form ‘user’ into a form ‘client’ and everything is ok but my user class has a realtion many-to-many with a ‘group’ class so when i try to save the object user from the embedded form the groups (from a group list widget) doesnt save, so i try to save it by hand so i call the saveUserGroup() method from my BaseUserGroup class and the i get an exception that the form is not valid. my question is: there is something to do in relations many-to-many to save embedded forms? thx

Avatar

leyan | May 26th, 2009 at 10:29 pm

i found this:

http://groups.google.es/group/symfony-devs/msg/99f98f386da3a2fd

t is an issue because the generated form classes
from your schema have generated functions for saving many to many
relationships which are invoked in the overridden doSave() method of each
model form. Since embedded forms don’t call save() or doSave() on the
embedded forms the m2m relationships are never saved. We discovered this
issue last week but haven’t gotten to talk to Fabien about it yet. This is
what I did for sfFormDoctrine to fix the issue.

public function saveEmbeddedForms($con = null, $forms = null)
{
if (is_null($con))
{
$con = $this->getConnection();
}
if (is_null($forms))
{
$forms = $this->embeddedForms;
}
foreach ($forms as $key => $form)
{
if ($form instanceof sfFormDoctrine)
{
$form->bind($this->values[$key]);
$form->doSave($con);
$form->saveEmbeddedForms($con);
}
else
{
$this->saveEmbeddedForms($con, $form->getEmbeddedForms());
}
}
}

i try this way and it works, im using 1.2.5, dont know if the issue is solved in newer version. so if anyone has this problem this a clue

Avatar

Benjamin | June 3rd, 2009 at 9:58 am

Hi,
I have some problems with the delete widget:

Fatal error: Class ’sfWidgetFormInputDelete’ not found

I put the widget into /var/www/sf/lib/widget/ do I have to run any command to enable it?

Thanks for your help!

Avatar

Ahmed El.Hussaini | June 3rd, 2009 at 2:52 pm

@leyan: nice work ,thanks for the tip, I’ll try to test it as soon as possible

Avatar

Ahmed El.Hussaini | June 3rd, 2009 at 2:55 pm

@Benjamin: just put the contents of the zip file in your_project/lib directory

Avatar

docz | June 9th, 2009 at 11:10 pm

In this image: http://forum.symfony-project.org/index.php/fa/1152/0/

How can I remove “Parameters” “Value” and “Data” and their boxes, and move “Company description” “Country” and so on on the same level as “Main Contact”, “Parameters” making it seem to be part of the same form? This is all generated code, so when I can’t seem to find the templates used for the embedded form fields.

Thank you in advance for any help.

Doc-Z

Avatar

Ahmed El.Hussaini | June 10th, 2009 at 6:53 am

@docz: are you sure the link you provided is correct ?, it’s not working with me.

Avatar

docz | June 10th, 2009 at 10:33 am

It seems the link only works if one visits the symfony forum first. Try this link instead: http://www.interactive.no/img/screenshot.GIF

Docz

Avatar

shmayek | June 18th, 2009 at 2:47 pm

Very nice. But how to add choice to add a new subcategory or choose existing one (with null category_id to be clear).
The similar solution for Section. There is only combobox with sections, but how to add possibility to choos to add new section or choose existing? Maybe you have idea hot to approach this or know a good tutorial or similar solutions to look for?

Avatar

Ryan Grenz | August 13th, 2009 at 9:21 pm

A tip for anyone who has run into the same problem as me:

I have a series of sub-forms embedded in a simple sfForm container (to make it easier to group related sub-forms together into 1), but couldn’t get the bind function to work for ages as you did above.

The solution was as follows:

public function bind(array $taintedValues = null, array $taintedFiles = null) {
$newCtrForm = &$this->embeddedForms['committers']->embeddedForms['committer'];

if (is_null($taintedValues['committers']['committer']['pkg_member_id']) || strlen($taintedValues['committers']['committer']['pkg_member_id']) === 0 ) {
unset($newCtrForm, $taintedValues['committers']['committer']);
$this->validatorSchema['committers']['committer'] = new sfValidatorPass();
} else {
$newCtrForm->getObject()->setDevnetPackage($this->object);
}

parent::bind($taintedValues, $taintedFiles);

}

Note how I make a reference variable to $this->embeddedForms, and then ->embeddedForms again to access the real form.

$newCtrForm = &$this->embeddedForms['committers']->embeddedForms['committer'];

I believe the ArrayAccess implementation is faulty when trying to access a nested array like $this->embeddedForms['committers']['committer']

Hope this helps someone like it did me! (Took a day to work out the problem!)

Ryan

Avatar

James | August 15th, 2009 at 6:29 am

Thanks for your examples, it’s a very good way of achieving one to many relationship with a cute and clean code.

Have you done something about the ajax version of this embeded forms??

James

Avatar

Ahmed El.Hussaini | August 15th, 2009 at 6:36 am

@jukea Thanks and I’m glad that you found my tutorial useful, but unfortunately I’ve not found the type yet to create the Ajax version.

Avatar

Create a pictures gallery with symfony in the admin « Guide pour le framework Symfony | August 18th, 2009 at 8:32 pm

[...] basically it will be added in the route. This widget is based on the code found in the following article which I slightly modified. // in [...]

Avatar

Pierre | August 21st, 2009 at 9:06 am

@Ryan : thank you so much, I tried almost every possible configuration to pass my embedded embedded form when null except the one you gave !!!

Avatar

Norbert | September 11th, 2009 at 11:09 am

Could not find the 1. part of this tutorial :-(
where is it gone?

Avatar

Ahmed El.Hussaini | September 11th, 2009 at 12:18 pm

@Norbert Part 1

Avatar

coupons for mcdonalds | September 14th, 2009 at 12:43 am

Thank you very much for this awesome blog post.

Avatar

zhekanax | September 15th, 2009 at 10:15 am

Just noticed that Doctrine need to ‘link-back’ object like this:

scheme:
User: { columns: id: … }
UserInfo: { columns: user_id: … }

function bind() {

$this->embeddedForms['UserInfo']->getObject()->User = $this->object;
// here
$this->object->UserInfo = $this->embeddedForms['UserInfo']->getObject();
}

at least in case of one-to-one relation.

Thanks for article!

Avatar

Pedro Casado | October 31st, 2009 at 12:23 am

Many thanks! Perfect ;D

Avatar

Christian | December 20th, 2009 at 9:48 pm

Hi Ryan,

I spent this beautiful Sunday (the whole day!!!) trying to understand this. Apparently I have the same trouble as you describe.

Unfortunately your idea does not solve it, any idea why?

I have a form (item), that embeds a form (item_link) that embeds a form (item_place), and I do the following within the ItemForm class:

public function bind(array $taintedValues = null, array $taintedFiles = null)
{
if (isset($taintedValues['link']['is_international']))
{
// get item place form
$item_place_form = &$this->embeddedForms['link']->embeddedForms['place'];

unset(
$item_place_form,
$this->embeddedForms['link']->embeddedForms['place'],
$taintedValues['link']['place']
);
}

return parent::bind($taintedValues, $taintedFiles);
}

Any idea what is going wrong? The if is true, so this is not the problem.

Thanks in advance,
Christian

Avatar

cifren | February 8th, 2010 at 3:12 pm

I love you !! It’s great Code !! COOOLLLL !!

Again : I Love YOU !! ;)

Avatar

Tanha | March 5th, 2010 at 10:01 am

Hi,

First of all would like to say thanks for great tutorial.

I did the way you pointed out and everything is working great, BUT one question if I would like to customize the form inside generator.yml what should I put in oreder to display the embedded form?

NOTE:
- if I don’t customize the generator.yml, that is ok and the embed form is displaying

- if I put “copy” as a field, it just display the new embed form and not the existence one, what is the clue?

Thanks

Avatar

Tanha | March 5th, 2010 at 10:32 am

Hi again,

I found that it is possible to use something like this:

$copies = new sfForm();

and then embed the forms into $copies object, and at the end wrap all the embedded forms like below:

$this->embedForm(’copies’, $copies);

Now it is possible to use inside the generator.yml something like below:

display: [ copies ]

Everything displaying as it should be, but unfortunately when want to add new entry error occurs; I think it is because I use the sfForm() inside the Propel form or you think something else?

Avatar

Ahmed El.Hussaini | March 5th, 2010 at 10:59 am

@Tanha

Unfortunatly when I wrote this article I didn’t take modifications in generator.yml under consideration but now I’m considering updating the article and I’ll sure take your note along with others under consideration.

Avatar

Tanha | March 6th, 2010 at 5:37 pm

Great. Looking forward :-)

Comment Form