ZF-4370: Zend_Form::getValues returns invalid rendered array when using subforms

Description

When using subforms in Zend_Form, getValues method returns subform values with empty key. In zf 1.5 it looks like this: array('value' => 1, 'value2' => 2, 'subform' => array(...)) In zf 1.6 it looks like this: array('value' => 1, 'value2' => 2, '' => array(...))

Comments

See the block below from Zend_Form::getValues().


foreach ($this->getSubForms() as $key => $subForm) {
    $fValues = $this->_attachToArray($subForm->getValues(true), $subForm->getElementsBelongTo());
    $values = array_merge($values, $fValues);
}

The issue described is caused by $subForm->setElementsBelongTo() never being called, which results in the call to $subForm->getElementsBelongsTo() returning a null value. The subsequent array_merge() call overrides the existing blank index on each iteration of the loop with the array from the last subform for which values were fetched.

Maybe the return value of $subForm->getElementsBelongsTo() should be checked prior to the loop and $subForm->getName() used in the event that it's empty? Either that or just using $subForm->getName() outright would be my suggestion. When adding a subform to a form, it has to be present and unique anyway.

Any news on this issue ? This is a major bug when having multible subforms, beacuse you only get the values from one of the subforms :S

plz let us know!

I believe this is already fixed in recent versions of ZF. I performed the following test just now:


$sub = new Zend_Form_SubForm();
$sub->setName('subform')
    ->addElement('text', 'foo')
    ->addElement('text', 'bar');

$form = new Zend_Form();
$form->addElement('text', 'foo')
     ->addElement('text', 'bar')
     ->addSubForm($sub, 'subform');

$form->foo->setValue('FOO');
$form->bar->setValue('BAR');
$form->subform->foo->setValue('FOO');
$form->subform->bar->setValue('BAR');

$values = $form->getValues();
var_export($values);

I get the expected output:


array (
  'foo' => 'FOO',
  'bar' => 'BAR',
  'subform' => 
  array (
    'foo' => 'FOO',
    'bar' => 'BAR',
  ),
)

I'm closing this for now. If you have a reproduce case that displays the issue you're reporting using current trunk, please re-open the issue and provide it.

And just as a followup, I did a test with multiple sub forms and nested subforms as well:


$sub = new Zend_Form_SubForm();
$sub->setName('subform')
    ->addElement('text', 'foo')
    ->addElement('text', 'bar');

$sub2 = new Zend_Form_SubForm();
$sub2->setName('subform2')
     ->addElement('text', 'foo')
     ->addElement('text', 'bar');

$sub3 = new Zend_Form_SubForm();
$sub3->setName('subform3')
     ->addElement('text', 'foo')
     ->addElement('text', 'bar');
$sub2->addSubForm($sub3, 'subform3');
$sub2->subform3->foo->setValue('FOO');
$sub2->subform3->bar->setValue('BAR');

$form = new Zend_Form();
$form->addElement('text', 'foo')
     ->addElement('text', 'bar')
     ->addSubForm($sub, 'subform')
     ->addSubForm($sub2, 'subform2');

$form->foo->setValue('FOO');
$form->bar->setValue('BAR');
$form->subform->foo->setValue('FOO');
$form->subform->bar->setValue('BAR');
$form->subform2->foo->setValue('FOO');
$form->subform2->bar->setValue('BAR');

$values = $form->getValues();
var_export($values);

This gave the expected output as well:


array (
  'foo' => 'FOO',
  'bar' => 'BAR',
  'subform' => 
  array (
    'foo' => 'FOO',
    'bar' => 'BAR',
  ),
  'subform2' => 
  array (
    'foo' => 'FOO',
    'bar' => 'BAR',
    'subform3' => 
    array (
      'foo' => 'FOO',
      'bar' => 'BAR',
    ),
  ),
)

Hi, I've tried the above code on ZF 1.7.8 (latest stable release), and indeed it works.

Problem is that it does not work if, instead of instanciating a Zend_Form_SubForm, you simply provide a Zend_Form:


$sub = new Zend_Form();
$sub->setName('subform')
    ->addElement('text', 'foo')
    ->addElement('text', 'bar');

$form = new Zend_Form();
$form->addElement('text', 'foo')
     ->addElement('text', 'bar')
     ->addSubForm($sub, 'subform');

