8000 Allow array keys for collection types · symfony/symfony@e0dcab0 · GitHub
[go: up one dir, main page]

Skip to content

Commit e0dcab0

Browse files
Allow array keys for collection types
1 parent e213cc7 commit e0dcab0

File tree

4 files changed

+187
-10
lines changed

4 files changed

+187
-10
lines changed

src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,34 @@
2121
* Resize a collection form element based on the data sent from the client.
2222
*
2323
* @author Bernhard Schussek <bschussek@gmail.com>
24+
* @author Patrick Bußmann <patrick.bussmann@bussmann-it.de>
2425
*/
2526
class ResizeFormListener implements EventSubscriberInterface
2627
{
2728
protected $type;
2829
protected $options;
2930
protected $allowAdd;
3031
protected $allowDelete;
32+
protected $indexName;
3133

3234
private $deleteEmpty;
3335

3436
/**
35-
* @param bool $allowAdd Whether children could be added to the group
36-
* @param bool $allowDelete Whether children could be removed from the group
37+
* @param string $type
38+
* @param array $options
39+
* @param bool $allowAdd Whether children could be added to the group
40+
* @param bool $allowDelete Whether children could be removed from the group
3741
* @param bool|callable $deleteEmpty
42+
* @param string|callable|null $indexName
3843
*/
39-
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false)
44+
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false, $indexName = null)
4045
{
4146
$this->type = $type;
4247
$this->allowAdd = $allowAdd;
4348
$this->allowDelete = $allowDelete;
4449
$this->options = $options;
4550
$this->deleteEmpty = $deleteEmpty;
51+
$this->indexName = $indexName;
4652
}
4753

4854
public static function getSubscribedEvents()
@@ -75,7 +81,7 @@ public function preSetData(FormEvent $event)
7581

