Development

#4906 (One to Many embeded form not validating)

You must first sign up to be able to contribute.

Ticket #4906 (closed defect: fixed)

Opened 8 months ago

Last modified 8 months ago

One to Many embeded form not validating

Reported by: Stephen.Ostrow Assigned to: fabien
Priority: major Milestone: 1.2.0 RC1
Component: sfDoctrinePlugin Version: 1.2.0 DEV
Keywords: sfForm, embedForm, 1-m, sfDoctrinePlugin Cc:
Qualification: Unreviewed

Description

I've been trying to get it so I can embed x number of forms in a top form for a 1-m relationship. The simple example I have been testing is that I have a User who has many Emails.

User:
  columns:
    first_name: string(255)
    last_name: string(255)

Email:
  columns:
    user_id:  integer
    address: string(255)
  relations:
    User:

I'm embedding the form in the action for ease of seeing what's going on. I would normally move this into the form class.

  public function executeIndex(sfWebRequest $request)
  {
    $this->form = new UserForm();
    $email_form = new EmailForm();

    $this->form->embedForm('Email[0]', $email_form);

    if ($request->isMethod('post'))
    {

      $this->form->bind($request->getParameter('user'));
      if ($this->form->isValid())
      {
        $this->form->save();
      }
    }
  }


When I do this the form displays properly, but when I submit the form validation fails with the following error:

  • Unexpected extra form field named "Email".

It seems this not a limit of doctrine but a bug in symfony. I had tried previously doing this with plain sfForms and had similar results.

Google Groups Thread: http://groups.google.com/group/symfony-users/browse_frm/thread/b006b0a56696d75d

Change History

11/12/08 22:54:38 changed by Stephen.Ostrow

  • milestone changed from 1.2.0 to 1.2.0 RC1.

11/13/08 10:15:26 changed by snoerd

I think the problem is in the line

this->form->embedForm('Email[0]', $email_form);

When you use a variable name (and not an array notation) it will work:

this->form->embedForm('Email0', $email_form);

I don't think this is a defect, but a property of the sfForm system and the way PHP builds an array of all request variables.

11/13/08 20:10:21 changed by Stephen.Ostrow

J.Wage and I had a discussion about this and said we should post it as a ticket to find out whether or not you should be able to use array notation. When I try to do it with the array it displays perfectly with the field names being turned into first_form[Email][0][address] correctly. I just seems not to be validating correctly.

As for the second way you described. I tried that following http://redotheoffice.com/?p=42 This works to some extent. It creates a new related object with the correct foreign id, but upon saving the form it does not save any of the data of the foreign object (in the case of that example title and release date). The other problem with doing it like this example is you have to create a blank new object before it's actually saved. So if the user abandons the form after click new you have a blank new object.

11/16/08 21:30:24 changed by fabien

  • owner changed from fabien to Jonathan.Wage.
  • component changed from form to sfDoctrinePlugin.

11/16/08 21:55:43 changed by Stephen.Ostrow

  • owner changed from Jonathan.Wage to fabien.
  • component changed from sfDoctrinePlugin to form.

This happens when I use plain sfForm as well. I believe this is a core form problem and not just sfDoctrineForm or sfDoctrinePlugin

11/16/08 23:00:57 changed by fabien

It is possible (you need the latest 1.2 dev):

class UserForm extends BaseUserForm
{
  public function configure()
  {
    $form = new sfForm();

    foreach ($this->object->getEmails() as $i => $question)
    {
      $emailForm = new EmailForm($email);
      unset($emailForm['user_id']);
      $form->embedForm($i, $emailForm);
    }

    $this->embedForm('emails', $form);
  }
}

The trick here is that the embedForm method does not create the hierarchy from a name contains []. So, you need to create an empty form that will contain the email forms.

11/16/08 23:06:09 changed by fabien

  • status changed from new to closed.
  • resolution set to fixed.

11/17/08 04:25:25 changed by Stephen.Ostrow

  • keywords changed from sfForm, embedForm, 1-m to sfForm, embedForm, 1-m, sfDoctrinePlugin.
  • resolution deleted.
  • status changed from closed to reopened.
  • component changed from form to sfDoctrinePlugin.
  • version changed from 1.2.0 BETA2 to 1.2.0 DEV.

