Posted on Dec 20, 2008

How to embed forms in symfony 1.2 admin generator

Symfony 1.2 added a lot of new and exciting new features to an already great and powerful PHP framework. One of those features is the ability to embed a form into another. So what excatly does this mean?

First consider the model shown in the diagram below:
company_contact

As shown in the above figure, there is a one-to-one relationship between the company and the contact model.

Here is the schema.yml:

propel:
  _attributes:
    package: lib.model
    defaultIdMethod: native
  company:
    _attributes: { phpName: Company }
    id: { type: INTEGER, size: '11', primaryKey: true, autoIncrement: true, required: true }
    name: { type: VARCHAR, size: '255', required: true }
    contact_id: { type: INTEGER, size: '11', required: false, foreignTable: contact, foreignReference: id, onDelete: SET NULL, onUpdate: RESTRICT }
    _indexes: { company_FI_1: [contact_id] }
  contact:
    _attributes: { phpName: Contact }
    id: { type: INTEGER, size: '11', primaryKey: true, autoIncrement: true, required: true }
    first_name: { type: VARCHAR, size: '255', required: true }
    last_name: { type: VARCHAR, size: '255', required: true }
    company_id: { type: INTEGER, size: '11', required: false, foreignTable: company, foreignReference: id, onDelete: SET NULL, onUpdate: RESTRICT }
    _indexes: { contact_FI_1: [company_id] }

Note that both foriegn keys are not required, that is a must for us to be able to embed the Contact form in the Company form.

Now that we have the schema ready, let us build the sql schema, models, forms, filters, and create the tables.

$ php symfony propel:build-sql
$ php symfony propel:build-model
$ php symfony propel:build-forms
$ php symfony propel:build-filters
$ php symfony propel:insert-sql --env=dev

Now let us generate a module using symfony’s admin generator and see how the generator will handle this relationship.

$ php symfony propel:generate-admin backend Company

By browsing to the category module we just created above we can see the below figure:
company_1

Now if we click on the “New” link we get the following screen:
company_2

Notice that the “contact_id” foreign column was interpreted by the admin generator as a drop down list. Of course that is not what we had in mind, we want to be able to add a new “Company” along with it’s “Contact”. Thanks to symfony 1.2 ability to embed forms, this can be done very easily.

First open “CompanyForm.class.php” file and edit the configure method to match the following:

public function configure() {

	// get Contact model object
	$contact = $this->getObject()->getContact();

	// contact object is null
	if (is_null($contact)) {

		// create a new Contact object
		$contact = new Contact();

		// set the copmany of the newly created object to the current company
		$contact->setCompany($this->getObject());

		// set the contact of the current company
		$this->getObject()->setContact($contact);

	}

	// create a new contact form
	$contact_form = new ContactForm($contact);

	// embed the contact form in the current company form
	$this->embedForm('contact', $contact_form);

	// remove the contact_id from the form
	unset($this['contact_id']);

}

Next, open the ContactForm.class.php and edit as follows:

public function configure() {

	unset($this['company_id']);

}

Now modify the Company.php model class to delete the contact while delete the company

public function delete(PropelPDO $con = null) {
	$this->getContact()->delete($con);
	parent::delete($con);
}

And last but not least, edit the generator.yml to match the following(this step is optional):

generator:
  class: sfPropelGenerator
  param:
    model_class:           Company
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          company
    with_propel_route:     1

    config:
      actions: ~
      fields:
          contact_id: { label: Company Contact }
      list:
          display: [=name, contact]
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

Now go to the add new “Company” screen and you should see the following:
company_3

Try adding, editing, and deleting some records now to make sure it’s working.
company_4
company_5

