Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 599aab4

Browse filesBrowse files
committed
merged branch bschussek/form-submit-2.2 (PR #8827)
This PR was merged into the 2.2 branch. Discussion ---------- [Form][2.2] Fixed Form::submit() to react to dynamic form modifications | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - ref #3767, #3768, #4548, #8748 cc @Burgov This PR ensures that fields that are added during the submission process of a form are submitted as well. For example: ```php $builder = $this->createFormBuilder() ->add('country', 'choice', ...) ->getForm(); $builder->get('country')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { $form = $event->getForm()->getParent(); $country = $event->getForm()->getData(); $form->add('province', 'choice', /* ... something with $country ... */); }); ``` Currently, the field "province" will not be submitted, because the submission loop uses `foreach`, which ignores changes in the underyling array. Additionally, this PR ensures that `setData()` is called on *every* element of a form (except those inheriting their parent data) when `setData()` is called on the root element (i.e., during initialization). Currently, when some of the intermediate nodes (e.g. embedded forms) are submitted with an empty value, `setData()` won't be called on the nodes below (i.e. the fields of the embedded form) until `get*Data()` is called on them. If `getData()` is *not* called before `createView()`, any effects of `*_DATA` event listeners attached to those fields will not be visible in the view. Commits ------- cd27e1f [Form] Extracted ReferencingArrayIterator out of VirtualFormAwareIterator ccaaedf [Form] PropertyPathMapper::mapDataToForms() *always* calls setData() on every child to ensure that all *_DATA events were fired when the initialization phase is over (except for virtual forms) 19b483f [Form] Removed superfluous reset() call 00bc270 [Form] Fixed: submit() reacts to dynamic modifications of the form children
2 parents ee3cb88 + cd27e1f commit 599aab4
Copy full SHA for 599aab4

File tree

Expand file treeCollapse file tree

8 files changed

+306
-31
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+306
-31
lines changed

‎src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php
+7-7Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,24 @@ public function __construct(PropertyAccessorInterface $propertyAccessor = null)
4444
*/
4545
public function mapDataToForms($data, array $forms)
4646
{
47-
if (null === $data || array() === $data) {
48-
return;
49-
}
47+
$empty = null === $data || array() === $data;
5048

51-
if (!is_array($data) && !is_object($data)) {
49+
if (!$empty && !is_array($data) && !is_object($data)) {
5250
throw new UnexpectedTypeException($data, 'object, array or empty');
5351
}
5452

5553
$iterator = new VirtualFormAwareIterator($forms);
5654
$iterator = new \RecursiveIteratorIterator($iterator);
5755

5856
foreach ($iterator as $form) {
59-
/* @var FormInterface $form */
57+
/* @var \Symfony\Component\Form\FormInterface $form */
6058
$propertyPath = $form->getPropertyPath();
6159
$config = $form->getConfig();
6260

63-
if (null !== $propertyPath && $config->getMapped()) {
61+
if (!$empty && null !== $propertyPath && $config->getMapped()) {
6462
$form->setData($this->propertyAccessor->getValue($data, $propertyPath));
63+
} else {
64+
$form->setData($form->getConfig()->getData());
6565
}
6666
}
6767
}
@@ -83,7 +83,7 @@ public function mapFormsToData(array $forms, &$data)
8383
$iterator = new \RecursiveIteratorIterator($iterator);
8484

8585
foreach ($iterator as $form) {
86-
/* @var FormInterface $form */
86+
/* @var \Symfony\Component\Form\FormInterface $form */
8787
$propertyPath = $form->getPropertyPath();
8888
$config = $form->getConfig();
8989

‎src/Symfony/Component/Form/Form.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Form.php
+5-2Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,10 @@ public function bind($submittedData)
552552
$submittedData = array();
553553
}
554554

555-
foreach ($this->children as $name => $child) {
555+
for (reset($this->children); false !== current($this->children); next($this->children)) {
556+
$child = current($this->children);
557+
$name = key($this->children);
558+
556559
$child->bind(isset($submittedData[$name]) ? $submittedData[$name] : null);
557560
unset($submittedData[$name]);
558561
}
@@ -829,7 +832,7 @@ public function getClientTransformers()
829832
/**
830833
* {@inheritdoc}
831834
*/
832-
public function all()
835+
public function &all()
833836
{
834837
return $this->children;
835838
}

‎src/Symfony/Component/Form/Tests/AbstractFormTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/AbstractFormTest.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ protected function getMockForm($name = 'name')
8181
$form->expects($this->any())
8282
->method('getName')
8383
->will($this->returnValue($name));
84+
$form->expects($this->any())
85+
->method('getConfig')
86+
->will($this->returnValue($this->getMock('Symfony\Component\Form\FormConfigInterface')));
8487

8588
return $form;
8689
}

‎src/Symfony/Component/Form/Tests/CompoundFormTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/CompoundFormTest.php
+41-14Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\Form\Tests;
1313

14-
use Symfony\Component\Form\Form;
1514
use Symfony\Component\Form\FormError;
1615
use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
1716
use Symfony\Component\HttpFoundation\Request;
@@ -46,19 +45,6 @@ public function testInvalidIfChildIsInvalid()
4645
$this->assertFalse($this->form->isValid());
4746
}
4847

49-
public function testBindForwardsNullIfValueIsMissing()
50-
{
51-
$child = $this->getMockForm('firstName');
52-
53-
$this->form->add($child);
54-
55-
$child->expects($this->once())
56-
->method('bind')
57-
->with($this->equalTo(null));
58-
59-
$this->form->bind(array());
60-
}
61-
6248
public function testCloneChildren()
6349
{
6450
$child = $this->getBuilder('child')->getForm();
@@ -322,6 +308,47 @@ public function testSetDataMapsViewDataToChildren()
322308
$form->setData('foo');
323309
}
324310

311+
public function testBindForwardsNullIfValueIsMissing()
312+
{
313+
$child = $this->getMockForm('firstName');
314+
315+
$this->form->add($child);
316+
317+
$child->expects($this->once())
318+
->method('bind')
319+
->with($this->equalTo(null));
320+
321+
$this->form->bind(array());
322+
}
323+
324+
public function testBindSupportsDynamicAdditionAndRemovalOfChildren()
325+
{
326+
$child = $this->getMockForm('child');
327+
$childToBeRemoved = $this->getMockForm('removed');
328+
$childToBeAdded = $this->getMockForm('added');
329+
330+
$this->form->add($child);
331+
$this->form->add($childToBeRemoved);
332+
333+
$form = $this->form;
334+
335+
$child->expects($this->once())
336+
->method('bind')
337+
->will($this->returnCallback(function () use ($form, $childToBeAdded) {
338+
$form->remove('removed');
339+
$form->add($childToBeAdded);
340+
}));
341+
342+
$childToBeRemoved->expects($this->never())
343+
->method('bind');
344+
345+
$childToBeAdded->expects($this->once())
346+
->method('bind');
347+
348+
// pass NULL to all children
349+
$this->form->bind(array());
350+
}
351+
325352
public function testBindMapsBoundChildrenOntoExistingViewData()
326353
{
327354
$test = $this;

‎src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php
+36-3Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@ public function testMapDataToFormsIgnoresUnmapped()
165165
$this->assertNull($form->getData());
166166
}
167167

168-
public function testMapDataToFormsIgnoresEmptyData()
168+
public function testMapDataToFormsSetsDefaultDataIfPassedDataIsNull()
169169
{
170+
$default = new \stdClass();
170171
$propertyPath = $this->getPropertyPath('engine');
171172

172173
$this->propertyAccessor->expects($this->never())
@@ -175,11 +176,43 @@ public function testMapDataToFormsIgnoresEmptyData()
175176
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
176177
$config->setByReference(true);
177178
$config->setPropertyPath($propertyPath);
178-
$form = $this->getForm($config);
179+
$config->setData($default);
180+
181+
$form = $this->getMockBuilder('Symfony\Component\Form\Form')
182+
->setConstructorArgs(array($config))
183+
->setMethods(array('setData'))
184+
->getMock();
185+
186+
$form->expects($this->once())
187+
->method('setData')
188+
->with($default);
179189

180190
$this->mapper->mapDataToForms(null, array($form));
191+
}
181192

182-
$this->assertNull($form->getData());
193+
public function testMapDataToFormsSetsDefaultDataIfPassedDataIsEmptyArray()
194+
{
195+
$default = new \stdClass();
196+
$propertyPath = $this->getPropertyPath('engine');
197+
198+
$this->propertyAccessor->expects($this->never())
199+
->method('getValue');
200+
201+
$config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
202+
$config->setByReference(true);
203+
$config->setPropertyPath($propertyPath);
204+
$config->setData($default);
205+
206+
$form = $this->getMockBuilder('Symfony\Component\Form\Form')
207+
->setConstructorArgs(array($config))
208+
->setMethods(array('setData'))
209+
->getMock();
210+
211+
$form->expects($this->once())
212+
->method('setData')
213+
->with($default);
214+
215+
$this->mapper->mapDataToForms(array(), array($form));
183216
}
184217

185218
public function testMapDataToFormsSkipsVirtualForms()
+122Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Tests\Util;
13+
14+
use Symfony\Component\Form\Util\VirtualFormAwareIterator;
15+
16+
/**
17+
* @author Bernhard Schussek <bschussek@gmail.com>
18+
*/
19+
class VirtualFormAwareIteratorTest extends \PHPUnit_Framework_TestCase
20+
{
21+
public function testSupportDynamicModification()
22+
{
23+
$form = $this->getMockForm('form');
24+
$formToBeAdded = $this->getMockForm('added');
25+
$formToBeRemoved = $this->getMockForm('removed');
26+
27+
$forms = array('form' => $form, 'removed' => $formToBeRemoved);
28+
$iterator = new VirtualFormAwareIterator($forms);
29+
30+
$iterator->rewind();
31+
$this->assertTrue($iterator->valid());
32+
$this->assertSame('form', $iterator->key());
33+
$this->assertSame($form, $iterator->current());
34+
35+
// dynamic modification
36+
unset($forms['removed']);
37+
$forms['added'] = $formToBeAdded;
38+
39+
// continue iteration
40+
$iterator->next();
41+
$this->assertTrue($iterator->valid());
42+
$this->assertSame('added', $iterator->key());
43+
$this->assertSame($formToBeAdded, $iterator->current());
44+
45+
// end of array
46+
$iterator->next();
47+
$this->assertFalse($iterator->valid());
48+
}
49+
50+
public function testSupportDynamicModificationInRecursiveCall()
51+
{
52+
$virtualForm = $this->getMockForm('virtual');
53+
$form = $this->getMockForm('form');
54+
$formToBeAdded = $this->getMockForm('added');
55+
$formToBeRemoved = $this->getMockForm('removed');
56+
57+
$virtualForm->getConfig()->expects($this->any())
58+
->method('getVirtual')
59+
->will($this->returnValue(true));
60+
61+
$virtualForm->add($form);
62+
$virtualForm->add($formToBeRemoved);
63+
64+
$forms = array('virtual' => $virtualForm);
65+
$iterator = new VirtualFormAwareIterator($forms);
66+
67+
$iterator->rewind();
68+
$this->assertTrue($iterator->valid());
69+
$this->assertSame('virtual', $iterator->key());
70+
$this->assertSame($virtualForm, $iterator->current());
71+
$this->assertTrue($iterator->hasChildren());
72+
73+
// enter nested iterator
74+
$nestedIterator = $iterator->getChildren();
75+
$this->assertSame('form', $nestedIterator->key());
76+
$this->assertSame($form, $nestedIterator->current());
77+
$this->assertFalse($nestedIterator->hasChildren());
78+
79+
// dynamic modification
80+
$virtualForm->remove('removed');
81+
$virtualForm->add($formToBeAdded);
82+
83+
// continue iteration - nested iterator discovers change in the form
84+
$nestedIterator->next();
85+
$this->assertTrue($nestedIterator->valid());
86+
$this->assertSame('added', $nestedIterator->key());
87+
$this->assertSame($formToBeAdded, $nestedIterator->current());
88+
89+
// end of array
90+
$nestedIterator->next();
91+
$this->assertFalse($nestedIterator->valid());
92+
}
93+
94+
/**
95+
* @param string $name
96+
*
97+
* @return \PHPUnit_Framework_MockObject_MockObject
98+
*/
99+
protected function getMockForm($name = 'name')
100+
{
101+
$config = $this->getMock('Symfony\Component\Form\FormConfigInterface');
102+
103+
$config->expects($this->any())
104+
->method('getName')
105+
->will($this->returnValue($name));
106+
$config->expects($this->any())
107+
->method('getCompound')
108+
->will($this->returnValue(true));
109+
$config->expects($this->any())
110+
->method('getDataMapper')
111+
->will($this->returnValue($this->getMock('Symfony\Component\Form\DataMapperInterface')));
112+
$config->expects($this->any())
113+
->method('getEventDispatcher')
114+
->will($this->returnValue($this->getMock('Symfony\Component\EventDispatcher\EventDispatcher')));
115+
116+
return $this->getMockBuilder('Symfony\Component\Form\Form')
117+
->setConstructorArgs(array($config))
118+
->disableArgumentCloning()
119+
->setMethods(array('getViewData'))
120+
->getMock();
121+
}
122+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.