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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy