Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit37634c6

Browse files
committed
feature#39438 [Form] Errors Property Paths mismatch CollectionType children when removing an entry (cristoforocervino)
This PR was squashed before being merged into the 7.1 branch.Discussion----------[Form] Errors Property Paths mismatch CollectionType children when removing an entry| Q | A| ------------- | ---| Branch? | 7.1| Bug fix? | no| New feature? | yes| Deprecations? | no| Tickets | -| License | MIT| Doc PR | -**Description**When you remove an entry from `CollectionType` and you have errors on children, errors property paths generated by `ValidatorExtension` are matching final object collection indexes but do not match From children entries index/names.**How to reproduce**_Unit test provided._**Solution**Introduce new feature with `keep_as_list` (`boolean` default to `false`) option to reindex children on submit.Commits-------39587cb [Form] Errors Property Paths mismatch CollectionType children when removing an entry
2 parentsc715b55 +39587cb commit37634c6

File tree

5 files changed

+238
-2
lines changed

5 files changed

+238
-2
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
protectedbool$allowDelete;
3232

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

35-
publicfunction__construct(string$type,array$options = [],bool$allowAdd =false,bool$allowDelete =false,bool|callable$deleteEmpty =false,array$prototypeOptions =null)
36+
publicfunction__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
publicstaticfunctiongetSubscribedEvents():array
@@ -153,6 +155,20 @@ public function onSubmit(FormEvent $event): void
153155
}
154156
}
155157

