diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 7df0c37a4ecd1..29c18c197e567 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -14,6 +14,7 @@ use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceFilterInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; /** @@ -21,13 +22,18 @@ * * @author Bernhard Schussek */ -class DoctrineChoiceLoader implements ChoiceLoaderInterface +class DoctrineChoiceLoader implements ChoiceLoaderInterface, ChoiceFilterInterface { private $manager; private $class; private $idReader; private $objectLoader; + /** + * @var callable + */ + private $choiceFilter; + /** * @var ChoiceListInterface */ @@ -68,6 +74,10 @@ public function loadChoiceList($value = null) ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); + if (null !== $this->choiceFilter) { + $objects = array_filter($objects, $this->choiceFilter, ARRAY_FILTER_USE_BOTH); + } + return $this->choiceList = new ArrayChoiceList($objects, $value); } @@ -146,4 +156,12 @@ public function loadChoicesForValues(array $values, $value = null) return $this->loadChoiceList($value)->getChoicesForValues($values); } + + /** + * {@inheritdoc} + */ + public function setChoiceFilter(callable $choiceFilter) + { + $this->choiceFilter = $choiceFilter; + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index c0061dabc540e..6de54a08877c3 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -1482,4 +1482,22 @@ public function testSetDataNonEmptyArraySubmitNullMultiple() $this->assertEquals(array(), $form->getNormData()); $this->assertSame(array(), $form->getViewData(), 'View data is always an array'); } + + public function testChoiceFilterOption() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', static::TESTED_TYPE, null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_filter' => function (SingleIntIdEntity $entity) { + return 'Bar' === $entity->name; + }, + )); + + $this->assertEquals(array(2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + } } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index cc344b2039ed7..57abe52543faf 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -27,7 +27,7 @@ "require-dev": { "symfony/stopwatch": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", - "symfony/form": "~3.4|~4.0", + "symfony/form": "^4.2", "symfony/http-kernel": "~3.4|~4.0", "symfony/property-access": "~3.4|~4.0", "symfony/property-info": "~3.4|~4.0", @@ -45,7 +45,8 @@ }, "conflict": { "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", - "symfony/dependency-injection": "<3.4" + "symfony/dependency-injection": "<3.4", + "symfony/form": "<4.2" }, "suggest": { "symfony/form": "", diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index e18359b90b3eb..ee479e13f4eb6 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -8,6 +8,8 @@ CHANGELOG * added `Symfony\Component\Form\ClearableErrorsInterface` * deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered * deprecated the `scale` option of the `IntegerType` + * added `Symfony\Component\Form\ChoiceList\Loader\ChoiceFilterInterface` + * added `choice_filter` option to `ChoiceType` 4.1.0 ----- diff --git a/src/Symfony/Component/Form/ChoiceList/Loader/CallbackChoiceLoader.php b/src/Symfony/Component/Form/ChoiceList/Loader/CallbackChoiceLoader.php index b2825051afda5..b1a0db59cc1cb 100644 --- a/src/Symfony/Component/Form/ChoiceList/Loader/CallbackChoiceLoader.php +++ b/src/Symfony/Component/Form/ChoiceList/Loader/CallbackChoiceLoader.php @@ -18,10 +18,15 @@ * * @author Jules Pietri */ -class CallbackChoiceLoader implements ChoiceLoaderInterface +class CallbackChoiceLoader implements ChoiceLoaderInterface, ChoiceFilterInterface { private $callback; + /** + * @var callable + */ + private $choiceFilter; + /** * The loaded choice list. * @@ -46,7 +51,13 @@ public function loadChoiceList($value = null) return $this->choiceList; } - return $this->choiceList = new ArrayChoiceList(\call_user_func($this->callback), $value); + $choices = \call_user_func($this->callback); + + if (null !== $this->choiceFilter) { + $choices = array_filter($choices, $this->choiceFilter, ARRAY_FILTER_USE_BOTH); + } + + return $this->choiceList = new ArrayChoiceList($choices, $value); } /** @@ -74,4 +85,12 @@ public function loadValuesForChoices(array $choices, $value = null) return $this->loadChoiceList($value)->getValuesForChoices($choices); } + + /** + * {@inheritdoc} + */ + public function setChoiceFilter(callable $choiceFilter) + { + $this->choiceFilter = $choiceFilter; + } } diff --git a/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceFilterInterface.php b/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceFilterInterface.php new file mode 100644 index 0000000000000..6336f704fdcbf --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceFilterInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +/** + * @author Yonel Ceruto + */ +interface ChoiceFilterInterface +{ + /** + * @param callable $choiceFilter The callable returning a filtered array of choices + */ + public function setChoiceFilter(callable $choiceFilter); +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index c9d45ba0d65a1..71103a3b98a13 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -17,9 +17,11 @@ use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceFilterInterface; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper; use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper; @@ -265,6 +267,16 @@ public function configureOptions(OptionsResolver $resolver) return $options['required'] ? null : ''; }; + $choiceFilterNormalizer = function (Options $options, $choiceFilter) { + if (null !== $choiceFilter && !\is_callable($choiceFilter)) { + return function ($choice) use ($choiceFilter) { + return \in_array($choice, $choiceFilter, true); + }; + } + + return $choiceFilter; + }; + $placeholderNormalizer = function (Options $options, $placeholder) { if ($options['multiple']) { // never use an empty value for this case @@ -301,6 +313,7 @@ public function configureOptions(OptionsResolver $resolver) 'expanded' => false, 'choices' => array(), 'choice_loader' => null, + 'choice_filter' => null, 'choice_label' => null, 'choice_name' => null, 'choice_value' => null, @@ -319,12 +332,14 @@ public function configureOptions(OptionsResolver $resolver) 'trim' => false, )); + $resolver->setNormalizer('choice_filter', $choiceFilterNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); $resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable')); $resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string')); $resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')); + $resolver->setAllowedTypes('choice_filter', array('null', 'array', 'callable')); $resolver->setAllowedTypes('choice_label', array('null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); $resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); $resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); @@ -390,6 +405,14 @@ private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceV private function createChoiceList(array $options) { if (null !== $options['choice_loader']) { + if (null !== $options['choice_filter']) { + if (!$options['choice_loader'] instanceof ChoiceFilterInterface) { + throw new RuntimeException(sprintf('The choice loader "%s" must implement "%s" to use the "choice_filter" option.', \get_class($options['choice_loader']), ChoiceFilterInterface::class)); + } + + $options['choice_loader']->setChoiceFilter($options['choice_filter']); + } + return $this->choiceListFactory->createListFromLoader( $options['choice_loader'], $options['choice_value'] @@ -399,6 +422,10 @@ private function createChoiceList(array $options) // Harden against NULL values (like in EntityType and ModelType) $choices = null !== $options['choices'] ? $options['choices'] : array(); + if (null !== $options['choice_filter'] && 0 !== \count($choices)) { + $choices = array_filter($choices, $options['choice_filter'], ARRAY_FILTER_USE_BOTH); + } + return $this->choiceListFactory->createListFromChoices($choices, $options['choice_value']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index e12ab6ff93116..651eb3295a850 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; @@ -2052,4 +2054,94 @@ public function provideTrimCases() 'Multiple expanded' => array(true, true), ); } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoiceFilterOptionExpectsCallable() + { + $this->factory->create(static::TESTED_TYPE, null, array( + 'choice_filter' => new \stdClass(), + )); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\RuntimeException + */ + public function testChoiceFilterOptionExpectsChoiceFilterInterface() + { + $this->factory->create(static::TESTED_TYPE, null, array( + 'choice_loader' => new class() implements ChoiceLoaderInterface { + public function loadChoiceList($value = null) + { + } + + public function loadChoicesForValues(array $values, $value = null) + { + } + + public function loadValuesForChoices(array $choices, $value = null) + { + } + }, + 'choice_filter' => function ($choice) {}, + )); + } + + public function testClosureChoiceFilterOptionWithChoiceLoaderOption() + { + $form = $this->factory->create(static::TESTED_TYPE, null, array( + // defined by superclass + 'choice_loader' => new CallbackChoiceLoader(function () { + return $this->choices; + }), + // defined by subclass or userland + 'choice_filter' => function ($choice) { + return \in_array($choice, array('b', 'c'), true); + }, + )); + + $options = array(); + foreach ($form->createView()->vars['choices'] as $choiceView) { + $options[$choiceView->label] = $choiceView->value; + } + + $this->assertSame(array('Fabien' => 'b', 'Kris' => 'c'), $options); + } + + public function testStaticChoiceFilterOptionWithChoiceLoaderOption() + { + $form = $this->factory->create(static::TESTED_TYPE, null, array( + // defined by superclass + 'choice_loader' => new CallbackChoiceLoader(function () { + return $this->choices; + }), + // defined by subclass or userland + 'choice_filter' => array('b', 'c'), + )); + + $options = array(); + foreach ($form->createView()->vars['choices'] as $choiceView) { + $options[$choiceView->label] = $choiceView->value; + } + + $this->assertSame(array('Fabien' => 'b', 'Kris' => 'c'), $options); + } + + public function testChoiceFilterOptionWithChoicesOption() + { + $form = $this->factory->create(static::TESTED_TYPE, null, array( + // defined by superclass + 'choices' => $this->choices, + // defined by subclass or userland + 'choice_filter' => array('b', 'c'), + )); + + $options = array(); + foreach ($form->createView()->vars['choices'] as $choiceView) { + $options[$choiceView->label] = $choiceView->value; + } + + $this->assertSame(array('Fabien' => 'b', 'Kris' => 'c'), $options); + } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Author.php b/src/Symfony/Component/Form/Tests/Fixtures/Author.php index 39765d9df3232..cda8b1937cc66 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Author.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/Author.php @@ -42,9 +42,11 @@ private function getPrivateGetter() return 'foobar'; } - public function setAustralian($australian) + public function setAustralian($australian): self { $this->australian = $australian; + + return $this; } public function isAustralian() diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json index 0f43316313f63..7638ea0f20922 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -4,6 +4,7 @@ "options": { "own": [ "choice_attr", + "choice_filter", "choice_label", "choice_loader", "choice_name", diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt index 34120481edb44..4fcb82318949a 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt @@ -6,18 +6,18 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") Options Overridden options Parent options Extension options --------------------------- -------------------- ------------------------- ----------------------- choice_attr FormType FormType FormTypeCsrfExtension - choice_label -------------------- ------------------------- ----------------------- - choice_loader compound action csrf_field_name - choice_name data_class attr csrf_message - choice_translation_domain empty_data auto_initialize csrf_protection - choice_value error_bubbling block_name csrf_token_id - choices trim by_reference csrf_token_manager - expanded data - group_by disabled - multiple help - placeholder inherit_data - preferred_choices label - label_attr + choice_filter -------------------- ------------------------- ----------------------- + choice_label compound action csrf_field_name + choice_loader data_class attr csrf_message + choice_name empty_data auto_initialize csrf_protection + choice_translation_domain error_bubbling block_name csrf_token_id + choice_value trim by_reference csrf_token_manager + choices data + expanded disabled + group_by help + multiple inherit_data + placeholder label + preferred_choices label_attr label_format mapped method