Ok, when I try this method it looks like it displays correctly, but upon hitting save I get the following error which indicate this is now a problem with the sfDoctrinePlugin component.

Fatal error: Call to undefined method sfForm::saveEmbeddedForms() in /srv/symfony/branches/1.2/lib/plugins/sfDoctrinePlugin/lib/form/sfFormDoctrine.class.php on line 349

To reproduce this error I starting with a clean project, create a schema file with a 1-m relation, create a crud using doctrine:init-module and then modify the FormClass? with the above code from fabien (don't forget to change the _form.php to just simple <?php echo $form ?> rather than doing each field separately. Then edit an existing record and hit save. My existing record already had 2 of the foreign items, but you will get the same result if you attempt to save a new item with no foreign relations.

11/17/08 05:49:14 changed by fabien

  • status changed from reopened to closed.
  • resolution set to fixed.

You need to update to the latest version of symfony.

11/17/08 07:59:46 changed by Stephen.Ostrow

  • status changed from closed to reopened.
  • resolution deleted.

First I wanted to thank you for all the help and say sorry because I keep reopening this ticket.

I'm now using the most up-to-date revision of branch 1.2 r13051

Once again,

#/config/doctrine/schema.yml
---
Author:
  columns:
    name: string(255)
    description: string
 
Book:
  columns:
    author_id: integer(20)
    title: string(255)
    released: integer(20)
  relations:
    Author:
      foreignAlias: Books
#/data/fixtures/fixture.yml
Author:
  kluun:
    name: Kluun
    description: Author living in Amsterdam
  winter:
    name: Leon de Winter
    description: Dutch author living in LA
  
Book:
  komt:
    title: Komt een vrouw bij de dokter
    released: 2003
    Author: kluun
  weduw:
    title: De weduwnaar
    released: 2006
    Author: kluun
    
  gym:
    title: 'God\'s gym'
    released: 2002
    Author: winter
  vijand:
    title: De vijand
    released: 2004
    Author: winter
# /lib/form/doctrine/Author.class.php
class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $form = new sfForm();
    foreach ( $this->object['Books'] as $i => $Book)
    {
      $emailForm = new BookForm($Book);
      unset($emailForm['author_id']);
      $form->embedForm($i, $emailForm);
    }

    $this->embedForm('emails', $form);
  }
}

user@localhost# ./symfony doctrine:build-all-reload 
user@localhost# ./symfony doctrine:generate-module frontend author Author
/apps/frontend/modules/author/templates/_form.php
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>

<form action="<?php echo url_for('author/'.($form->getObject()->isNew() ? 'create' : 'update').(!$form->getObject()->isNew() ? '?id='.$form->getObject()->getid() : '')) ?>" method="POST" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>
<?php if (!$form->getObject()->isNew()): ?>
<input type="hidden" name="sf_method" value="PUT" />
<?php endif; ?>
  <table>
    <tfoot>
      <tr>
        <td colspan="2">
          <?php echo $form->renderHiddenFields() ?>
          &nbsp;<a href="<?php echo url_for('author/index') ?>">Cancel</a>
          <?php if (!$form->getObject()->isNew()): ?>
            &nbsp;<?php echo link_to('Delete', 'author/delete?id='.$form->getObject()->getid(), array('post' => true, 'confirm' => 'Are you sure?')) ?>
          <?php endif; ?>
          <input type="submit" value="Save" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form ?>
    </tbody>
  </table>
</form>

When clicking save with related records listed I get the error:

