Skip to content

Commit

Permalink
[Form] Fixed: submit() reacts to dynamic modifications of the form ch…
Browse files Browse the repository at this point in the history
…ildren
  • Loading branch information
webmozart committed Aug 22, 2013
1 parent 1ad64ee commit 00bc270
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 21 deletions.
9 changes: 7 additions & 2 deletions src/Symfony/Component/Form/Form.php
Expand Up @@ -552,7 +552,12 @@ public function bind($submittedData)
$submittedData = array();
}

foreach ($this->children as $name => $child) {
reset($this->children);

for (reset($this->children); false !== current($this->children); next($this->children)) {
$child = current($this->children);
$name = key($this->children);

$child->bind(isset($submittedData[$name]) ? $submittedData[$name] : null);
unset($submittedData[$name]);
}
Expand Down Expand Up @@ -829,7 +834,7 @@ public function getClientTransformers()
/**
* {@inheritdoc}
*/
public function all()
public function &all()

This comment has been minimized.

Copy link
@fabpot

fabpot Aug 23, 2013

Member

That breaks FormInterface contract and makes things fail on at least PHP 5.3. See https://travis-ci.org/fabpot/symfony/jobs/10541904

This comment has been minimized.

Copy link
@fabpot

fabpot Aug 23, 2013

Member

ping @bschussek

This comment has been minimized.

Copy link
@webmozart

webmozart Aug 23, 2013

Author Contributor

Really? Weird, everything was green for me. Have to check again.

This comment has been minimized.

Copy link
@webmozart

webmozart Aug 24, 2013

Author Contributor

Apparently this is an issue on PHP <= 5.3.3 only.

Source:

Starting with PHP 5.3.4, the prototype checks were relaxed and it's possible for implementations of this method to return by reference. This makes indirect modifications to the overloaded array dimensions of ArrayAccess objects possible.

I could reproduce the problem on 5.3.3, but not on 5.3.27. I'm currently building 5.3.4 to test it there.

Would bumping the minimum requirement for Symfony to 5.3.4 be an option?

This comment has been minimized.

Copy link
@webmozart

webmozart Aug 24, 2013

Author Contributor

If we trust these statistics, 5.3.3 is used by 20%+ of the web servers running on PHP 5.3. I guess I need to find some work-around then.

This comment has been minimized.

Copy link
@webmozart

webmozart Aug 25, 2013

Author Contributor

fixed #8851 #8852

{
return $this->children;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Form/Tests/AbstractFormTest.php
Expand Up @@ -81,6 +81,9 @@ protected function getMockForm($name = 'name')
$form->expects($this->any())
->method('getName')
->will($this->returnValue($name));
$form->expects($this->any())
->method('getConfig')
->will($this->returnValue($this->getMock('Symfony\Component\Form\FormConfigInterface')));

return $form;
}
Expand Down
55 changes: 41 additions & 14 deletions src/Symfony/Component/Form/Tests/CompoundFormTest.php
Expand Up @@ -11,7 +11,6 @@

namespace Symfony\Component\Form\Tests;

use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -46,19 +45,6 @@ public function testInvalidIfChildIsInvalid()
$this->assertFalse($this->form->isValid());
}

public function testBindForwardsNullIfValueIsMissing()
{
$child = $this->getMockForm('firstName');

$this->form->add($child);

$child->expects($this->once())
->method('bind')
->with($this->equalTo(null));

$this->form->bind(array());
}

public function testCloneChildren()
{
$child = $this->getBuilder('child')->getForm();
Expand Down Expand Up @@ -322,6 +308,47 @@ public function testSetDataMapsViewDataToChildren()
$form->setData('foo');
}

public function testBindForwardsNullIfValueIsMissing()
{
$child = $this->getMockForm('firstName');

$this->form->add($child);

$child->expects($this->once())
->method('bind')
->with($this->equalTo(null));

$this->form->bind(array());
}

public function testBindSupportsDynamicAdditionAndRemovalOfChildren()
{
$child = $this->getMockForm('child');
$childToBeRemoved = $this->getMockForm('removed');
$childToBeAdded = $this->getMockForm('added');

$this->form->add($child);
$this->form->add($childToBeRemoved);

$form = $this->form;

$child->expects($this->once())
->method('bind')
->will($this->returnCallback(function () use ($form, $childToBeAdded) {
$form->remove('removed');
$form->add($childToBeAdded);
}));

$childToBeRemoved->expects($this->never())
->method('bind');

$childToBeAdded->expects($this->once())
->method('bind');

// pass NULL to all children
$this->form->bind(array());
}