$form->foo->setValue('FOO');
$form->bar->setValue('BAR');
$form->subform->foo->setValue('FOO');
$form->subform->bar->setValue('BAR');

$values = $form->getValues();
var_export($values);

This gives the wrong output (subform name not used as key in array => multiple subforms overwrite one another):


array (
  'foo' => 'FOO',
  'bar' => 'BAR',
  '' => 
  array (
    'foo' => 'FOO',
    'bar' => 'BAR',
  ),
)

By the way, is it possible to make the call to {{$sub->setName('subform')}} optional since when we attach the subform, we do provide a name! {{->addSubForm($sub, 'subform');}}

PS1: I did not find how to re-open the bug, I hope that commenting will automatically re-open it PS2: it has nothing to do with this bug, but please update the downloadable api documentation upon each release! (it's currently stuck on 1.7.2 and it's a real pain to regenerate it each time): http://framework.zend.com/docs/api

I can confirm the comment above as well (on 1.8.2). It only happens when using Zend_Form as a subform instead of Zend_Form_SubForm.

According to the docs, this is perfectly legal: "A sub form may be a Zend_Form object, or, more typically, a Zend_Form_SubForm object."

Perhaps this needs to be opened as a new issue?

To add a Zend_Form object as a subform, you need to call $subform->setIsArray(true). This will allow $form->getValues() to produce the expected array.


$form = new MyForm(); // extends Zend_Form
$subform = new MySubform(); // extends Zend_Form
$subform->setIsArray(true);
$form->addSubform($subform, 'mysubform');

Another issue related to adding Zend_Form objects as subforms is that the subforms have their own

<

form> tag. The fix is to remove the "Form" decorator from each subform:


$form = new MyForm(); // extends Zend_Form
$subform = new MySubform(); // extends Zend_Form
$subform->setIsArray(true);
$subform->removeDecorator('Form');
$form->addSubform($subform, 'mysubform');

It would be nice if Zend_Form::addSubform() handled these tasks automatically.

I don't believe this issue is closed. Matthew's tests did not hit the sweet spot, which is having multiple subforms of the same level. Only the last one is getting returned. Please fix this.

I am able to replicate what I understand the issue to be. It seems that array_merge is overwriting previous entries of the same subform. Using array_merge_recursive solves the issue. Here is an example on replicating it.


<?php

require 'Zend/Form.php';
require 'Zend/Form/SubForm.php';

class MyForm extends Zend_Form
{
    public function init()
    {
        $this->addElement('text', 'foo');
    }
}

class MySubForm extends Zend_Form_SubForm
{
    static protected $formId = 0;

    public function init()
    {
        $this->setElementsBelongTo('mysubform[' . self::$formId++ . ']');
        $this->addElement('text', 'bar');
    }
}

$data = array(
    'foo' => 'one',
    'mysubform' => array(
        array(
            'bar' => 'two',
        ),
        array(
            'bar' => 'three',
        ),
    ),
);

// Method 1:
// Simply calling Zend_Form::isValid() will not work because the form
// has no knowledge of the SubForms.
$form = new MyForm();
$form->isValid($data);
var_dump($form->getValues());
/* Result:
array(1) {
  ["foo"]=>
  string(3) "one"
}
*/

// Method 2: 
// So the second option is just to add the subforms and then try to
// validate the data. This method almost works. The data is validated
// correctly. The problem here is in Zend_Form::getValues(). The issue,
// is that array_merge will overwrite each mysubform key even if they
// have differing keys within it. The only way to solve this is by using
// array_merge_recursive instead.
$form = new MyForm();

foreach ($data['mysubform'] as $key => $datasub) {
    $subform = new MySubForm();
    $form->addSubForm($subform, 'mysubform' . $key);
}

$form->isValid($data);
var_dump($form->getValues());
/* Result:
array(3) {
  ["foo"]=>
  string(3) "one"
  ["mysubform0"]=>
  array(0) {
  }
  ["mysubform1"]=>
  array(0) {
  }
}
*/

// Now, changing array_merge to array_merge_recursive in 
// Zend/Form.php:1308 solves the problem, yielding the expected result
/* Result:
array(2) {
  ["foo"]=>
  string(3) "one"
  ["mysubform"]=>
  array(2) {
    [0]=>
    array(1) {
      ["bar"]=>
      string(3) "two"
    }
    [1]=>
    array(1) {
      ["bar"]=>
      string(5) "three"
    }
  }
}
 */