500 | Internal Server Error | Doctrine_Connection_Sqlite_Exception
SQLSTATE[HY000]: General error: 20 datatype mismatch
stack trace

    * at ()
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection.php line 1075 ...
            1072.         
            1073.         $name = 'Doctrine_Connection_' . $this->driverName . '_Exception';
            1074.
            1075.         $exc  = new $name($e->getMessage(), (int) $e->getCode());
            1076.         if ( ! isset($e->errorInfo) || ! is_array($e->errorInfo)) {
            1077.             $e->errorInfo = array(null, null, null, null);
            1078.         }
    * at Doctrine_Connection->rethrowException(object('PDOException'), object('Doctrine_Connection_Statement'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection/Statement.php line 244 ...
             241.         } catch (Doctrine_Adapter_Exception $e) {
             242.         }
             243.
             244.         $this->_conn->rethrowException($e, $this);
             245.
             246.         return false;
             247.     } 
    * at Doctrine_Connection_Statement->execute(array(null, '', null, '3'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection.php line 1040 ...
            1037.         try {
            1038.             if ( ! empty($params)) {
            1039.                 $stmt = $this->prepare($query);
            1040.                 $stmt->execute($params);
            1041.
            1042.                 return $stmt->rowCount();
            1043.             } else {
    * at Doctrine_Connection->exec('UPDATE book SET id = ?, title = ?, released = ? WHERE id = ?', array(null, '', null, '3'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection.php line 640 ...
             637.               . ' WHERE ' . implode(' = ? AND ', $this->quoteMultipleIdentifier($table->getIdentifierColumnNames()))
             638.               . ' = ?';
             639.           
             640.         return $this->exec($sql, $params);
             641.     }
             642.
             643.     /**
    * at Doctrine_Connection->update(object('BookTable'), array(null, 'title' => '', null), array('id' => '3'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection/UnitOfWork.php line 520 ...
             517.                 //--
             518.             } else {
             519.                 $array = $record->getPrepared();
             520.                 $this->conn->update($table, $array, $identifier);
             521.             }
             522.             $record->assignIdentifier(true);
             523.         }
    * at Doctrine_Connection_UnitOfWork->update(object('Book'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection/UnitOfWork.php line 82 ...
              79.                             break;
              80.                         case Doctrine_Record::STATE_DIRTY:
              81.                         case Doctrine_Record::STATE_PROXY:
              82.                             $this->update($record);
              83.                             break;
              84.                         case Doctrine_Record::STATE_CLEAN:
              85.                             // do nothing
    * at Doctrine_Connection_UnitOfWork->saveGraph(object('Book'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Record.php line 1266 ...
            1263.         if ($conn === null) {
            1264.             $conn = $this->_table->getConnection();
            1265.         }
            1266.         $conn->unitOfWork->saveGraph($this);
            1267.     }
            1268.
            1269.     /** 
    * at Doctrine_Record->save(object('Doctrine_Connection_Sqlite'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Collection.php line 827 ...
             824.             }
             825.
             826.             foreach ($this->getData() as $key => $record) {
             827.                 $record->save($conn);
             828.             }
             829.
             830.             $conn->commit();
    * at Doctrine_Collection->save(object('Doctrine_Connection_Sqlite'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Connection/UnitOfWork.php line 113 ...
             110.
             111.                     // check that the related object is not an instance of Doctrine_Null
             112.                     if ( ! ($obj instanceof Doctrine_Null)) {
             113.                         $obj->save($conn);
             114.                     }
             115.                 }
             116.             }
    * at Doctrine_Connection_UnitOfWork->saveGraph(object('Author'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/doctrine/Doctrine/Record.php line 1266 ...
            1263.         if ($conn === null) {
            1264.             $conn = $this->_table->getConnection();
            1265.         }
            1266.         $conn->unitOfWork->saveGraph($this);
            1267.     }
            1268.
            1269.     /** 
    * at Doctrine_Record->save(object('Doctrine_Connection_Sqlite'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/form/sfFormDoctrine.class.php line 361 ...
             358.
             359.     $this->updateObject();
             360.
             361.     $this->object->save($con);
             362.
             363.     // embedded forms
             364.     $this->saveEmbeddedForms($con);
    * at sfFormDoctrine->doSave(object('Doctrine_Connection_Sqlite'))
      in SF_SYMFONY_LIB_DIR/plugins/sfDoctrinePlugin/lib/form/sfFormDoctrine.class.php line 179 ...
             176.     {
             177.       $con->beginTransaction();
             178.
             179.       $this->doSave($con);
             180.
             181.       $con->commit();
             182.     }
    * at sfFormDoctrine->save()
      in SF_ROOT_DIR/apps/frontend/modules/author/actions/actions.class.php line 66 ...
              63.     $form->bind($request->getParameter('author'));
              64.     if ($form->isValid())
              65.     {
              66.       $author = $form->save();
              67.
              68.       $this->redirect('author/edit?id='.$author['id']);
              69.     }
    * at authorActions->processForm(object('sfWebRequest'), object('AuthorForm'))
      in SF_ROOT_DIR/apps/frontend/modules/author/actions/actions.class.php line 48 ...
              45.     $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
              46.     $this->form = new AuthorForm($author);
              47.
              48.     $this->processForm($request, $this->form);
              49.
              50.     $this->setTemplate('edit');
              51.   }
    * at authorActions->executeUpdate(object('sfWebRequest'))
      in SF_SYMFONY_LIB_DIR/action/sfActions.class.php line 53 ...
              50.     }
              51.
              52.     // run action
              53.     return $this->$actionToRun($request);
              54.   }
              55. }
  56.
    * at sfActions->execute(object('sfWebRequest'))
      in SF_SYMFONY_LIB_DIR/filter/sfExecutionFilter.class.php line 90 ...
              87.   {
              88.     // execute the action
              89.     $actionInstance->preExecute();
              90.     $viewName = $actionInstance->execute($this->context->getRequest());
              91.     $actionInstance->postExecute();
              92.
              93.     return is_null($viewName) ? sfView::SUCCESS : $viewName;
    * at sfExecutionFilter->executeAction(object('authorActions'))
      in SF_SYMFONY_LIB_DIR/filter/sfExecutionFilter.class.php line 76 ...
              73.       return sfView::SUCCESS;
              74.     }
              75.
              76.     return $this->executeAction($actionInstance);
              77.   }
              78.
              79.   /**
    * at sfExecutionFilter->handleAction(object('sfFilterChain'), object('authorActions'))
      in SF_SYMFONY_LIB_DIR/filter/sfExecutionFilter.class.php line 42 ...
              39.     {
              40.       $timer = sfTimerManager::getTimer(sprintf('Action "%s/%s"', $actionInstance->getModuleName(), $actionInstance->getActionName()));
              41.
              42.       $viewName = $this->handleAction($filterChain, $actionInstance);
              43.
              44.       $timer->addTime();
              45.       $timer = sfTimerManager::getTimer(sprintf('View "%s" for "%s/%s"', $viewName, $actionInstance->getModuleName(), $actionInstance->getActionName()));
    * at sfExecutionFilter->execute(object('sfFilterChain'))
      in SF_SYMFONY_LIB_DIR/filter/sfFilterChain.class.php line 53 ...
              50.       }
              51.
              52.       // execute the next filter
              53.       $this->chain[$this->index]->execute($this);
              54.     }
              55.   }
  56.
    * at sfFilterChain->execute()
      in SF_SYMFONY_LIB_DIR/filter/sfCommonFilter.class.php line 29 ...
              26.   public function execute($filterChain)
              27.   {
              28.     // execute next filter
              29.     $filterChain->execute();
              30.
              31.     // execute this filter only once
              32.     $response = $this->context->getResponse();
    * at sfCommonFilter->execute(object('sfFilterChain'))
      in SF_SYMFONY_LIB_DIR/filter/sfFilterChain.class.php line 53 ...
              50.       }
              51.
              52.       // execute the next filter
              53.       $this->chain[$this->index]->execute($this);
              54.     }
              55.   }
  56.
    * at sfFilterChain->execute()
      in SF_SYMFONY_LIB_DIR/filter/sfRenderingFilter.class.php line 33 ...
              30.   public function execute($filterChain)
              31.   {
              32.     // execute next filter
              33.     $filterChain->execute();
              34.
              35.     // get response object
              36.     $response = $this->context->getResponse();
    * at sfRenderingFilter->execute(object('sfFilterChain'))
      in SF_SYMFONY_LIB_DIR/filter/sfFilterChain.class.php line 53 ...
              50.       }
              51.
              52.       // execute the next filter
              53.       $this->chain[$this->index]->execute($this);
              54.     }
              55.   }
  56.
    * at sfFilterChain->execute()
      in SF_SYMFONY_LIB_DIR/controller/sfController.class.php line 245 ...
             242.       }
             243.
             244.       // process the filter chain
             245.       $filterChain->execute();
             246.     }
             247.     else
             248.     {
    * at sfController->forward('author', 'update')
      in SF_SYMFONY_LIB_DIR/controller/sfFrontWebController.class.php line 48 ...
              45.       }
              46.
              47.       // make the first request
              48.       $this->forward($moduleName, $actionName);
              49.     }
              50.     catch (sfException $e)
              51.     {
    * at sfFrontWebController->dispatch()
      in SF_SYMFONY_LIB_DIR/util/sfContext.class.php line 159 ...
             156.    */
             157.   public function dispatch()
             158.   {
             159.     $this->getController()->dispatch();
             160.   }
             161.
             162.   /**
    * at sfContext->dispatch()
      in SF_ROOT_DIR/web/frontend_dev.php line 13 ...

I've also tried it with the form class being like this:

#/lib/form/doctrine/AuthorForm.class.php
class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    foreach ( $this->object['Books'] as  $index => $Book ) {
      $this->embedForm('book_'.$index, new BookForm($Book));
    }
    $form = new BookForm();
    $form->setWidget('author_id', new sfWidgetFormInputHidden());
    $form->setDefault('author_id', $this->object['id']);
    $this->embedForm('new_book', $form);
  }
}

This is now the closest thing I can get to it working. It displays correctly; it saves changes in existing foreign records, it will save new foreign records correctly, but only if the parent record already exists. I can obviously put a conditional in there to check if the record is new and if so not display the foreign records part, however my main goal is for a registration type form where the user would enter their name (User object) as well as home phone and work phone (Phone objects [1-m])

Any direction as always would be greatly appreciated. Sorry for the extremely long post.

P.s. Once again I'm not sure if this should now be moved back to into the sfForm component or should remain in sfDoctrinePlugin

11/17/08 21:10:02 changed by Jonathan.Wage

The error we're getting now is not related to symfony at all. I setup your exact test and it worked in mysql, but then I saw you were using sqlite and as soon as I changed to sqlite I got the same error. Still looking around and I'll let you know.

11/17/08 22:19:56 changed by Jonathan.Wage

  • status changed from reopened to closed.
  • resolution set to fixed.

(In [13082]) [1.1, 1.2] Fixes issue with embedded forms with a name that is a 0 integer (closes #4906)

11/17/08 23:36:23 changed by Stephen.Ostrow

Awesome, this now works as expected. For anyone else looking to do this sort of thing the following Form class adds an extra blank foreign object while will only be added on save.

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $form = new sfForm();
    foreach ( $this->object['Books'] as $i => $Book)
    {
      $bookForm = new BookForm($Book);
      unset($bookForm['author_id']);
      $form->embedForm($i, $bookForm);
    }

    # This is for a blank new book at the form
    $Book = new Book();
    $Book['author_id'] = $this->object['id'];
    $bookForm = new BookForm($Book);
    unset($bookForm['author_id']);
    $form->embedForm('new', $bookForm);

    # Embed all the emails in the current form
    $this->embedForm('books', $form);
  }

}

Now to come up with a good way to add that blank field when wanting to add a new foreign object or modify the way we handle the form to only add it when filled in.

11/17/08 23:42:28 changed by Jonathan.Wage

To do that I would add a new action to the edit form in admin generator which when clicked will add a blank related object and redirect back to the edit screen. For new objects, you can simply add one blank related record.

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    $form = new sfForm();

    if ($this->isNew())
    {
      $this->object['Books'][] = new Book();
    }

    foreach ( $this->object['Books'] as $i => $Book)
    {
      $bookForm = new BookForm($Book);
      unset($bookForm['author_id']);
      $form->embedForm($i, $bookForm);
    }

    # This is for a blank new book at the form
    $Book = new Book();
    $Book['author_id'] = $this->object['id'];
    $bookForm = new BookForm($Book);
    unset($bookForm['author_id']);
    $form->embedForm('new', $bookForm);

    # Embed all the emails in the current form
    $this->embedForm('books', $form);
  }

}

11/17/08 23:50:38 changed by Stephen.Ostrow

I like the

 if ($this->isNew())
    {
      $this->object['Books'][] = new Book();
    }

it's very clean. However, I don't know if I like the idea of adding a new blank foreign record because say they click "Add", it adds the blank record and directs them back, then they click cancel and don't save any changes. You now have a blank foreign record attached.

The Sensio Labs Network

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