8000 Allow array keys for collection types by patrickbussmann · Pull Request #37646 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

Allow array keys for collection types #37646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ CHANGELOG
* Deprecated `Symfony\Component\Form\Extension\Validator\Util\ServerParams` in favor of its parent class `Symfony\Component\Form\Util\ServerParams`
* Added the `html5` option to the `ColorType` to validate the input
* Deprecated `NumberToLocalizedStringTransformer::ROUND_*` constants, use `\NumberFormatter::ROUND_*` instead
* Added `index_name` for `CollectionType` which allows submitting ke 8000 ys in collections

5.0.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,32 @@
* Resize a collection form element based on the data sent from the client.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Patrick Bußmann <patrick.bussmann@bussmann-it.de>
*/
class ResizeFormListener implements EventSubscriberInterface
{
protected $type;
protected $options;
protected $allowAdd;
protected $allowDelete;
protected $indexName;

private $deleteEmpty;

/**
* @param bool $allowAdd Whether children could be added to the group
* @param bool $allowDelete Whether children could be removed from the group
* @param bool|callable $deleteEmpty
* @param bool $allowAdd Whether children could be added to the group
* @param bool $allowDelete Whether children could be removed from the group
* @param bool|callable $deleteEmpty
* @param string|callable|null $indexName
*/
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false)
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false, $indexName = null)
{
$this->type = $type;
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
$this->options = $options;
$this->deleteEmpty = $deleteEmpty;
$this->indexName = $indexName;
}

public static function getSubscribedEvents()
Expand Down Expand Up @@ -75,7 +79,7 @@ public function preSetData(FormEvent $event)

// Then add all rows again in the correct order
foreach ($data as $name => $value) {
$form->add($name, $this->type, array_replace([
$form->add($this->getIndexFromValue($value, $name), $this->type, array_replace([
'property_path' => '['.$name.']',
], $this->options));
}
Expand All @@ -102,9 +106,10 @@ public function preSubmit(FormEvent $event)
// Add all additional rows
if ($this->allowAdd) {
foreach ($data as $name => $value) {
if (!$form->has($name)) {
$form->add($name, $this->type, array_replace([
'property_path' => '['.$name.']',
$indexedName = $this->getIndexFromValue($value, $name);
if (!$form->has($indexedName)) {
$form->add($indexedName, $this->type, array_replace([
'property_path' => '['.$indexedName.']',
], $this->options));
}
}
Expand Down Expand Up @@ -150,7 +155,8 @@ public function onSubmit(FormEvent $event)
$toDelete = [];

foreach ($data as $name => $child) {
if (!$form->has($name)) {
$indexedName = $this->getIndexFromValue($child, $name);
if (!$form->has($indexedName)) {
$toDelete[] = $name;
}
}
Expand All @@ -160,6 +166,27 @@ public function onSubmit(FormEvent $event)
}
}

$event->setData($data);
if (null !== $this->indexName) {
$newData = [];
foreach ($form->all() as $item) {
$newData[$item->getName()] = $item->getData();
}
$event->setData($newData);
} else {
$event->setData($data);
}
}

private function getIndexFromValue($value, $defaultValue = null)
{
if (null === $this->indexName || null === $value) {
return $defaultValue;
}
$indexGetter = 'get'.ucfirst($this->indexName);

return (\is_callable($this->indexName) ? ($this->indexName)($value, $defaultValue)
: (method_exists($value, $indexGetter) ? $value->$indexGetter()
: (\is_array($value) && \array_key_exists($this->indexName, $value)
? $value[$this->indexName] : null))) ?: $defaultValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$options['entry_options'],
$options['allow_add'],
$options['allow_delete'],
$options['delete_empty']
$options['delete_empty'],
$options['index_name']
);

$builder->addEventSubscriber($resizeListener);
Expand Down Expand Up @@ -126,6 +127,7 @@ public function configureOptions(OptionsResolver $resolver)
? $previousValue
: 'The collection is invalid.';
},
'index_name' => null,
]);

$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Symfony\Component\Form\Tests\Extension\Core\EventListener;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

/**
* Class FooType.
*
* @author Patrick Bußmann <patrick.bussmann@bussmann-it.de>
*/
class FooType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('foo', TextType::class)
;
}
}
9A82
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormEvent;
Expand Down Expand Up @@ -66,6 +68,21 @@ public function testPreSetDataResizesForm()
$this->assertTrue($this->form->has('2'));
}