158+
if ($this->keepAsList) {
159+
$formReindex = [];
160+
foreach ($formas$name =>$child) {
161+
$formReindex[] =$child;
162+
$form->remove($name);
163+
}
164+
foreach ($formReindexas$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
publicfunctiongetBlockPrefix():string

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

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
useSymfony\Component\Form\Form;
1616
useSymfony\Component\Form\Forms;
1717
useSymfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
18+
useSymfony\Component\Form\Tests\Extension\Core\Type\CollectionTypeTest;
1819
useSymfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest;
1920
useSymfony\Component\Form\Tests\Extension\Core\Type\TextTypeTest;
2021
useSymfony\Component\Form\Tests\Fixtures\Author;
22+
useSymfony\Component\Form\Tests\Fixtures\AuthorType;
23+
useSymfony\Component\Form\Tests\Fixtures\Organization;
2124
useSymfony\Component\OptionsResolver\Exception\InvalidOptionsException;
2225
useSymfony\Component\Validator\Constraints\GroupSequence;
2326
useSymfony\Component\Validator\Constraints\Length;
@@ -158,4 +161,185 @@ protected function createForm(array $options = [])
158161
{
159162
return$this->factory->create(FormTypeTest::TESTED_TYPE,null,$options);
160163
}
164+
165+
publicfunctiontestCollectionTypeKeepAsListOptionFalse()
166+
{
167+
$formMetadata =newClassMetadata(Form::class);
168+
$authorMetadata = (newClassMetadata(Author::class))
169+
->addPropertyConstraint('firstName',newNotBlank());
170+
$organizationMetadata = (newClassMetadata(Organization::class))
171+
->addPropertyConstraint('authors',newValid());
172+
$metadataFactory =$this->createMock(MetadataFactoryInterface::class);
173+
$metadataFactory->expects($this->any())
174+
->method('getMetadataFor')
175+
->willReturnCallback(staticfunction ($classOrObject)use ($formMetadata,$authorMetadata,$organizationMetadata) {
176+
if (Author::class ===$classOrObject ||$classOrObjectinstanceof Author) {
177+
return$authorMetadata;
178+
}
179+
180+
if (Organization::class ===$classOrObject ||$classOrObjectinstanceof Organization) {
181+
return$organizationMetadata;
182+
}
183+
184+
if (Form::class ===$classOrObject ||$classOrObjectinstanceof Form) {
185+
return$formMetadata;
186+
}
187+
188+
returnnewClassMetadata(\is_string($classOrObject) ?$classOrObject :$classOrObject::class);
189+
});
190+
191+
$validator = Validation::createValidatorBuilder()
192+
->setMetadataFactory($metadataFactory)
193+
->getValidator();
194+
195+
$form = Forms::createFormFactoryBuilder()
196+
->addExtension(newValidatorExtension($validator))
197+
->getFormFactory()
198+
->create(FormTypeTest::TESTED_TYPE,newOrganization([]), [
199+
'data_class' => Organization::class,
200+
'by_reference' =>false,
201+
])
202+
->add('authors', CollectionTypeTest::TESTED_TYPE, [
203+
'entry_type' => AuthorType::class,
204+
'allow_add' =>true,
205+
'allow_delete' =>true,
206+
'keep_as_list' =>false,
207+
])
208+
;
209+
210+
$form->submit([
211+
'authors' => [
212+
0 => [
213+
'firstName' =>'',// Fires a Not Blank Error
214+
'lastName' =>'lastName1',
215+
],
216+
// key "1" could be missing if we add 4 blank form entries and then remove it.
217+
2 => [
218+
'firstName' =>'',// Fires a Not Blank Error
219+
'lastName' =>'lastName3',
220+
],
221+
3 => [
222+
'firstName' =>'',// Fires a Not Blank Error
223+
'lastName' =>'lastName3',
224+
],
225+
],
226+
]);
227+
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+
240+
$this->assertTrue($form->get('authors')->has('0'));
241+
$this->assertContains('data.authors[0].firstName',$errorPaths);
242+
243+
$this->assertFalse($form->get('authors')->has('1'));
244+
$this->assertContains('data.authors[1].firstName',$errorPaths);
245+
246+
$this->assertTrue($form->get('authors')->has('2'));
247+
$this->assertContains('data.authors[2].firstName',$errorPaths);
248+
249+
$this->assertTrue($form->get('authors')->has('3'));
250+
$this->assertNotContains('data.authors[3].firstName',$errorPaths);
251+
252+
// As result, root form contain errors
253+
$this->assertCount(1,$form->getErrors(false));
254+
}
255+
256+
publicfunctiontestCollectionTypeKeepAsListOptionTrue()
257+
{
258+
$formMetadata =newClassMetadata(Form::class);
259+
$authorMetadata = (newClassMetadata(Author::class))
260+
->addPropertyConstraint('firstName',newNotBlank());
261+
$organizationMetadata = (newClassMetadata(Organization::class))
262+
->addPropertyConstraint('authors',newValid());
263+
$metadataFactory =$this->createMock(MetadataFactoryInterface::class);
264+
$metadataFactory->expects($this->any())
265+
->method('getMetadataFor')
266+
->willReturnCallback(staticfunction ($classOrObject)use ($formMetadata,$authorMetadata,$organizationMetadata) {
267+
if (Author::class ===$classOrObject ||$classOrObjectinstanceof Author) {
268+
return$authorMetadata;
269+
}
270+
271+
if (Organization::class ===$classOrObject ||$classOrObjectinstanceof Organization) {
272+
return$organizationMetadata;
273+
}
274+
275+
if (Form::class ===$classOrObject ||$classOrObjectinstanceof Form) {
276+
return$formMetadata;
277+
}
278+
279+
returnnewClassMetadata(\is_string($classOrObject) ?$classOrObject :$classOrObject::class);
280+
});
281+
282+
$validator = Validation::createValidatorBuilder()
283+
->setMetadataFactory($metadataFactory)
284+
->getValidator();
285+
286+
$form = Forms::createFormFactoryBuilder()
287+
->addExtension(newValidatorExtension($validator))
288+
->getFormFactory()
289+
->create(FormTypeTest::TESTED_TYPE,newOrganization([]), [
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
320+
$errors =$form->getErrors(true);
321+
$this->assertCount(3,$errors);
322+
323+
// No property paths mismatch happening with "keep_as_list" option set to true
324+
$errorPaths = [
325+
$errors[0]->getCause()->getPropertyPath(),
326+
$errors[1]->getCause()->getPropertyPath(),
327+
$errors[2]->getCause()->getPropertyPath(),
328+
];
329+
330+
$this->assertTrue($form->get('authors')->has('0'));
331+
$this->assertContains('data.authors[0].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'));
337+
$this->assertContains('data.authors[2].firstName',$errorPaths);
338+
339+
$this->assertFalse($form->get('authors')->has('3'));
340+
$this->assertNotContains('data.authors[3].firstName',$errorPaths);
341+
342+
// Root form does NOT contain errors
343+
$this->assertCount(0,$form->getErrors(false));
344+
}
161345
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespaceSymfony\Component\Form\Tests\Fixtures;
4+
5+
class Organization
6+
{
7+
public$authors = [];
8+
9+
publicfunction__construct(array$authors = [])
10+
{
11+
$this->authors =$authors;
12+
}
13+
14+
publicfunctiongetAuthors():array
15+
{
16+
return$this->authors;
17+
}
18+
19+
publicfunctionaddAuthor(Author$author):self
20+
{
21+
$this->authors[] =$author;
22+
return$this;
23+
}
24+
25+
publicfunctionremoveAuthor(Author$author):self
26+
{
27+
if (false !==$key =array_search($author,$this->authors,true)) {
28+
array_splice($this->authors,$key,1);
29+
}
30+
return$this;
31+
}
32+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp