diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index 96c1c8b0e2d90..e01e8ffb1ee96 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -24,6 +24,49 @@ Form * Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method is deprecated. The method will be added to the interface in 6.0. * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()` - not defining them is deprecated. + * Usage of `choice_attr` option as an array of nested arrays has been deprecated and indexes will be considered as attributes in 6.0. Use a unique array for all choices or a `callable` instead. + + Before: + ```php + // Single array for all choices using callable + 'choice_attr' => function () { + return ['class' => 'choice-options']; + }, + + // Different arrays per choice using array + 'choices' => [ + 'Yes' => true, + 'No' => false, + 'Maybe' => null, + ], + 'choice_attr' => [ + 'Yes' => ['class' => 'option-green'], + 'No' => ['class' => 'option-red'], + ], + ``` + + After: + ```php + // Single array for all choices using array + 'choice_attr' => ['class' => 'choice-options'], + + // Different arrays per choice using callable + 'choices' => [ + 'Yes' => true, + 'No' => false, + 'Maybe' => null, + ], + 'choice_attr' => function ($choice, $index, $value) { + if ('Yes' === $index) { + return ['class' => 'option-green']; + } + if ('No' === $index) { + return ['class' => 'option-red']; + } + + return []; + }, + ``` FrameworkBundle --------------- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 4180954165f54..a9cfcb20bed26 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -22,6 +22,50 @@ Form * Added the `getIsEmptyCallback()` method to the `FormConfigInterface`. * Added the `setIsEmptyCallback()` method to the `FormConfigBuilderInterface`. * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`. + * Usage of `choice_attr` option as an array of nested arrays has been removed + and indexes are considered as attributes. Use a unique array for all choices or a `callable` instead. + + Before: + ```php + // Single array for all choices using callable + 'choice_attr' => function () { + return ['class' => 'choice-options']; + }, + + // Different arrays per choice using array + 'choices' => [ + 'Yes' => true, + 'No' => false, + 'Maybe' => null, + ], + 'choice_attr' => [ + 'Yes' => ['class' => 'option-green'], + 'No' => ['class' => 'option-red'], + ], + ``` + + After: + ```php + // Single array for all choices using array + 'choice_attr' => ['class' => 'choice-options'], + + // Different arrays per choice using callable + 'choices' => [ + 'Yes' => true, + 'No' => false, + 'Maybe' => null, + ], + 'choice_attr' => function ($choice, $index, $value) { + if ('Yes' === $index) { + return ['class' => 'option-green']; + } + if ('No' === $index) { + return ['class' => 'option-red']; + } + + return []; + }, + ``` FrameworkBundle --------------- diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php index 3929877438132..ded940d72c090 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\Twig\Tests\Extension; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + abstract class AbstractBootstrap3HorizontalLayoutTest extends AbstractBootstrap3LayoutTest { public function testLabelOnForm() @@ -212,6 +214,42 @@ public function testCheckboxRowWithHelp() ./span[text() = "[trans]really helpful text[/trans]"] ] ] +' + ); + } + + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => false, + 'expanded' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], + '/div + [ + ./div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] ' ); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php index 1db827d194329..9f263269abe85 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Twig\Tests\Extension; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Tests\AbstractLayoutTest; @@ -531,8 +532,13 @@ public function testSingleChoiceWithPlaceholderWithoutTranslation() ); } - public function testSingleChoiceAttributes() + /** + * @group legacy + */ + public function testLegacySingleChoiceAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -554,6 +560,54 @@ public function testSingleChoiceAttributes() ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], +'/select + [@name="name"] + [@class="my&class form-control"] + [not(@required)] + [ + ./option[@value="&a"][@class="foo&bar"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleChoiceAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], +'/select + [@name="name"] + [@class="my&class form-control"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testSingleChoiceWithPreferred() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ @@ -847,8 +901,13 @@ public function testMultipleChoice() ); } - public function testMultipleChoiceAttributes() + /** + * @group legacy + */ + public function testLegacyMultipleChoiceAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -872,6 +931,58 @@ public function testMultipleChoiceAttributes() ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'required' => true, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], +'/select + [@name="name[]"] + [@class="my&class form-control"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@class="foo&bar"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testMultipleChoiceAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'required' => true, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], +'/select + [@name="name[]"] + [@class="my&class form-control"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testMultipleChoiceSkipsPlaceholder() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ @@ -1109,8 +1220,13 @@ public function testSingleChoiceExpandedWithoutTranslation() ); } - public function testSingleChoiceExpandedAttributes() + /** + * @group legacy + */ + public function testLegacySingleChoiceExpandedAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -1120,6 +1236,79 @@ public function testSingleChoiceExpandedAttributes() $this->assertWidgetMatchesXpath($form->createView(), [], '/div + [ + ./div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => false, + 'expanded' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' == $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => false, + 'expanded' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div [ ./div [@class="radio"] @@ -1484,8 +1673,13 @@ public function testMultipleChoiceExpandedWithoutTranslation() ); } - public function testMultipleChoiceExpandedAttributes() + /** + * @group legacy + */ + public function testLegacyMultipleChoiceExpandedAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -1530,6 +1724,100 @@ public function testMultipleChoiceExpandedAttributes() ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => true, + 'expanded' => true, + 'required' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./div + [@class="checkbox"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.=" [trans]Choice&C[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => true, + 'expanded' => true, + 'required' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./div + [@class="checkbox"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.=" [trans]Choice&C[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CountryType', 'AT'); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php index c2ab198e746cb..d8d248a1f0909 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php @@ -639,8 +639,13 @@ public function testSingleChoiceExpandedWithoutTranslation() ); } - public function testSingleChoiceExpandedAttributes() + /** + * @group legacy + */ + public function testLegacySingleChoiceExpandedAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -648,13 +653,79 @@ public function testSingleChoiceExpandedAttributes() 'expanded' => true, ]); + $this->assertWidgetMatchesXpath($form->createView(), [], + '/div + [ + ./div + [@class="form-check"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked][@class="form-check-input"] + /following-sibling::label + [.="[trans]Choice&A[/trans]"] + ] + /following-sibling::div + [@class="form-check"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&B[/trans]"] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => false, + 'expanded' => true, + ]); + $this->assertWidgetMatchesXpath($form->createView(), [], '/div [ ./div [@class="form-check"] [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&A[/trans]"] + ] + /following-sibling::div + [@class="form-check"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&B[/trans]"] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testSingleChoiceExpandedAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' == $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => false, + 'expanded' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], + '/div + [ + ./div + [@class="form-check"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked][@class="form-check-input"] /following-sibling::label [.="[trans]Choice&A[/trans]"] ] @@ -968,8 +1039,13 @@ public function testMultipleChoiceExpandedWithoutTranslation() ); } - public function testMultipleChoiceExpandedAttributes() + /** + * @group legacy + */ + public function testLegacyMultipleChoiceExpandedAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a', '&c'], [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -984,7 +1060,7 @@ public function testMultipleChoiceExpandedAttributes() ./div [@class="form-check"] [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)][@class="form-check-input"] /following-sibling::label [.="[trans]Choice&A[/trans]"] ] @@ -1008,6 +1084,88 @@ public function testMultipleChoiceExpandedAttributes() ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a', '&c'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => true, + 'expanded' => true, + 'required' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./div + [@class="form-check"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&A[/trans]"] + ] + /following-sibling::div + [@class="form-check"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&B[/trans]"] + ] + /following-sibling::div + [@class="form-check"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&C[/trans]"] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + + public function testMultipleChoiceExpandedAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a', '&c'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => true, + 'expanded' => true, + 'required' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], + '/div + [ + ./div + [@class="form-check"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)][@class="form-check-input"] + /following-sibling::label + [.="[trans]Choice&A[/trans]"] + ] + /following-sibling::div + [@class="form-check"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar form-check-input"] + /following-sibling::label + [.="[trans]Choice&B[/trans]"] + ] + /following-sibling::div + [@class="form-check"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)][@class="form-check-input"] + /following-sibling::label + [.="[trans]Choice&C[/trans]"] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + public function testCheckedRadio() { $form = $this->factory->createNamed('name', RadioType::class, true); diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index e22e5826fb149..e8e9d7d9afb1d 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 5.1.0 ----- + * Deprecated `choice_attr` option as array of nested arrays mapped by indexes * Added `collection_entry` block prefix to `CollectionType` entries * Added a `choice_filter` option to `ChoiceType` * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()` - not defining them is deprecated. diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index 45d3d046bd36e..f63ee9dab4805 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -77,6 +77,21 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $choices = $list->getChoices(); $keys = $list->getOriginalKeys(); + // BC, to be removed in 6.0 + if (\is_array($attr)) { + foreach ($attr as $choiceIndex => $choiceAttr) { + if (\is_array($choiceAttr) && false !== array_search($choiceIndex, $keys)) { + trigger_deprecation('symfony/form', '5.1', 'Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + + $attr = static function ($choice, $index) use ($attr) { + return $attr[$index] ?? []; + }; + } + + break; + } + } + if (!\is_callable($preferredChoices)) { if (empty($preferredChoices)) { $preferredChoices = null; @@ -184,9 +199,8 @@ private static function addChoiceView($choice, string $value, $label, array $key $choice, $value, $label, - // The attributes may be a callable or a mapping from choice indices - // to nested arrays - \is_callable($attr) ? $attr($choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : []) + // The attributes may be a callable or a unique array + \is_callable($attr) ? $attr($choice, $key, $value) : (null !== $attr ? $attr : []) ); // $isPreferred may be null if no choices are preferred diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 9c5594bcb8dd6..f75841c48b043 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Form\Tests; use PHPUnit\Framework\SkippedTestError; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Csrf\CsrfExtension; @@ -21,6 +23,7 @@ abstract class AbstractLayoutTest extends FormIntegrationTestCase { + use ExpectDeprecationTrait; use VersionAwareTest; protected $csrfTokenManager; @@ -656,8 +659,13 @@ public function testSingleChoiceWithPlaceholderWithoutTranslation() ); } - public function testSingleChoiceAttributes() + /** + * @group legacy + */ + public function testLegacySingleChoiceAttributes() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], @@ -725,6 +733,52 @@ public function testSingleExpandedChoiceAttributesWithMainAttributes() ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/select + [@name="name"] + [not(@required)] + [ + ./option[@value="&a"][@class="foo&bar"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleChoiceAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/select + [@name="name"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testSingleChoiceWithPreferred() { $this->requiresFeatureSet(404); @@ -996,7 +1050,12 @@ public function testMultipleChoice() ); } - public function testMultipleChoiceAttributes() + /** + * @group legacy + * + * @expectedDeprecation Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead. + */ + public function testLegacyMultipleChoiceAttributes() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], @@ -1020,6 +1079,56 @@ public function testMultipleChoiceAttributes() ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'required' => true, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/select + [@name="name[]"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@class="foo&bar"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testMultipleChoiceAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'required' => true, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/select + [@name="name[]"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testMultipleChoiceSkipsPlaceholder() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ @@ -1110,7 +1219,12 @@ public function testSingleChoiceExpandedWithoutTranslation() ); } - public function testSingleChoiceExpandedAttributes() + /** + * @group legacy + * + * @expectedDeprecation Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead. + */ + public function testLegacySingleChoiceExpandedAttributes() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], @@ -1133,6 +1247,54 @@ public function testSingleChoiceExpandedAttributes() ); } + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => false, + 'expanded' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@class="foo&bar"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=3] +' + ); + } + + public function testSingleChoiceExpandedAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => false, + 'expanded' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=3] +' + ); + } + public function testSingleChoiceExpandedWithPlaceholder() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ @@ -1259,7 +1421,12 @@ public function testMultipleChoiceExpandedWithoutTranslation() ); } - public function testMultipleChoiceExpandedAttributes() + /** + * @group legacy + * + * @expectedDeprecation Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead. + */ + public function testLegacyMultipleChoiceExpandedAttributes() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], @@ -1285,6 +1452,60 @@ public function testMultipleChoiceExpandedAttributes() ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a', '&c'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], + 'choice_attr' => ['class' => 'foo&bar'], + 'multiple' => true, + 'expanded' => true, + 'required' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@class="foo&bar"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@class="foo&bar"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=4] +' + ); + } + + public function testMultipleChoiceExpandedAttributesSetByCallable() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a', '&c'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], + 'choice_attr' => function ($choice, $key, $value) { + return '&b' === $choice ? ['class' => 'foo&bar'] : []; + }, + 'multiple' => true, + 'expanded' => true, + 'required' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), [], +'/div + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=4] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CountryType', 'AT'); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index a124b48ffda31..c5b6ef7cfd851 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests\ChoiceList\Factory; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; @@ -23,6 +24,8 @@ class DefaultChoiceListFactoryTest extends TestCase { + use ExpectDeprecationTrait; + private $obj1; private $obj2; @@ -652,8 +655,13 @@ function ($object, $key, $value) { $this->assertGroupedView($view); } - public function testCreateViewFlatAttrAsArray() + /** + * @group legacy + */ + public function testLegacyCreateViewFlatAttrAsArray() { + $this->expectDeprecation('Since symfony/form 5.1: Using an array of arrays mapped by choice indexes to define the "choice_attr" option is deprecated. Use a callable or a unique array for all choices instead.'); + $view = $this->factory->createView( $this->list, [$this->obj2, $this->obj3], @@ -666,6 +674,20 @@ public function testCreateViewFlatAttrAsArray() ] ); + $this->assertFlatViewWithDynamicAttr($view); + } + + public function testCreateViewFlatAttrAsArray() + { + $view = $this->factory->createView( + $this->list, + [$this->obj2, $this->obj3], // preferred choices + null, // label + null, // index + null, // group + ['attribute_name' => 'attribute_value'] + ); + $this->assertFlatViewWithAttr($view); } @@ -694,7 +716,7 @@ public function testCreateViewFlatAttrAsCallable() [$this, 'getAttr'] ); - $this->assertFlatViewWithAttr($view); + $this->assertFlatViewWithDynamicAttr($view); } public function testCreateViewFlatAttrAsClosure() @@ -710,7 +732,7 @@ function ($object) { } ); - $this->assertFlatViewWithAttr($view); + $this->assertFlatViewWithDynamicAttr($view); } public function testCreateViewFlatAttrClosureReceivesKey() @@ -730,7 +752,7 @@ function ($object, $key) { } ); - $this->assertFlatViewWithAttr($view); + $this->assertFlatViewWithDynamicAttr($view); } public function testCreateViewFlatAttrClosureReceivesValue() @@ -750,7 +772,7 @@ function ($object, $key, $value) { } ); - $this->assertFlatViewWithAttr($view); + $this->assertFlatViewWithDynamicAttr($view); } private function assertScalarListWithChoiceValues(ChoiceListInterface $list) @@ -859,7 +881,7 @@ private function assertFlatViewWithCustomIndices($view) ), $view); } - private function assertFlatViewWithAttr($view) + private function assertFlatViewWithDynamicAttr($view) { $this->assertEquals(new ChoiceListView( [ @@ -894,6 +916,49 @@ private function assertFlatViewWithAttr($view) ), $view); } + private function assertFlatViewWithAttr($view) + { + $this->assertEquals(new ChoiceListView([ + 0 => new ChoiceView( + $this->obj1, + '0', + 'A', + ['attribute_name' => 'attribute_value'] + ), + 1 => new ChoiceView( + $this->obj2, + '1', + 'B', + ['attribute_name' => 'attribute_value'] + ), + 2 => new ChoiceView( + $this->obj3, + '2', + 'C', + ['attribute_name' => 'attribute_value'] + ), + 3 => new ChoiceView( + $this->obj4, + '3', + 'D', + ['attribute_name' => 'attribute_value'] + ), + ], [ + 1 => new ChoiceView( + $this->obj2, + '1', + 'B', + ['attribute_name' => 'attribute_value'] + ), + 2 => new ChoiceView( + $this->obj3, + '2', + 'C', + ['attribute_name' => 'attribute_value'] + ), + ]), $view); + } + private function assertGroupedView($view) { $this->assertEquals(new ChoiceListView(