27 Comments

  • forganna says:

    easyyy
    hope keeping us up to date always using this easy explanation

  • Hebatollah Moaz says:

    thanks for the article i find it easy to get and very helpfull…

  • NiKo says:

    Nice tuto, kudos :)

  • jukea says:

    great! but now, let’s do the same for a 1 to many relation … : My companies have many contacts, so that would be better for me!

  • @jukea: The one-to-many relationship will automatically be implmemented by symfony’s admin generator in the form of a select many form elemeny. If you’d like more options, try reading this article

  • jukea says:

    @ahmed : yes, but I’m more looking into something like in-place editing of contacts inside a company, like :

    http://redotheoffice.com/?p=42

  • @jukea: I see, I’ll look into it and update the article ASAP.

  • Gopal says:

    thanks for the article i find it easy to get and very help full me …

  • @Gopal: You’re most welcome, I’m glad that I can help. By he way if you need any help regarding symfony please don’t hesitate in contacting me.

  • John says:

    Hi,

    This is great and has helped me a lot, however I’m having one issue…

    This line:
    # // get Contact model object
    # $contact = $this->getObject()->getContact();

    Modified to call the model object I want, fails with the error Call to undefined method BaseCompany::getContact (I’ve modified names back to suit your example rather than my own code)…

    If I just comment it out and the if structure following, leaving everything else, the form displays fine, validates, submits etc, but when you bring it back up, it can’t populate the embedded form with the data to math the parent form, presumably because the above, which I’ve commented out, is what controls this.

    So how can you get it to call on a model outside of itself (ie call the Contact model from the Company model) when you’re using $this?? (There is probably just something I am missing / don’t understand :) )

    Thanks,
    JM

  • John says:

    Sorry I should add I am using it in a non admin-generated frontend, but I don’t believe for this problem it makes a difference?

  • @John: I’m not quite sure what the problem is, but I think it has to to with your MySQL tables, are they MYISAM or InnoDB?

  • John says:

    @Ahmed El.Hussaini: I had the model wrong, no foreign key from the main table to the ‘child’ table I wanted to embed… I thought I could do it without this (just a link back the other way) but obviously not! Thanks :)

  • roland says:

    snip
    Note that both foriegn keys are not required, that is a must for us to be able to embed the Contact form in the Company form.
    /snip

    should that be “foreign keys *are* required”?

  • Joao Correia says:

    Hello !
    Very nice tutorial. Hey by the way, can you give any tips on managing many-to-many relations on admin generator ?

    I have
    Portfolio

    Tags

    portfolio_tags

    and want to manage the tags associated inside my portfolio form.

    Thanks
    Joao

  • germana says:

    Hi!!

    I have the same problem that jhon:
    “Call to undefined method BaseCompany::getContact”

    But my tables are: Denuncia and Denunciado, i want to embed Denunciado into Denuncia, so my model have the foreing keys on each table, and the exact error is:

    “Call to undefined method BaseDenuncia::getDenunciado”

    and my tables are InnoDB..

    What could be my problem???

  • ojie43 says:

    I get error when i add __toString function in lib/model/contac.php

    Catchable fatal error: Method Contact::__toString() must return a string value in E:\Web\company\cache\backend\dev\modules\autoCompany\templates\_list_td_tabular.php on line 5

    plese help this..

    thanks

  • @ojie43: Open lib/model/Contact.php and override the __toString() function.

    ex:

    public function __toString() {
      return $this->getFullName();
    }
    
  • ojie43 says:

    thanks…
    now, I can use it to my form..
    sory my english not good

    Now, I have probel with year range ini my birdday field, I only get year range 2004- 2014..

    why I can get more year range ?, example 1950-2000
    please help me
    thanks before

  • sudhir says:

    Great tutorial, This is exactly what I was looking for since last two days. I was not able to save my embedded for.
    You solved my problem.

    Thanks

  • Norbert says:

    Is there a version for users of the doctrine orm available ?
    i have troubles implementing this with doctrine …
    maybe you could be so kind and describe the differences / modifations neccessary when ussing doctrine. thx a lot!!!!

  • lukis says:

    I have problem i am struggling with for some time now:
    Is there a way to save file with filename same as primary key of inserted record?
    For example such method works only for updates couse primary id already exists:

    public function generateImageFilename(sfValidatedFile $file)
    {
    return $this->getId().$file->getExtension($file->getOriginalExtension());
    }

    Do u have any solution for situation when new record is being saved?

  • stereoscott says:

    This is a great post. I really appreciate the screen shots. It’s this type of functionality that helps illustrate why the forms framework is what it is. Although sometimes I really miss the simplicity of the old forms functionality in 1.0, embedding forms is hard to compete with. For @Norbert, besides a different schema, remember that with doctrine “$contact = $this->getObject()->getContact();” will always return a contact object, even if one does not exist in the database.

  • Roman Piekarski says:

    Hello,
    Your schema example is not correct for postgresql databese.
    Correct:
    propel:
    _attributes:
    package: lib.model
    defaultIdMethod: native
    contact:
    _attributes: { phpName: Contact }
    id: { type: INTEGER, primaryKey: true, autoIncrement: true, required: true }
    first_name: { type: VARCHAR, size: 255, required: true }
    last_name: { type: VARCHAR, size: 255, required: true }
    company_id: { type: INTEGER, required: false }
    # foreign key is creating as an “alter table” on the end of *.sql file
    _foreignKeys:
    -
    foreignTable: company
    onDelete: cascade
    references:
    – { local: company_id, foreign: id }
    company:
    _attributes: { phpName: Company }
    id: { type: INTEGER, primaryKey: true, autoIncrement: true, required: true }
    name: { type: VARCHAR, size: 255, required: true }
    contact_id: { type: INTEGER, required: false }
    _foreignKeys:
    -
    foreignTable: contact
    onDelete: cascade
    references:
    – { local: contact_id, foreign: id }

    Your schema is creating constraints in create table so it gives a lot of conflicts beetween tables.

    V

  • Roman Piekarski says:

    Better solution for:
    public function configure() {

    unset($this['company_id']);
    }

    .. is unset company_id directly in CompanyForm.class.php:

    // create a new contact form
    $contact_form = new ContactForm($contact);
    unset($contact_form['company_id']);

    Why? When you create standalone module for contact you can show company name or other informations related to.
    Am i right? ;)

  • barış says:

    wonderful post! Thanks a lot!