Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f31de2b

Browse files
cristoforocervinoCristoforo Cervino
authored andcommitted
add keepAsList option to ResizeFormListener
1 parent 5bd334d commit f31de2b

File tree

4 files changed

+124
-11
lines changed

4 files changed

+124
-11
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0
8+
* Add a `keep_as_list` option to `CollectionType`
89

910
7.0
1011
---

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,17 @@ class ResizeFormListener implements EventSubscriberInterface
3131
protected bool $allowDelete;
3232

3333
private \Closure|bool $deleteEmpty;
34+
private bool $keepAsList;
3435

35-
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, bool|callable $deleteEmpty = false, array $prototypeOptions = null)
36+
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, bool|callable $deleteEmpty = false, array $prototypeOptions = null, bool $keepAsList = false)
3637
{
3738
$this->type = $type;
3839
$this->allowAdd = $allowAdd;
3940
$this->allowDelete = $allowDelete;
4041
$this->options = $options;
4142
$this->deleteEmpty = \is_bool($deleteEmpty) ? $deleteEmpty : $deleteEmpty(...);
4243
$this->prototypeOptions = $prototypeOptions ?? $options;
44+
$this->keepAsList = $keepAsList;
4345
}
4446

4547
public static function getSubscribedEvents(): array
@@ -153,6 +155,20 @@ public function onSubmit(FormEvent $event): void
153155
}
154156
}
155157

158+
if ($this->keepAsList) {
159+
$formReindex = [];
160+
foreach ($form as $name => $child) {
161+
$formReindex[] = $child;
162+
$form->remove($name);
163+
}
164+
foreach ($formReindex as $index => $child) {
165+
$form->add($index, $this->type, array_replace([
166+
'property_path' => '['.$index.']',
167+
], $this->options));
168+
}
169+
$data = array_values($data);
170+
}
171+
156172
$event->setData($data);
157173
}
158174
}

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

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

5152
$builder->addEventSubscriber($resizeListener);
@@ -114,12 +115,14 @@ public function configureOptions(OptionsResolver $resolver): void
114115
'prototype_options' => [],
115116
'delete_empty' => false,
116117
'invalid_message' => 'The collection is invalid.',
118+
'keep_as_list' => false,
117119
]);
118120

119121
$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
120122

121123
$resolver->setAllowedTypes('delete_empty', ['bool', 'callable']);
122124
$resolver->setAllowedTypes('prototype_options', 'array');
125+
$resolver->setAllowedTypes('keep_as_list', ['bool']);
123126
}
124127

125128
public function getBlockPrefix(): string

src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
use Symfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest;
2020
use Symfony\Component\Form\Tests\Extension\Core\Type\TextTypeTest;
2121
use Symfony\Component\Form\Tests\Fixtures\Author;
22-
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
2322
use Symfony\Component\Form\Tests\Fixtures\AuthorType;
2423
use Symfony\Component\Form\Tests\Fixtures\Organization;
24+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
2525
use Symfony\Component\Validator\Constraints\GroupSequence;
2626
use Symfony\Component\Validator\Constraints\Length;
2727
use Symfony\Component\Validator\Constraints\NotBlank;
@@ -162,7 +162,7 @@ protected function createForm(array $options = [])
162162
return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options);
163163
}
164164

165-
public function testErrorPathOnCollections()
165+
public function testCollectionTypeKeepAsListOptionFalse()
166166
{
167167
$formMetadata = new ClassMetadata(Form::class);
168168
$authorMetadata = (new ClassMetadata(Author::class))
@@ -185,7 +185,7 @@ public function testErrorPathOnCollections()
185185
return $formMetadata;
186186
}
187187

188-
return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : \get_class($classOrObject));
188+
return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class);
189189
});
190190

191191
$validator = Validation::createValidatorBuilder()
@@ -203,6 +203,7 @@ public function testErrorPathOnCollections()
203203
'entry_type' => AuthorType::class,
204204
'allow_add' => true,
205205
'allow_delete' => true,
206+
'keep_as_list' => false,
206207
])
207208
;
208209

@@ -224,29 +225,121 @@ public function testErrorPathOnCollections()
224225
],
225226
]);
226227

