Ask your Symfony questions! Pay money and get answers fast! (more info)

Embedded Forms Not Saving Symfony

  • SOLVED

I use Symfony 1.4.10 with Doctrine and MySQL

I was following this guide: "http://www.symfony-project.org/more-with-symfony/1_4/en/06-Advanced-Forms"

I got to a part where i can see 2 Products in the Factura, but when I hit the save button I get this error

I have this 3 tables
Factura:
actAs: { Timestampable: ~ }
columns:
escuela_id: { type: integer, notnull: true }
recibo_sistema: { type: string(255), notnull: true, unique: false }
facturador_id: { type: integer, notnull: true }
tipo_compra: { type: string(255), notnull: true, unique: false }
estudiante_id: { type: integer, notnull: true, unique: false }
aplicar_descuento: { type: string(255), notnull: true, unique: false }
pocentaje_descuento: { type: float, notnull: true, unique: false }
aplicar_IV: { type: string(255), notnull: true, unique: false }
porcentaje_IV: { type: float, notnull: true, unique: false }
subtotal: { type: float, notnull: true, unique: false }
total: { type: float, notnull: true, unique: false }
deuda_cancelada: { type: string(255), notnull: true, unique: false }
relations:
Escuela: { class: Escuela, local: escuela_id, foreign: id, foreignAlias: Facturas }
Estudiante: { class: Estudiante, local: estudiante_id, foreign: id, foreignAlias: Facturas }
Usuario: { class: sfGuardUser, local: facturador_id, foreign: id, foreignAlias: Facturas }

Producto:
actAs: { Timestampable: ~ }
columns:
nombre: { type: string(255), notnull: true, unique: false }
descripcion: { type: string(255), notnull: true, unique: false }
tipo: { type: string(255), notnull: true, unique: false }
precio_unitario: { type: float, notnull: true, unique: false }
cantidad_disponible: { type: int, notnull: true, unique: false }

FacturaProducto:
actAs: [Timestampable]
columns:
factura_id: { type: integer, primary: true }
producto_id: { type: integer, primary: true }
cantidad_comprada: { type: integer, primary: true }
relations:
Factura: { alias: Factura, foreignType: many, foreignAlias: FacturaProductos, onDelete: cascade }
Producto: { class: Producto, local: producto_id, foreignAlias: FacturaProductos }


Now, in the FacturaForm.class.php I have this
class FacturaForm extends BaseFacturaForm {

public function configure() {

$this->removeFields();

$UsuarioID = sfContext::getInstance()->getUser()->getGuardUser()->getIncremented();

$EscuelaID = sfContext::getInstance()->getUser()->getGuardUser()->getEscuelaId();

$this->widgetSchema['facturador_id']->setDefault($UsuarioID);

$this->widgetSchema['escuela_id']->setDefault($EscuelaID);

$this->validatorSchema['recibo_sistema'] = new sfValidatorString(array('required' => false));

$form = new ProductosCollectionForm(null, array(
'producto' => $this->getObject(),
'size' => 2,
));

$this->embedForm('nuevosProductos', $form);

//$this->embedRelation('FacturaProductos');
}

public function saveEmbeddedForms($con = null, $forms = null) {
if(null === $forms) {
$productos = $this->getValue('nuevosProductos');
$forms = $this->embeddedForms;
foreach ($this->embeddedForms['nuevosProductos'] as $name => $form) {
if(!isset($productos[$name])) {
unset($forms['nuevosProductos'][$name]);
}
}
}

return parent::saveEmbeddedForms($con, $forms);
}

protected function removeFields() {
unset($this['created_at'], $this['updated_at']);

$objeto = $this->getObject();
if($objeto->isNew())
unset($this['recibo_sistema']);
else
$this->widgetSchema['recibo_sistema']->setAttribute('readonly', 'readonly');
}


This is my FacturaProductoForm.class.php
I have an autocomplete so its easier for the user to find products.
class FacturaProductoForm extends BaseFacturaProductoForm {

public function configure() {
unset($this['factura_id']);

$this->useFields(array('cantidad_comprada', 'producto_id'));

$this->setWidget('cantidad_comprada', new sfWidgetFormInputText());

$this->widgetSchema['producto_id'] = new sfWidgetFormDoctrineChoice(array(
'model' => $this->getRelatedModelName('Producto'),));

//Indica a la pantalla que el la seleccion del proyecto se haga por medio del JqueryAutocompleter
$this->widgetSchema['producto_id']->setOption('renderer_class', 'sfWidgetFormDoctrineJQueryAutocompleter');
//Indica al autocompleter el modelo de objetos que va a recibir y a donde llamar para obtenerlos
$this->widgetSchema['producto_id']->setOption('renderer_options', array(
'model' => 'Producto',
'url' => url_for('@autocomplete_producto'),));

$this->widgetSchema['producto_id']->setAttribute('size', '70');

$this->widgetSchema['cantidad_comprada']->setAttribute('size', '70');

$this->validatorSchema['cantidad_comprada'] = new sfValidatorString(array(
'required' => false,));

$this->validatorSchema['producto_id'] = new sfValidatorString(array(
'required' => false,));
}

}


And I made a ProductosCollectionForm.class.php to follow the steps of the guide i mentioned at the beginning.
class ProductosCollectionForm extends sfForm {

public function configure() {
if(!$producto = $this->getOption('producto')) {
throw new InvalidArgumentException('Debe ser un objecto FacturaProducto.');
}

for ($i = 0; $i < $this->getOption('size', 2); $i++) {
$productosComprados = new FacturaProducto();
$productosComprados->Producto = $producto;

$form = new FacturaProductoForm($productosComprados);

$this->embedForm($i, $form);
}

$this->mergePostValidator(new FacturaProductoValidatorSchema());
}

}



I also made a custom Validator called FacturaProductoValidatorSchema that is used in the CollectionForm
class FacturaProductoValidatorSchema extends sfValidatorSchema {

protected function configure($options = array(), $messages = array()) {
$this->addMessage('producto_id', 'El Producto es requerido.');
$this->addMessage('cantidad_comprada', 'La Cantidad Comprada es requerida.');
}

protected function doClean($values) {
$errorSchema = new sfValidatorErrorSchema($this);

foreach ($values as $key => $value) {
$errorSchemaLocal = new sfValidatorErrorSchema($this);

// producto_id is filled but no cantidad_comprada
if($value['producto_id'] && !$value['cantidad_comprada']) {
$errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'cantidad_comprada');
}

// cantidad_comprada is filled but no producto_id
if($value['cantidad_comprada'] && !$value['producto_id']) {
$errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'producto_id');
}

// no cantidad_comprada and no producto_id, remove the empty values
if(!$value['producto_id'] && !$value['cantidad_comprada']) {
unset($values[$key]);
}

// some error for this embedded-form
if(count($errorSchemaLocal)) {
$errorSchema->addError($errorSchemaLocal, (string) $key);
}
}

// throws the error for the main form
if(count($errorSchema)) {
throw new sfValidatorErrorSchema($this, $errorSchema);
}

return $values;
}

}


When I hit the save buton I get this error
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`fundacionpiedad`.`factura_producto`, CONSTRAINT `factura_producto_factura_id_factura_id` FOREIGN KEY (`factura_id`) REFERENCES `factura` (`id`) ON DELETE CASCADE)

I think this is all the info needed to help me.

To be honest I not 100% sure what I am doing, I just followed a guide(mentioned at the beginning) that was doing what I need to do but it didn't work for me.

Please help!

Answers (2)

2011-03-28

Konrad Riedel answers:

Problem is the unknown factura_id in the subform, which don`t get updated automatically.

Something like that is missing in saveEmbeddedForms (in foreach):


$form->getObject()->setFacturaId($this->getObject()->getId());
$form->getObject()->save($con);


Javier Espinoza comments:

My new saveEmbeddedForms method in FacturaForm.class.php is this
public function saveEmbeddedForms($con = null, $forms = null) {
if(null === $forms) {
$productos = $this->getValue('nuevosProductos');
$forms = $this->embeddedForms;
foreach ($this->embeddedForms['nuevosProductos'] as $name => $form) {
$form->getObject()->setFacturaId($this->getObject()->getId());
$form->getObject()->save($con);

if(!isset($productos[$name])) {
unset($forms['nuevosProductos'][$name]);
}
}
}

return parent::saveEmbeddedForms($con, $forms);
}


But when I save i get this error:
Fatal error: Call to undefined method sfFormFieldSchema::getObject() in /XXXXX/XXXXX/XXXXX/XXXXX/lib/form/doctrine/FacturaForm.class.php on line 42

In this line of code:
$form->getObject()->setFacturaId($this->getObject()->getId());


Konrad Riedel comments:

ok, my fix was for Propel ORM

perhaps you should this recipe (6. embedMergeForm) here:

http://itsmajax.com/2011/01/29/6-things-to-know-about-embedded-forms-in-symfony/


Javier Espinoza comments:

I guess you are the winner because you pointed out the problem. Although the fix was this:

In the Factura form I renamed the variable I was sending to the CollectionForm to factura, because that is what it is, I had that it was a product:
$form = new ProductosCollectionForm(null, array(
'factura' => $this->getObject(),
'size' => 1,
));


And in the ProductosCollectionForm.class.php I also renamed the variable to factura and now it saves:
class ProductosCollectionForm extends sfForm {

public function configure() {
if(!$factura = $this->getOption('factura')) {
throw new InvalidArgumentException('Debe ser un objecto Factura.');
}

for ($i = 0; $i < $this->getOption('size', 2); $i++) {
$nuevoProducto = new FacturaProducto();
$nuevoProducto->Factura = $factura;

$form = new FacturaProductoForm($nuevoProducto);

$this->embedForm($i, $form);
}

$this->mergePostValidator(new FacturaProductoValidatorSchema());
}

}


The only problem I have now is that when I go to edit the Factura, the fields of Productos are empty, they dont fill up. But in the database they are saved correctly.

Other thing I want to do with this form is to make it Dynamic, so by default there is a field to add one product, but the user should be able to add as many products as they wish. But I have no idea how to do this.

Any help?

2011-03-29

Serge HARDY answers:

could you be more precise about what you are trying to do?

if you just want to save relations in embedded forms, there are much less complicated ways to achieve this!

are you using innodb or myisam?