Posted on Jan 7, 2009

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.

58 Comments

  • Aanchal says:

    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 :(

  • geraldo says:

    great Tutorial, thanks!

    about the plans to add multiple embeded forms with AJAX you could give laiguAdminThemePlugina try.

  • Joshe says:

    Gracias por es tutorial es exelente

  • Toms says:

    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

  • Alan says:

    Excelent post, thank you so much …
    you solved my problems, 2 1/2 days of work!

  • milon says:

    Excellent post, thank you so much …
    but i need for admin panel

  • milon says:

    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