227-
//Form behaves right (...?). It has index 0, 2 and 3 (1 has been removed)
228+
// Form does have 3 not blank errors
229+
$errors = $form->getErrors(true);
230+
$this->assertCount(3, $errors);
231+
232+
// Form behaves as expected. It has index 0, 2 and 3 (1 has been removed)
233+
// But errors property paths mismatch happening with "keep_as_list" option set to false
234+
$errorPaths = [
235+
$errors[0]->getCause()->getPropertyPath(),
236+
$errors[1]->getCause()->getPropertyPath(),
237+
$errors[2]->getCause()->getPropertyPath(),
238+
];
239+
228240
$this->assertTrue($form->get('authors')->has('0'));
241+
$this->assertContains('data.authors[0].firstName', $errorPaths);
242+
229243
$this->assertFalse($form->get('authors')->has('1'));
244+
$this->assertContains('data.authors[1].firstName', $errorPaths);
245+
230246
$this->assertTrue($form->get('authors')->has('2'));
247+
$this->assertContains('data.authors[2].firstName', $errorPaths);
248+
231249
$this->assertTrue($form->get('authors')->has('3'));
250+
$this->assertNotContains('data.authors[3].firstName', $errorPaths);
232251

233-
//Form does have 3 not blank errors
252+
// As result, root form contain errors
253+
$this->assertCount(1, $form->getErrors(false));
254+
}
255+
256+
public function testCollectionTypeKeepAsListOptionTrue()
257+
{
258+
$formMetadata = new ClassMetadata(Form::class);
259+
$authorMetadata = (new ClassMetadata(Author::class))
260+
->addPropertyConstraint('firstName', new NotBlank());
261+
$organizationMetadata = (new ClassMetadata(Organization::class))
262+
->addPropertyConstraint('authors', new Valid());
263+
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
264+
$metadataFactory->expects($this->any())
265+
->method('getMetadataFor')
266+
->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata, $organizationMetadata) {
267+
if (Author::class === $classOrObject || $classOrObject instanceof Author) {
268+
return $authorMetadata;
269+
}
270+
271+
if (Organization::class === $classOrObject || $classOrObject instanceof Organization) {
272+
return $organizationMetadata;
273+
}
274+
275+
if (Form::class === $classOrObject || $classOrObject instanceof Form) {
276+
return $formMetadata;
277+
}
278+
279+
return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class);
280+
});
281+
282+
$validator = Validation::createValidatorBuilder()
283+
->setMetadataFactory($metadataFactory)
284+
->getValidator();
285+
286+
$form = Forms::createFormFactoryBuilder()
287+
->addExtension(new ValidatorExtension($validator))
288+
->getFormFactory()
289+
->create(FormTypeTest::TESTED_TYPE, new Organization([]), [
290+
'data_class' => Organization::class,
291+
'by_reference' => false,
292+
])
293+
->add('authors', CollectionTypeTest::TESTED_TYPE, [
294+
'entry_type' => AuthorType::class,
295+
'allow_add' => true,
296+
'allow_delete' => true,
297+
'keep_as_list' => true,
298+
])
299+
;
300+
301+
$form->submit([
302+
'authors' => [
303+
0 => [
304+
'firstName' => '', // Fires a Not Blank Error
305+
'lastName' => 'lastName1',
306+
],
307+
// key "1" could be missing if we add 4 blank form entries and then remove it.
308+
2 => [
309+
'firstName' => '', // Fires a Not Blank Error
310+
'lastName' => 'lastName3',
311+
],
312+
3 => [
313+
'firstName' => '', // Fires a Not Blank Error
314+
'lastName' => 'lastName3',
315+
],
316+
],
317+
]);
318+
319+
// Form does have 3 not blank errors
234320
$errors = $form->getErrors(true);
235321
$this->assertCount(3, $errors);
236322

237-
//But errors property paths are messing up
323+
// No property paths mismatch happening with "keep_as_list" option set to true
238324
$errorPaths = [
239325
$errors[0]->getCause()->getPropertyPath(),
240326
$errors[1]->getCause()->getPropertyPath(),
241327
$errors[2]->getCause()->getPropertyPath(),
242328
];
243329

330+
$this->assertTrue($form->get('authors')->has('0'));
244331
$this->assertContains('data.authors[0].firstName', $errorPaths);
245-
$this->assertNotContains('data.authors[1].firstName', $errorPaths);
332+
333+
$this->assertTrue($form->get('authors')->has('1'));
334+
$this->assertContains('data.authors[1].firstName', $errorPaths);
335+
336+
$this->assertTrue($form->get('authors')->has('2'));
246337
$this->assertContains('data.authors[2].firstName', $errorPaths);
247-
$this->assertContains('data.authors[3].firstName', $errorPaths);
248338

249-
//In fact, root form should NOT contain errors but it does
339+
$this->assertFalse($form->get('authors')->has('3'));
340+
$this->assertNotContains('data.authors[3].firstName', $errorPaths);
341+
342+
// Root form does NOT contain errors
250343
$this->assertCount(0, $form->getErrors(false));
251344
}
252345
}

0 commit comments

Comments
 (0)