public function testBindMapsBoundChildrenOntoExistingViewData()
{
$test = $this;
Expand Down
122 changes: 122 additions & 0 deletions src/Symfony/Component/Form/Tests/Util/VirtualFormAwareIteratorTest.php
@@ -0,0 +1,122 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Tests\Util;

use Symfony\Component\Form\Util\VirtualFormAwareIterator;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class VirtualFormAwareIteratorTest extends \PHPUnit_Framework_TestCase
{
public function testSupportDynamicModification()
{
$form = $this->getMockForm('form');
$formToBeAdded = $this->getMockForm('added');
$formToBeRemoved = $this->getMockForm('removed');

$forms = array('form' => $form, 'removed' => $formToBeRemoved);
$iterator = new VirtualFormAwareIterator($forms);

$iterator->rewind();
$this->assertTrue($iterator->valid());
$this->assertSame('form', $iterator->key());
$this->assertSame($form, $iterator->current());

// dynamic modification
unset($forms['removed']);
$forms['added'] = $formToBeAdded;

// continue iteration
$iterator->next();
$this->assertTrue($iterator->valid());
$this->assertSame('added', $iterator->key());
$this->assertSame($formToBeAdded, $iterator->current());

// end of array
$iterator->next();
$this->assertFalse($iterator->valid());
}

public function testSupportDynamicModificationInRecursiveCall()
{
$virtualForm = $this->getMockForm('virtual');
$form = $this->getMockForm('form');
$formToBeAdded = $this->getMockForm('added');
$formToBeRemoved = $this->getMockForm('removed');

$virtualForm->getConfig()->expects($this->any())
->method('getVirtual')
->will($this->returnValue(true));

$virtualForm->add($form);
$virtualForm->add($formToBeRemoved);

$forms = array('virtual' => $virtualForm);
$iterator = new VirtualFormAwareIterator($forms);

$iterator->rewind();
$this->assertTrue($iterator->valid());
$this->assertSame('virtual', $iterator->key());
$this->assertSame($virtualForm, $iterator->current());
$this->assertTrue($iterator->hasChildren());

// enter nested iterator
$nestedIterator = $iterator->getChildren();
$this->assertSame('form', $nestedIterator->key());
$this->assertSame($form, $nestedIterator->current());
$this->assertFalse($nestedIterator->hasChildren());

// dynamic modification
$virtualForm->remove('removed');
$virtualForm->add($formToBeAdded);

// continue iteration - nested iterator discovers change in the form
$nestedIterator->next();
$this->assertTrue($nestedIterator->valid());
$this->assertSame('added', $nestedIterator->key());
$this->assertSame($formToBeAdded, $nestedIterator->current());

// end of array
$nestedIterator->next();
$this->assertFalse($nestedIterator->valid());
}

/**
* @param string $name
*
* @return \PHPUnit_Framework_MockObject_MockObject
*/
protected function getMockForm($name = 'name')
{
$config = $this->getMock('Symfony\Component\Form\FormConfigInterface');

$config->expects($this->any())
->method('getName')
->will($this->returnValue($name));
$config->expects($this->any())
->method('getCompound')
->will($this->returnValue(true));
$config->expects($this->any())
->method('getDataMapper')
->will($this->returnValue($this->getMock('Symfony\Component\Form\DataMapperInterface')));
$config->expects($this->any())
->method('getEventDispatcher')
->will($this->returnValue($this->getMock('Symfony\Component\EventDispatcher\EventDispatcher')));

return $this->getMockBuilder('Symfony\Component\Form\Form')
->setConstructorArgs(array($config))
->disableArgumentCloning()
->setMethods(array('getViewData'))
->getMock();
}
}
74 changes: 69 additions & 5 deletions src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php
Expand Up @@ -12,22 +12,86 @@
namespace Symfony\Component\Form\Util;

/**
* Iterator that traverses fields of a field group
* Iterator that traverses an array of forms.
*
* If the iterator encounters a virtual field group, it enters the field
* group and traverses its children as well.
* Contrary to \ArrayIterator, this iterator recognizes changes in the original
* array during iteration.
*
* You can wrap the iterator into a {@link \RecursiveIterator} in order to
* enter any virtual child form and iterate the children of that virtual form.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class VirtualFormAwareIterator extends \ArrayIterator implements \RecursiveIterator
class VirtualFormAwareIterator implements \RecursiveIterator
{
/**
* @var \Symfony\Component\Form\FormInterface[]
*/
private $forms;

/**
* Creates a new iterator.
*
* @param \Symfony\Component\Form\FormInterface[] $forms An array of forms
*/
public function __construct(array &$forms)
{
$this->forms = &$forms;
}

/**
*{@inheritdoc}
*/
public function current()
{
return current($this->forms);
}

/**
*{@inheritdoc}
*/
public function next()
{
next($this->forms);
}

/**
*{@inheritdoc}
*/
public function key()
{
return key($this->forms);
}

/**
*{@inheritdoc}
*/
public function valid()
{
return null !== key($this->forms);
}

/**
*{@inheritdoc}
*/
public function rewind()
{
reset($this->forms);
}

/**
*{@inheritdoc}
*/
public function getChildren()
{
return new self($this->current()->all());
}

/**
*{@inheritdoc}
*/
public function hasChildren()
{
return $this->current()->getConfig()->getVirtual();
return (bool) $this->current()->getConfig()->getVirtual();
}
}

0 comments on commit 00bc270

Please sign in to comment.