public function testPreSetDataResizesFormWithIndexedName()
{
$this->form->add($this->getForm('my-id-0'));
$this->form->add($this->getForm('my-id-1'));

$data = ['my-id-2' => 'string', 'my-id-1' => 'string xy'];
$event = new FormEvent($this->form, $data);
$listener = new ResizeFormListener(TextType::class, ['attr' => ['maxlength' => 10]], false, false);
$listener->preSetData($event);

$this->assertFalse($this->form->has('my-id-0'));
$this->assertTrue($this->form->has('my-id-1'));
$this->assertTrue($this->form->has('my-id-2'));
}

public function testPreSetDataRequiresArrayOrTraversable()
{
$this->expectException('Symfony\Component\Form\Exception\UnexpectedTypeException');
Expand Down Expand Up @@ -187,6 +204,18 @@ public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDelete(
$this->assertEquals([1 => 'second'], $event->getData());
}

public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDeleteWithIndexedName()
{
$this->form->add($this->getForm('my-id-1'));

$data = ['my-id-0' => 'first', 'my-id-1' => 'second', 'my-id-2' => 'third'];
$event = new FormEvent($this->form, $data);
$listener = new ResizeFormListener('text', [], false, true);
$listener->onSubmit($event);

$this->assertEquals(['my-id-1' => 'second'], $event->getData());
}

public function testOnSubmitNormDataDoesNothingIfNotAllowDelete()
{
$this->form->add($this->getForm('1'));
Expand Down Expand Up @@ -292,4 +321,97 @@ public function testOnSubmitDeleteEmptyCompoundEntriesIfAllowDelete()

$this->assertEquals(['0' => ['name' => 'John']], $event->getData());
}

public function testOnSubmitDeleteEmptyCompoundEntriesIfAllowDeleteWithIndexedName()
{
$this->form->setData(['my-id-1' => ['name' => 'John'], 'my-id-2' => ['name' => 'Jane']]);
$form1 = $this->getBuilder('my-id-1')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->getForm();
$form1->add($this->getForm('name'));
$form2 = $this->getBuilder('my-id-2')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->getForm();
$form2->add($this->getForm('name'));
$this->form->add($form1);
$this->form->add($form2);

$data = ['my-id-1' => ['name' => 'John'], 'my-id-2' => ['name' => '']];
foreach ($data as $child => $dat) {
$this->form->get($child)->setData($dat);
}
$event = new FormEvent($this->form, $data);
$callback = function ($data) {
return '' === $data['name'];
};
$listener = new ResizeFormListener('text', [], false, true, $callback);
$listener->onSubmit($event);

$this->assertEquals(['my-id-1' => ['name' => 'John']], $event->getData());
}

public function testIndexedNameFeature()
{
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
->add('items', CollectionType::class, [
'entry_type' => TextType::class,
'allow_add' => true,
'data' => ['foo'],
'index_name' => 'id',
])
->getForm()
;

$this->assertSame(['foo'], $form->get('items')->getData());
$form->submit(['items' => ['foo', 'my-id-1' => 'foo', 'my-id-2' => 'bar']]);
$this->assertSame(['foo', 'my-id-1' => 'foo', 'my-id-2' => 'bar'], $form->get('items')->getData());
}

public function testIndexedNameFeatureWithAllowDelete()
{
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
->add('items', CollectionType::class, [
'entry_type' => TextType::class,
'allow_add' => true,
'allow_delete' => true,
'data' => ['foo'],
'index_name' => 'id',
])
->getForm()
;

$this->assertSame(['foo'], $form->get('items')->getData());
$form->submit(['items' => ['my-id-1' => 'foo', 'my-id-2' => 'bar']]);
$this->assertSame(['my-id-1' => 'foo', 'my-id-2' => 'bar'], $form->get('items')->getData());
}

public function testIndexedNameFeatureWithSimulatedArray()
{
$form = $this->factory->createNamedBuilder('root', FormType::class, ['items' => null])
->add('items', CollectionType::class, [
'entry_type' => FooType::class,
'allow_add' => true,
'allow_delete' => true,
'data' => $data = [
['id' => 'custom-id-1', 'foo' => 'bar'],
['id' => 'custom-id-2', 'foo' => 'me'],
['id' => 'custom-id-3', 'foo' => 'foo'],
],
'index_name' => 'id',
])
->getForm()
;

$this->assertSame($data, $form->get('items')->getData());
$form->submit(['items' => [
'custom-id-3' => ['foo' => 'foo 2'],
'custom-id-1' => ['foo' => 'bar 2'],
]]);
$this->assertSame([
'custom-id-1' => ['id' => 'custom-id-1', 'foo' => 'bar 2'],
'custom-id-3' => ['id' => 'custom-id-3', 'foo' => 'foo 2'],
], $form->get('items')->getData());
}
}
0