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.

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
- Modify the CategoryForm to include embedded forms for all available subcategories of the current category.
- Modify the widget of the name field in the subcategory
- Modify the CategoryForm to add a new blank subcategory form
- Override the bind method of the sfForm class to skip saving and validating the new subcategory form if the name field was left blank
- 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.
Hey, Its an awesome blog.
The trouble I am having is that I want to have a validator on the embedded forms. My requirement is that the values in the embedded forms can’t be duplicated.How can it be achieved??
ur help will be really appreciated.Stuck on it since 4 days
great Tutorial, thanks!
about the plans to add multiple embeded forms with AJAX you could give laiguAdminThemePlugina try.
Gracias por es tutorial es exelente
Thanks a lot for your work !
But I moved the call to the controller into the actions.class.php, because is recommanded, like :
// in module/actions/actions.class.php
executeEdit(sfWebRequest $request){
$options = array(‘controller’ => $this->getController());
$this->form = new CentreJourForm($this->centre_jour, $options );
}
//in my form
configure(){
$controller = $this->getOption(‘controller’);
….
And in your widget i have add an option called controller.
I have made the same thing for a relationship many to many if you are interested.
Example with a schema:
company[idC, name]
charter[immat#, idC#, dateChar] // Doctrine doesn’t understand this notation because there are an additional column
Plane[immat, type]
(Sorry for my english I’m french)
@toms nice work, thanks
Excelent post, thank you so much …
you solved my problems, 2 1/2 days of work!
Excellent post, thank you so much …
but i need for admin panel
thanks for your post..
i have follow your descriptions . see my Schema and form class definition .
TestQuestion:
actAs: [ Timestampable,DmSortable ]
columns:
name: { type: string(200), notnull: true}
is_active: { type: boolean, notnull: true, default: false }
AnswerOption:
actAs: [ Timestampable,DmSortable ]
columns:
test_id: { type: integer, notnull: true}
options: { type: string(255), notnull: true }
relations:
TestQuestion:
class: TestQuestion
onDelete: CASCADE
local: test_id
foreign: id
foreignAlias: AnswerOptions
foreach ($this->getObject()-> getAnswerOptions() as $answerOption) {
// create a new subcategory form for the current subcategory model object
$answerOption_form = new AnswerOptionForm($answerOption);
// embed the subcategory form in the main category form
$this->embedForm(‘AnswerOption’.$answerOption->getId(), $answerOption_form);
// set a custom label for the embedded form
$this->widgetSchema['answerOption'.$answerOption->getId()]->setLabel(‘Subcategory: ‘.$answerOption->getOptions());
// 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
$answerOption_form = new AnswerOptionForm();
// embed the subcategory form in the main category form
$this->embedForm(‘answerOption’, $answerOption_form);
// set a custom label for the embedded form
$this->widgetSchema['answerOption']->setLabel(‘New answerOption’);
}
class AnswerOptionForm extends BaseAnswerOptionForm
{
public function configure()
{
unset($this['created_at'], $this['updated_at'], $this['test_id'], $this['position']);
}
}
input field load successfully but when i try to add data its shown the following error
”
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`learning`.`answer_option`, CONSTRAINT `answer_option_test_id_test_question_id` FOREIGN KEY (`test_id`) REFERENCES `test_question` (`id`) ON DELETE CASCADE)
”
plz any one who can help me for solving this problem?
thank advance