Skip to content

Commit 13dbd70

Browse files
committed
bug #33999 [Form] Make sure to collect child forms created on *_SET_DATA events (yceruto)
This PR was merged into the 3.4 branch. Discussion ---------- [Form] Make sure to collect child forms created on *_SET_DATA events | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #29291 | License | MIT | Doc PR | - See reproducer provided by @WubbleWobble https://github.com/WubbleWobble/symfony-issue-29291. Commits ------- 50efc1a Make sure to collect child forms created on *_SET_DATA events
2 parents dfd3d4b + f9c9dae commit 13dbd70

File tree

2 files changed

+65
-4
lines changed

2 files changed

+65
-4
lines changed

Extension/DataCollector/FormDataCollector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ public function collectDefaultData(FormInterface $form)
128128
$hash = spl_object_hash($form);
129129

130130
if (!isset($this->dataByForm[$hash])) {
131-
$this->dataByForm[$hash] = [];
131+
// field was created by form event
132+
$this->collectConfiguration($form);
132133
}
133134

134135
$this->dataByForm[$hash] = array_replace(

Tests/Extension/DataCollector/FormDataCollectorTest.php

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@
1313

1414
use PHPUnit\Framework\MockObject\MockObject;
1515
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\EventDispatcher\EventDispatcher;
17+
use Symfony\Component\Form\Extension\Core\CoreExtension;
18+
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
19+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
20+
use Symfony\Component\Form\Extension\Core\Type\FormType;
21+
use Symfony\Component\Form\Extension\Core\Type\TextType;
1622
use Symfony\Component\Form\Extension\DataCollector\FormDataCollector;
1723
use Symfony\Component\Form\Form;
1824
use Symfony\Component\Form\FormBuilder;
25+
use Symfony\Component\Form\FormFactory;
26+
use Symfony\Component\Form\FormInterface;
27+
use Symfony\Component\Form\FormRegistry;
1928
use Symfony\Component\Form\FormView;
29+
use Symfony\Component\Form\ResolvedFormTypeFactory;
2030

2131
class FormDataCollectorTest extends TestCase
2232
{
@@ -69,9 +79,9 @@ protected function setUp()
6979
{
7080
$this->dataExtractor = $this->getMockBuilder('Symfony\Component\Form\Extension\DataCollector\FormDataExtractorInterface')->getMock();
7181
$this->dataCollector = new FormDataCollector($this->dataExtractor);
72-
$this->dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
73-
$this->factory = $this->getMockBuilder('Symfony\Component\Form\FormFactoryInterface')->getMock();
74-
$this->dataMapper = $this->getMockBuilder('Symfony\Component\Form\DataMapperInterface')->getMock();
82+
$this->dispatcher = new EventDispatcher();
83+
$this->factory = new FormFactory(new FormRegistry([new CoreExtension()], new ResolvedFormTypeFactory()));
84+
$this->dataMapper = new PropertyPathMapper();
7585
$this->form = $this->createForm('name');
7686
$this->childForm = $this->createForm('child');
7787
$this->view = new FormView();
@@ -726,6 +736,56 @@ public function testReset()
726736
);
727737
}
728738

739+
public function testCollectMissingDataFromChildFormAddedOnFormEvents()
740+
{
741+
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
742+
->add('items', CollectionType::class, [
743+
'entry_type' => TextType::class,
744+
'allow_add' => true,
745+
// data is locked and modelData (null) is different to the
746+
// configured data, so modifications of the configured data
747+
// won't be allowed at this point. It also means *_SET_DATA
748+
// events won't dispatched either. Therefore, no child form
749+
// is created during the mapping of data to the form.
750+
'data' => ['foo'],
751+
])
752+
->getForm()
753+
;
754+
$this->dataExtractor->expects($extractConfiguration = $this->exactly(4))
755+
->method('extractConfiguration')
756+
->willReturn([])
757+
;
758+
$this->dataExtractor->expects($extractDefaultData = $this->exactly(4))
759+
->method('extractDefaultData')
760+
->willReturnCallback(static function (FormInterface $form) {
761+
// this simulate the call in extractDefaultData() method
762+
// where (if defaultDataSet is false) it fires *_SET_DATA
763+
// events, adding the form related to the configured data
764+
$form->getNormData();
765+
766+
return [];
767+
})
768+
;
769+
$this->dataExtractor->expects($this->exactly(4))
770+
->method('extractSubmittedData')
771+
->willReturn([])
772+
;
773+
774+
$this->dataCollector->collectConfiguration($form);
775+
$this->assertSame(2, $extractConfiguration->getInvocationCount(), 'only "root" and "items" forms were collected, the "items" children do not exist yet.');
776+
777+
$this->dataCollector->collectDefaultData($form);
778+
$this->assertSame(3, $extractConfiguration->getInvocationCount(), 'extracted missing configuration of the "items" children ["0" => foo].');
779+
$this->assertSame(3, $extractDefaultData->getInvocationCount());
780+
$this->assertSame(['foo'], $form->get('items')->getData());
781+
782+
$form->submit(['items' => ['foo', 'bar']]);
783+
$this->dataCollector->collectSubmittedData($form);
784+
$this->assertSame(4, $extractConfiguration->getInvocationCount(), 'extracted missing configuration of the "items" children ["1" => bar].');
785+
$this->assertSame(4, $extractDefaultData->getInvocationCount(), 'extracted missing default data of the "items" children ["1" => bar].');
786+
$this->assertSame(['foo', 'bar'], $form->get('items')->getData());
787+
}
788+
729789
private function createForm($name)
730790
{
731791
$builder = new FormBuilder($name, null, $this->dispatcher, $this->factory);

0 commit comments

Comments
 (0)