Archives


Master Symfony2 fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Symfony hosting done right

ServerGrove, outstanding support at the right price for your Symfony hosting needs.
servergrove.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Fabien Potencier
Call the expert: Nested forms - A real implementation
by Fabien Potencier – November 10, 2008 – 18 comments

As announced in a previous post, symfony 1.2 is able to automatically save objects from deep nested forms. I gave a simple example in the announcement post, but some people asked me for a real project example. So here it is.

The project

Let's take a classified website. The website is composed of ads. Each ad is described with some generic information (like a title, a description, ...) and some more specific ones based on the ad type (like the number of beds or the year of construction for a house, or the make, the model, or the color for a car).

So, the model schema is composed of a main demo_ad table and some type tables (demo_ad_type_house and demo_ad_type_car) to host the detailed information for the ad:

# config/schema.yml
propel:
  demo_ad:
    id:              ~
    title:           { type: varchar(255), required: true }
    description:     { type: longvarchar, required: true }
    price:           float
    type:            { type: varchar(255), required: true }
 
  demo_ad_type_house:
    id:              ~
    demo_ad_id:      { type: integer, foreignTable: demo_ad, foreignReference: id, required: true }
    square_footage:  { type: integer, required: true }
    nb_beds:         { type: integer, required: true }
    nb_baths:        { type: integer, required: true }
    year:            { type: varchar(255), required: true }
 
  demo_ad_type_car:
    id:              ~
    demo_ad_id:      { type: integer, foreignTable: demo_ad, foreignReference: id, required: true }
    make:            { type: varchar(255), required: true }
    model:           { type: varchar(255), required: true }
    year:            { type: varchar(255), required: true }
    color:           { type: varchar(255), required: true }
 

To make it more real, let's add some initial data:

# data/fixtures/ads.yml
DemoAd:
  house_1:
    title: Farm
    description: |
      250 acres with irrigation, several shares of water rights, creek, spring and a well.
    price: 2225000
    type: house
  car_1:
    title: Honda Accor
    description: |
      Honda accord fully loaded, power windows, sun roof, new timing belt, new brakes, a/c , cd player.
    price: 6900
    type: car
 
DemoAdTypeHouse:
  house_1_desc:
    demo_ad_id: house_1
    square_footage: 4500
    nb_beds:  4
    nb_baths: 3
    year:     1910
 
DemoAdTypeCar:
  car_1_desc:
    demo_ad_id: car_1
    make:  honda
    model: accor
    year:  2002
    color: green
 

Project Initialization

If you want to follow along, create a new symfony project the usual way:

$ mkdir classifieds
$ cd classifieds
$ symfony generate:project classifieds

Then create the two files we have described above (config/schema.yml and data/fixtures/ads.yml), configure the database, build the model, and feed the database with the initial data:

$ ./symfony configure:database "mysql:host=localhost;dbname=classifieds" root mYsEcret
$ mysqladmin -uroot -pmYsEcret create classifieds
$ ./symfony propel:build-all-load

In this post, we will create the backend of the application to demonstrate all the power of the new admin generator bundled with symfony 1.2.

$ ./symfony generate:app --escaping-strategy=on --csrf-secret=Unique$ecret1 backend

Then create the backend module to list, create, edit, and delete the ads:

$ ./symfony propel:generate-admin backend DemoAd

The propel:generate-admin automatically adds a route to the routing.yml configuration file.

The ad module is now ready to be used as shown on these screenshots:

Ads list action

Ads edit action

Project Customization

As you can see for yourself on the screenshots, it is not quite finished yet.

The type column, which is stored as a string in the database, need to be changed from an input text box to a select box in the form.

Instead of hardcoding the possible types in the form, declare them as a simple property of the DemoAdPeer class, so it can be reused later on in the project:

// lib/model/DemoAdPeer.php
class DemoAdPeer extends BaseDemoAdPeer
{
  static public $types = array('house' => 'house', 'car' => 'car');
}
 

It is now easy to change the type widget of DemoAdForm from a text input to a choice:

