Development

#5867 (Embedded forms do not call save() or doSave() on the form)

You must first sign up to be able to contribute.

Ticket #5867 (new defect)

Opened 1 year ago

Last modified 8 months ago

Embedded forms do not call save() or doSave() on the form

Reported by: Jonathan.Wage Assigned to: fabien
Priority: minor Milestone:
Component: other Version: 1.2.7
Keywords: Cc: Joaquin.Bravo, Dannyrulez, BigBadBassMan
Qualification: Unreviewed

Description

This is an issue because the generated forms for your models override the doSave() method and call save*List() functions which are generated for each many to many relationship that exists on the model. Since the many 2 many relationships are saved by calling save() on the embedded form model object the many 2 many relationships are never updated. To fix the issue in our project I had to override the saveEmbeddedForms() method in BaseFormDoctrine?.class.php with the following code:

  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], $this->taintedValues[$key]);
        $form->doSave($con);
        $form->saveEmbeddedForms($con);
      }
      else
      {
        $this->saveEmbeddedForms($con, $form->getEmbeddedForms());
      }
    }
  }

You can see I bind the data to the embedded form then call doSave(). I don't call save() b/c it would then wrap everything in more unnecessary transations.

Change History

(follow-up: ↓ 2 ) 02/17/09 13:02:36 changed by nervo

This is a really annoying bug... Anyway, there is a little typo in the jonhattan's "patch" :

$form->bind($this->values[$key], $this->taintedValues[$key]);

should be replaced by something like :

$form->bind($this->taintedValues[$key], $this->taintedFiles);

which correspond to the bind() method parameters orders. But it does not resolve the problem of uploading files in embedded forms. See http://stereointeractive.com/blog/2008/12/23/symfony-12-upload-a-file-inside-an-embedded-form/

(in reply to: ↑ 1 ) 02/17/09 15:43:40 changed by nervo

MUCH better :

$form->bindAndSave($this->taintedValues[$key], $this->taintedFiles, $con);

03/17/09 15:10:33 changed by tyx

Hello,

Apparently this patch doesn't fix this issue on my side.

Is there any other solution?

Thanks

03/28/09 17:50:17 changed by FabianLange

  • milestone changed from 1.2.5 to 1.2.6.

04/01/09 17:29:37 changed by nervo

I have found something else. If your primary form contains some file inputs (ie. sfWidgetFormInputFile), as $taintedFiles is not an indexed array like $taintedValues, your embedded forms also received the new files fields. That can throw an exception like "Unexpected extra form field named "file".", because your embedded forms does not have file input validators. BUT this exception is silently ignored in the process, and cause the doSave() method of the embedded form to be never called. doSave() is the place where the object relations are handled (1:n, 1:1, n:n) via additionnal methods.

So you can say something like : "if i have a file input in my primary forms, theres is chance that the relations of my embedded forms will not be saved"

The only workaround i found is to add a :

$this->validatorSchema->setOption('allow_extra_fields', true);

to all embedded forms.

Tyx, maybe that's your case ?

04/09/09 17:02:01 changed by zhuljen

For our case we had to use code below. Because in jonhattan's "patch" if there is a Propel/Doctrine based form below a simple sfForm, then this deep embedded form did not receive it's taintedValues. Right values for this embedded forms really is in nested arrays. Maybe someone will find it useful. Took me couple of days to figure it out :)

  public function saveEmbeddedForms($con = null, $forms = null, $taintedValues = null, $taintedFiles = null)
  {
    if (is_null($con))
    {
      $con = $this->getConnection();
    }

    if (is_null($forms))
    {
      $forms = $this->embeddedForms;
    }

    if (is_null($taintedValues))
    {
        $taintedValues = $this->taintedValues;
    }

    if (is_null($taintedFiles))
    {
        $taintedFiles = $this->taintedFiles;
    }

    foreach ($forms as $key => $form)
    {
      if ($form instanceof sfFormPropel)
      {
          $form->bindAndSave($taintedValues[$key], $taintedFiles);
      }
      else
      {
        $this->saveEmbeddedForms($con, $form->getEmbeddedForms(), $taintedValues[$key], $taintedFiles);
      }
    }
  }

05/01/09 23:02:43 changed by FabianLange

  • milestone deleted.

05/06/09 03:44:28 changed by Joaquin.Bravo

  • cc set to Joaquin.Bravo.

(in reply to: ↑ description ) 05/12/09 16:05:00 changed by drahpal

You may add the following just before the bind call if you set any CSRF protection

unset($form[self::$CSRFFieldName]);

06/10/09 20:36:57 changed by Dannyrulez

  • cc changed from Joaquin.Bravo to Joaquin.Bravo, Dannyrulez.

I'm having the same problem than everyone here.

06/12/09 11:28:51 changed by BigBadBassMan

  • cc changed from Joaquin.Bravo, Dannyrulez to Joaquin.Bravo, Dannyrulez, BigBadBassMan.
  • version changed from 1.2.4 to 1.2.7.

With latest 1.2.8-dev version (r19193), the embedded forms get saved, but do net receive the ID of the master form, so m2m relations are missing one side.

There is no difference in using the proposed function overrides, or the 1.2.8-dev stock code.

06/12/09 16:57:26 changed by Dannyrulez

This is what I do to save the M2M relations from and embedded forms:

I make my changes to the updateObject() of the embedded form. It look like this:

The $object is "MusicSong?"

  public function updateObject($values = null)
  {
    $object = parent::updateObject($values);

    if ($values['id'] > 0)
    {
      $c = new Criteria();
      $c->add(MusicSongArtistPeer::SONG, $object->getPrimaryKey());
      MusicSongArtistPeer::doDelete($c);

      $object->setNew(false);
    }
    else
    {
      $object->setNew(true);
    }

    foreach ($values['music_song_artist_list'] as $artistId)
    {
      $obj = new MusicSongArtist();
      $obj->setArtist($artistId);

      $object->addMusicSongArtist($obj);
    }

    return $object;
  }

This is my schema for make you an idea of what is happening:

music_artist:
  id:
  name:       { type: varchar(255) }

music_song:
  id:
  name:         { type: varchar(255) }
  number:       { type: tinyint }

music_song_artist:
  song:              { type: integer, primaryKey: true, required: true, foreignTable: music_song, foreignReference: id, onDelete: cascade }
  artist:            { type: integer, primaryKey: true, required: true, foreignTable: music_artist, foreignReference: id, onDelete: cascade }

The Sensio Labs Network

Since 1998, Sensio Labs has been promoting the Open-Source software movement by providing quality web application development, training, consulting.
Sensio Labs also supports several large Open-Source projects.