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

Skip to content

Commit e30f01c

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

File tree

5 files changed

+185
-11
lines changed

5 files changed

+185
-11
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ CHANGELOG
2020
* Deprecated `Symfony\Component\Form\Extension\Validator\Util\ServerParams` in favor of its parent class `Symfony\Component\Form\Util\ServerParams`
2121
* Added the `html5` option to the `ColorType` to validate the input
2222
* Deprecated `NumberToLocalizedStringTransformer::ROUND_*` constants, use `\NumberFormatter::ROUND_*` instead
23+
* Added `index_name` for `CollectionType` which allows submitting keys in collections
2324

2425
5.0.0
2526
-----

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

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,32 @@
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 bool|callable $deleteEmpty
37+
* @param bool $allowAdd Whether children could be added to the group
38+
* @param bool $allowDelete Whether children could be removed from the group
39+
* @param bool|callable $deleteEmpty
40+
* @param string|callable|null $indexName
3841
*/
39-
public function __construct(string $type, array $options = [], bool $allowAdd = false 6D47 , bool $allowDelete = false, $deleteEmpty = false)
42+
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false, $indexName = null)
4043
{
4144
$this->type = $type;
4245
$this->allowAdd = $allowAdd;
4346
$this->allowDelete = $allowDelete;
4447
$this->options = $options;
4548
$this->deleteEmpty = $deleteEmpty;
49+
$this->indexName = $indexName;
4650
}
4751

4852
public static function getSubscribedEvents()
@@ -75,7 +79,7 @@ public function preSetData(FormEvent $event)
7579

7680
// Then add all rows again in the correct order
7781
foreach ($data as $name => $value) {
78-
$form->add($name, $this->type, array_replace([
82+
$form->add($this->getIndexFromValue($value, $name), $this->type, array_replace([
7983
9E81 'property_path' => '['.$name.']',
8084
], $this->options));
8185
}
@@ -102,9 +106,10 @@ public function preSubmit(FormEvent $event)
102106
// Add all additional rows
103107
if ($this->allowAdd) {
104108
foreach ($data as $name => $value) {
105-
if (!$form->has($name)) {
106-
$form->add($name, $this->type, array_replace([
107-
'property_path' => '['.$name.']',
109+
$indexedName = $this->getIndexFromValue($value, $name);
110+
if (!$form->has($indexedName)) {
111+
$form->add($indexedName, $this->type, array_replace([
112+
'property_path' => '['.$indexedName.']',
108113
], $this->options));
109114
}
110115
}
@@ -150,7 +155,8 @@ public function onSubmit(FormEvent $event)
150155
$toDelete = [];
151156

152157
foreach ($data as $name => $child) {
153-
if (!$form->has($name)) {
158+
$indexedName = $this->getIndexFromValue($child, $name);
159+
if (!$form->has($indexedName)) {
154160
$toDelete[] = $name;
155161
}
156162
}
@@ -160,6 +166,27 @@ public function onSubmit(FormEvent $event)
160166
}
161167
}
162168

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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
4545
$options['entry_options'],
4646
$options['allow_add'],
4747
$options['allow_delete'],
48-
$options['delete_empty']
48+
$options['delete_empty'],
49+
$options['index_name']
4950
);
5051

5152
$builder->addEventSubscriber($resizeListener);
@@ -126,6 +127,7 @@ public function configureOptions(OptionsResolver $resolver)
126127
? $previousValue
127128
: 'The collection is invalid.';
128129
},
130+
'index_name' => null,
129131
]);
130132

131133
$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
* @author Patrick Bußmann <patrick.bussmann@bussmann-it.de>
13+
*/
14+
class FooType extends AbstractType
15+
{
16+
public function buildForm(FormBuilderInterface $builder, array $options)
17+
{
18+
$builder
19+
->add('foo', TextType::class)
20+
;
21+
}
22+
}

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

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
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< 55F span class="diff-text-marker">+
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;
@@ -66,6 +68,21 @@ public function testPreSetDataResizesForm()
6668
$this->assertTrue($this->form->has('2'));
6769
}
6870

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

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

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

0 commit comments

Comments
 (0)
0