// lib/form/DemoAdForm.class.php
class DemoAdForm extends BaseDemoAdForm
{
  public function configure()
  {
    $this->widgetSchema['type'] = new sfWidgetFormChoice(array('choices' => DemoAdPeer::$types));
    $this->validatorSchema['type'] = new sfValidatorChoice(array('choices' => array_keys(DemoAdPeer::$types)));
  }
}
 

Ads edit action with type

When editing an ad, we want to edit the main information but also the detailed ones. So, we need to embed the specific description form in the main form.

As there is no database relation between the ad and the type, we need to create a custom method in the DemoAd model to get the DemoAdType* object:

// lib/model/DemoAd.php
class DemoAd extends BaseDemoAd
{
  public function getTypeObject()
  {
    // if no type has been defined yet, there is no type object
    if (!$this->getType())
    {
      return null;
    }
 
    // the type class depends on the ad type
    $class = sprintf('DemoAdType%s', ucfirst($this->getType()));
    $peer = constant($class.'::PEER');
 
    // get the type object related to the current ad
    $criteria = new Criteria();
    $criteria->add(constant($peer.'::DEMO_AD_ID'), $this->getId());
 
    // if there is none, create a new one associated with this ad
    if (is_null($type = call_user_func(array($peer, 'doSelectOne'), $criteria)))
    {
      $type = new $class();
      $type->setDemoAd($this);
    }
 
    return $type;
  }
}
 

Now for the fun part. Embed the type form into the main ad form if there is one:

// lib/form/DemoAdForm.class.php
class DemoAdForm extends BaseDemoAdForm
{
  public function configure()
  {
    $this->widgetSchema['type'] = new sfWidgetFormChoice(array('choices' => DemoAdPeer::$types));
    $this->validatorSchema['type'] = new sfValidatorChoice(array('choices' => array_keys(DemoAdPeer::$types)));
 
    // only embed if there is a type object (edit vs create)
    if ($this->getObject()->getType())
    {
      $this->embedForm('desc', $this->getTypeForm());
    }
  }
 
  public function getTypeForm()
  {
    $class = sprintf('DemoAdType%sForm', ucfirst($this->object->getType()));
 
    return new $class($this->object->getTypeObject());
  }
}
 

If you refresh your browser now, you will have an exception because the embedded form has a select box to choose the ad to which it is linked to. To render this select box, symfony needs a text representation of an Ad:

class DemoAd extends BaseDemoAd
{
  public function __toString()
  {
    return $this->getTitle();
  }
 
  // ...
}
 

Ads edit action with the embed form

As we don't want people to be able to change the link between the ad and the type, we need to disable the corresponding widget in the type form classes:

// lib/form/DemoAdTypeCarForm.class.php
class DemoAdTypeCarForm extends BaseDemoAdTypeCarForm
{
  public function configure()
  {
    unset($this['demo_ad_id']);
  }
}
 
// lib/form/DemoAdTypeHouseForm.class.php
class DemoAdTypeHouseForm extends BaseDemoAdTypeHouseForm
{
  public function configure()
  {
    unset($this['demo_ad_id']);
  }
}
 

Final ads edit action with the embed form

That's all there is to it. You can now change the main ad columns or the specific ones and when saving the form, symfony will save everything back to the database. And this has been possible with the admin generator without customizing anything. It just works!

Final ads edit action with the embed save

Final ads edit action with the embed form errors

Of course, you will find some edge cases that need to be worked on, but hopefully you are now able to customize it further.

Add a Comment

You must be connected to post a comment.

