diff --git a/src/Symfony/Component/Form/Event/ChildDataEvent.php b/src/Symfony/Component/Form/Event/ChildDataEvent.php new file mode 100644 index 0000000000000..651f7b62befaa --- /dev/null +++ b/src/Symfony/Component/Form/Event/ChildDataEvent.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Event; + +use Symfony\Component\Form\FormInterface; + +class ChildDataEvent extends DataEvent +{ + private $name; + + /** + * Constructs an event. + * + * @param FormInterface $form The associated form + * @param string $name The name of the field that was just set + * @param mixed $data The data of the field that was just set + */ + public function __construct(FormInterface $form, $name, $data) + { + $this->name = $name; + parent::__construct($form, $data); + } + + /** + * Gets the data of the field that was just set + */ + public function getData() + { + return parent::getData(); + } + + /** + * Gets the name of the field that was updated + */ + public function getName() + { + return $this->name; + } +} diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 0e68b88c01edf..f886fff7043e3 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\Event\DataEvent; use Symfony\Component\Form\Event\FilterDataEvent; +use Symfony\Component\Form\Event\ChildDataEvent; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Exception\AlreadyBoundException; use Symfony\Component\Form\Exception\UnexpectedTypeException; @@ -506,13 +507,21 @@ public function bind($clientData) } } - foreach ($clientData as $name => $value) { - if ($this->has($name)) { - $this->children[$name]->bind($value); - } else { - $extraData[$name] = $value; + $extraData = $clientData; + do { + $lastChildren = $this->children; + foreach ($extraData as $name => $value) { + if ($this->has($name)) { + $this->children[$name]->bind($value); + unset($extraData[$name]); + + // every time we bind a child, we dispatch an event to allow + // listeners to add or remove field based on the result value + $event = new ChildDataEvent($this, $name, $this->children[$name]->getData()); + $this->dispatcher->dispatch(FormEvents::BIND_CHILD, $event); + } } - } + } while(count($extraData) && $lastChildren !== $this->children); // If we have a data mapper, use old client data and merge // data from the children into it later diff --git a/src/Symfony/Component/Form/FormEvents.php b/src/Symfony/Component/Form/FormEvents.php index a97337ec5a187..a30f66ab75623 100644 --- a/src/Symfony/Component/Form/FormEvents.php +++ b/src/Symfony/Component/Form/FormEvents.php @@ -26,6 +26,8 @@ final class FormEvents const BIND_CLIENT_DATA = 'form.bind_client_data'; + const BIND_CHILD = 'form.bind_child'; + const BIND_NORM_DATA = 'form.bind_norm_data'; const SET_DATA = 'form.set_data'; diff --git a/src/Symfony/Component/Form/Tests/FormTest.php b/src/Symfony/Component/Form/Tests/FormTest.php index d6784f4e52393..e47e15318f430 100644 --- a/src/Symfony/Component/Form/Tests/FormTest.php +++ b/src/Symfony/Component/Form/Tests/FormTest.php @@ -22,6 +22,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; use Symfony\Component\Form\Tests\Fixtures\FixedFilterListener; +use Symfony\Component\Form\Event\ChildDataEvent; +use Symfony\Component\Form\FormEvents; class FormTest extends \PHPUnit_Framework_TestCase { @@ -1303,6 +1305,78 @@ public function testGetValidatorsReturnsValidators() $this->assertEquals(array($validator), $form->getValidators()); } + + public function testBindChild() + { + $field1builder = $this->getBuilder('field1'); + $field2builder = $this->getBuilder('field2'); + + $builder = $this->getBuilder('form', new EventDispatcher()) + ->addEventListener(FormEvents::BIND_CHILD, function(ChildDataEvent $e) use ($field2builder) { + if ($e->getName() == 'field1') { + if ($e->getData() == 'a') { + $e->getForm()->add($field2builder->getForm()); + } else { + $e->getForm()->remove('field2'); + } + } + }); + $form = $builder->getForm(); + $form->add($field1builder->getForm()); + + $form->bind(array('field1'=>'a', 'field2'=>'b')); + $this->assertTrue($form->has('field2')); + $this->assertEmpty($form->getExtraData()); + + $form = $builder->getForm(); + $form->add($field1builder->getForm()); + $form->add($field2builder->getForm()); + + $form->bind(array('field1'=>'b', 'field2'=>'a')); + $this->assertFalse($form->has('field2')); + $this->assertEquals(array('field2'=>'a'), $form->getExtraData()); + } + + public function testBindChildDoesntResultInEndlessLoop() + { + $field1builder = $this->getBuilder('field1'); + + $builder = $this->getBuilder('form'); + + $form = $builder->getForm(); + $form->add($field1builder->getForm()); + + $form->bind(array('field1'=>'a', 'field2'=>'b')); + } + + public function testBindChildKeepsLoopingUntilNoFieldsAreAdded() + { + $transformer = $this->getDataTransformer(); + $transformer->expects($this->exactly(3))->method('reverseTransform')->will($this->onConsecutiveCalls(array('abound', 'bbound', 'cbound'))); + + $field2builder = $this->getBuilder('field2')->appendClientTransformer($transformer); + $field3builder = $this->getBuilder('field3')->appendClientTransformer($transformer); + + $builder = $this->getBuilder('form', new EventDispatcher()) + ->addEventListener(FormEvents::BIND_CHILD, function(ChildDataEvent $e) use ($field2builder, $field3builder) { + if ($e->getName() == 'field1') { + $e->getForm()->add($field2builder->getForm()); + } elseif ($e->getName() == 'field2') { + $e->getForm()->add($field3builder->getForm()); + } + }); + + $form = $builder->getForm(); + + $form->add($this->getBuilder('field1')->appendClientTransformer($transformer)->getForm()); + + $form->bind(array('field1'=>'a', 'field2'=>'b', 'field3'=>'c', 'field4'=>'d')); + + // issue #3770 + //$this->assertEquals(array('field1'=>'abound', 'field2'=>'bbound', 'field3'=>'cbound', 'field4'=>'d'), $form->getData()); + $this->assertEquals(array('field1'=>'a', 'field2'=>'b', 'field3'=>'c', 'field4'=>'d'), $form->getData()); + $this->assertEquals(array('field4'=>'d'), $form->getExtraData()); + } protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null) {