7682
// Then add all rows again in the correct order
7783
foreach ($data as $name => $value) {
78-
$form->add($name, $this->type, array_replace([
84+
$form->add($this->getIndexFromValue($value, $name), $this->type, array_replace([
7985
'property_path' => '['.$name.']',
8086
], $this->options));
8187
}
@@ -102,9 +108,10 @@ public function preSubmit(FormEvent $event)
102108
// Add all additional rows
103109
if ($this->allowAdd) {
104110
foreach ($data as $name => $value) {
105-
if (!$form->has($name)) {
106-
$form->add($name, $this->type, array_replace([
107-
'property_path' => '['.$name.']',
111+
$indexedName = $this->getIndexFromValue($value, $name);
112+
if (!$form->has($indexedName)) {
113+
$form->add($indexedName, $this->type, array_replace([
114+
'property_path' => '['.$indexedName.']',
108115
], $this->options));
109116
}
110117
}
@@ -150,7 +157,8 @@ public function onSubmit(FormEvent $event)
150157
$toDelete = [];
151158

152159
foreach ($data as $name => $child) {
153-
if (!$form->has($name)) {
160+
$indexedName = $this->getIndexFromValue($child, $name);
161+
if (!$form->has($indexedName)) {
154162
$toDelete[] = $name;
155163
}
156164
}
@@ -160,6 +168,26 @@ public function onSubmit(FormEvent $event)
160168
}
161169
}
162170

163-
$event->setData($data);
171+
if ($this->indexName !== null) {
172+
$newData = [];
173+
foreach ($form->all() as $item) {
174+
$newData[$item->getName()] = $item->getData();
175+
}
176+
$event->setData($newData);
177+
} else {
178+
$event->setData($data);
179+
}
180+
}
181+
182+
private function getIndexFromValue($value, $defaultValue = null) {
183+
if ($this->indexName === null || $value === null)
184+
{
185+
return $defaultValue;
186+
}
187+
$indexGetter = 'get' . ucfirst($this->indexName);
188+
return (\is_callable($this->indexName) ? ($this->indexName)($value, $defaultValue)
189+
: (\method_exists($value, $indexGetter) ? $value->$indexGetter()
190+
: (is_array($value) && array_key_exists($this->indexName, $value)
191+
? $value[$this->indexName] : null))) ?: $defaultValue;
164192
}
165193
}

src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Form\AbstractType;
1515
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
16+
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormWithIndexListener;
1617
use Symfony\Component\Form\FormBuilderInterface;
1718
use Symfony\Component\Form\FormInterface;
1819
use Symfony\Component\Form\FormView;
@@ -45,7 +46,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
4546
$options['entry_options'],
4647
$options['allow_add'],
4748
$options['allow_delete'],
48-
$options['delete_empty']
49+
$options['delete_empty'],
50+
$options['index_name']
4951
);
5052

5153
$builder->addEventSubscriber($resizeListener);
@@ -126,6 +128,7 @@ public function configureOptions(OptionsResolver $resolver)
126128
? $previousValue
127129
: 'The collection is invalid.';
128130
},
131+
'index_name' => null,
129132
]);
130133

131134
$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Symfony\Component\Form\Tests\Extension\Core\EventListener;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\TextType;
7+
use Symfony\Component\Form\FormBuilderInterface;
8+
9+
/**
10+
* Class FooType
11+
*
12+
* @package Symfony\Component\Form\Tests\Extension\Core\EventListener
13+
* @author Patrick Bußmann <patrick.bussmann@bussmann-it.de>
14+
*/
15+
class FooType extends AbstractType
16+
{
17+
public function buildForm(FormBuilderInterface $builder, array $options)
18+
{
19+
$builder
20+
->add('foo', TextType::class)
21+
;
22+
}
23+
}

src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
use Symfony\Component\EventDispatcher\EventDispatcher;
1717
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
1818
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
19+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
20+
use Symfony\Component\Form\Extension\Core\Type\FormType;
1921
use Symfony\Component\Form\Extension\Core\Type\TextType;
2022
use Symfony\Component\Form\FormBuilder;
2123
use Symfony\Component\Form\FormEvent;
2224
use Symfony\Component F987 \Form\FormFactoryBuilder;
25+
use Symfony\Component\Form\Tests\Extension\Core\EventListener\FooType;
2326

2427
class ResizeFormListenerTest extends TestCase
2528
{
@@ -66,6 +69,21 @@ public function testPreSetDataResizesForm()
6669
$this->assertTrue($this->form->has('2'));
6770
}
6871

72+
public function testPreSetDataResizesFormWithIndexedName()
73+
{
74+
$this->form->add($this->getForm('my-id-0'));
75+
$this->form->add($this->getForm('my-id-1'));
76+
77+
$data = ['my-id-2' => 'string', 'my-id-1' => 'string xy'];
78+
$event = new FormEvent($this->form, $data);
79+
$listener = new ResizeFormListener(TextType::class, ['attr' => ['maxlength' => 10]], false, false);
80+
$listener->preSetData($event);
81+
82+
$this->assertFalse($this->form->has('my-id-0'));
83+
$this->assertTrue($this->form->has('my-id-1'));
84+
$this->assertTrue($this->form->has('my-id-2'));
85+
}
86+
6987
public function testPreSetDataRequiresArrayOrTraversable()
7088
{
7189
$this->expectException('Symfony\Component\Form\Exception\UnexpectedTypeException');
@@ -187,6 +205,18 @@ public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDelete(
187205
$this->assertEquals([1 => 'second'], $event->getData());
188206
}
189207

208+
public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDeleteWithIndexedName()
209+
{
210+
$this->form->add($this->getForm('my-id-1'));
211+
212+
$data = ['my-id-0' => 'first', 'my-id-1' => 'second', 'my-id-2' => 'third'];
213+
$event = new FormEvent($this->form, $data);
214+
$listener = new ResizeFormListener('text', [], false, true);
215+
$listener->onSubmit($event);
216+
217+
$this->assertEquals(['my-id-1' => 'second'], $event->getData());
218+
}
219+
190220
public function testOnSubmitNormDataDoesNothingIfNotAllowDelete()
191221
{
192222
$this->form->add($this->getForm('1'));
@@ -292,4 +322,97 @@ public function testOnSubmitDeleteEmptyCompoundEntriesIfAllowDelete()
292322

293323
$this->assertEquals(['0' => ['name' => 'John']], $event->getData());
294324
}
325+
326+
public function testOnSubmitDeleteEmptyCompoundEntriesIfAllowDeleteWithIndexedName()
327+
{
328+
$this->form->setData(['my-id-1' => ['name' => 'John'], 'my-id-2' => ['name' => 'Jane']]);
329+
$form1 = $this->getBuilder('my-id-1')
330+
->setCompound(true)
331+
->setDataMapper(new PropertyPathMapper())
332+
->getForm();
333+
$form1->add($this->getForm('name'));
334+
$form2 = $this->getBuilder('my-id-2')
335+
->setCompound(true)
336+
->setDataMapper(new PropertyPathMapper())
337+
->getForm();
338+
$form2->add($this->getForm('name'));
339+
$this->form->add($form1);
340+
$this->form->add($form2);
341+
342+
$data = ['my-id-1' => ['name' => 'John'], 'my-id-2' => ['name' => '']];
343+
foreach ($data as $child => $dat) {
344+
$this->form->get($child)->setData($dat);
345+
}
346+
$event = new FormEvent($this->form, $data);
347+
$callback = function ($data) {
348+
return '' === $data['name'];
349+
};
350+
$listener = new ResizeFormListener('text', [], false, true, $callback);
351+
$listener->onSubmit($event);
352+
353+
$this->assertEquals(['my-id-1' => ['name' => 'John']], $event->getData());
354+
}
355+
356+
public function testIndexedNameFeature()
357+
{
358+
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
359+
->add('items', CollectionType::class, [
360+
'entry_type' => TextType::class,
361+
'allow_add' => true,
362+
'data' => ['foo'],
363+
'index_name' => 'id',
364+
])
365+
->getForm()
366+
;
367+
368+
$this->assertSame(['foo'], $form->get('items')->getData());
369+
$form->submit(['items' => ['foo', 'my-id-1' => 'foo', 'my-id-2' => 'bar']]);
370+
$this->assertSame(['foo', 'my-id-1' => 'foo', 'my-id-2' => 'bar'], $form->get('items')->getData());
371+
}
372+
373+
public function testIndexedNameFeatureWithAllowDelete()
374+
{
375+
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
376+
->add('items', CollectionType::class, [
377+
'entry_type' => TextType::class,
378+
'allow_add' => true,
379+
'allow_delete' => true,
380+
'data' => ['foo'],
381+
'index_name' => 'id',
382+
])
383+
->getForm()
384+
;
385+
386+
$this->assertSame(['foo'], $form->get('items')->getData());
387+
$form->submit(['items' => ['my-id-1' => 'foo', 'my-id-2' => 'bar']]);
388+
$this->assertSame(['my-id-1' => 'foo', 'my-id-2' => 'bar'], $form->get('items')->getData());
389+
}
390+
391+
public function testIndexedNameFeatureWithSimulatedArray()
392+
{
393+
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
394+
->add('items', CollectionType::class, [
395+
'entry_type' => FooType::class,
396+
'allow_add' => true,
397+
'allow_delete' => true,
398+
'data' => $data = [
399+
['id' => 'custom-id-1', 'foo' => 'bar'],
400+
['id' => 'custom-id-2', 'foo' => 'me'],
401+
['id' 985C => 'custom-id-3', 'foo' => 'foo']
402+
],
403+
'index_name' => 'id',
404+
])
405+
->getForm()
406+
;
407+
408+
$this->assertSame($data, $form->get('items')->getData());
409+
$form->submit(['items' => [
410+
'custom-id-3' => ['foo' => 'foo 2'],
411+
'custom-id-1' => ['foo' => 'bar 2']
412+
]]);
413+
$this->assertSame([
414+
'custom-id-1' => ['id' => 'custom-id-1', 'foo' => 'bar 2'],
415+
'custom-id-3' => ['id' => 'custom-id-3', 'foo' => 'foo 2']
416+
], $form->get('items')->getData());
417+
}
295418
}

0 commit comments

Comments
 (0)
0