Comments RSS

  • gravatar
    #1 Éric Rogé said on the 2008/11/10 at 10:01
    So great !
    First I was a little confused with the new form system, but now I begin to find it really tasty.

    Do you already have a date for the next beta/release condidate ?

    Ho, and I think I've found a small typo :
    $peer = constant($class.'::PEER');
    It maybe sould be $peer = constant($class.'Peer');
  • gravatar
    #2 Geoffrey said on the 2008/11/10 at 10:22
    Éric, if you look at your Base* model classes, you will see that they define a PEER class constant which value is the model's peer class, so there's no typo here :-)
  • gravatar
    #3 FX Poster said on the 2008/11/10 at 10:57
    Filters in the Admin Page don't work...
  • gravatar
    #4 Ian Dominey said on the 2008/11/10 at 11:24
    Another typo (I think).

    Shouldn't

    if (is_null($desc = call_user_func(array($peer, 'doSelectOne'), $criteria)))

    actually be

    if (is_null($type = call_user_func(array($peer, 'doSelectOne'), $criteria)))
  • gravatar
    #5 Krešo Kunjas said on the 2008/11/10 at 11:24
    you have a typo in lib/model/DemoAd.php:

    if (is_null($desc = call_user_func(array($peer, 'doSelectOne'), $criteria)))

    should be:

    if (is_null($type = call_user_func(array($peer, 'doSelectOne'), $criteria)))
  • gravatar
    #6 puentesdiaz said on the 2008/11/10 at 13:24
    Great sample!!

    Now, i have some election to make
    Propel or Doctrine, right? It's not possible work with both...

    Your sample always use Propel... But Doctrine is now the first recommend, right?

    In the future, symfony work better with Doctrine ?

    For now, i cann't make this choose...
    I donn't what usr for generate my schema yet
  • gravatar
    #7 Ice_j7 said on the 2008/11/10 at 15:02
    Incredibly useful!
  • gravatar
    #8 Rob said on the 2008/11/10 at 15:43
    You need to change "# config/fixtures/ads.yml" to "# data/fixtures/ads.yml".
  • gravatar
    #9 Fabien said on the 2008/11/10 at 22:54
    I have fixed the typos. Thanks.
  • gravatar
    #10 Stephen Ostrow said on the 2008/11/11 at 00:11
    I know this might be an "edge case", but I think it will be helpful to a lot of people besides me. Could you please make an example of putting multiple on one form in a top form in a 1-n relationship?
    Example being a person has many addresses.

    Top form of User, second form of Address. How can I embed 4 Address form in the top User form? I've had a lot of problems doing this so far. I can get it to display but the validation doesn't seem to work.

    Thanks.
  • gravatar
    #11 mitjad said on the 2008/11/12 at 12:35
    I tried the example with propel:generate-module and inserted embeded fields seperatly.

    Not like:
    <?php echo $form['desc'] ?>

    but like:
    <?php echo $form['desc']['make'] ?>
    <?php echo $form['desc']['model'] ?>
    ...


    When I create new form the embeded fields are saved, but when I try to change the make, model.. fields are not saved.
    With <?php echo $form['desc'] ?> it wotks ok?
  • gravatar
    #12 Leonardo said on the 2008/11/15 at 06:44
    A great example, I think this is the answer for the commonly question about sfGuardUser and multiple profile.

    Congrats.
  • gravatar
    #13 Murena said on the 2008/11/21 at 17:54
    I also would like to see an example of embedding forms from a 1-n relationship like Stephen Ostrow said.
  • gravatar
    #14 Gorka said on the 2008/11/23 at 02:55
    Yet another vote for the 1:N relationship example.

    It would be particularly useful to know how handle adding or removing sub-elements with javascript on the form.
  • gravatar
    #15 Sébastien Wauquier said on the 2008/11/23 at 20:06
    As Stephen,Murena and Gorka, "It would be particularly useful to know how handle adding or removing sub-elements with javascript on the form."; even though this is yet great job !
    Cheers !
  • gravatar
    #16 benji07 said on the 2008/12/10 at 18:12
    what's happend if you add object instead of editing it?
  • gravatar
    #17 Junho said on the 2008/12/30 at 05:48
    Thank you Fabien. Symfony is changing my life!

    I'm also looking forward to see another example of dynamically adding/removing sub-elements.

    Is subscribing to this blog posts feed enough to get notice about it?
  • gravatar
    #18 Enrique said on the 2008/12/31 at 18:25
    I've written a post about 1:N relationships in one form. If someone is interested visit http://dev.markhaus.com/blog/2008/12/a-way-to-edit-n-1-relationships-in-one-form-with-symfony-12/
    and please, give some feedback.