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

Skip to content

[Form][DX] Add choice_filter option to ChoiceType #28607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,26 @@
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;

/**
* Loads choices using a Doctrine object manager.
*
* @author Bernhard Schussek <[email protected]>
*/
class DoctrineChoiceLoader implements ChoiceLoaderInterface
class DoctrineChoiceLoader implements ChoiceLoaderInterface, ChoiceFilterInterface
{
private $manager;
private $class;
private $idReader;
private $objectLoader;

/**
* @var callable
*/
private $choiceFilter;

/**
* @var ChoiceListInterface
*/
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -146,4 +156,12 @@ public function loadChoicesForValues(array $values, $value = null)

return $this->loadChoiceList($value)->getChoicesForValues($values);
}

/**
* {@inheritdoc}
*/
public function setChoiceFilter(callable $choiceFilter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wild thoughts, as I don't know all the implications right now: I don't know if a new interface with a setter is a good idea. Shouldn't we simply inject in the loaders' constructors the callable $choiceFilter instead?

Copy link
Contributor

@ro0NL ro0NL Sep 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 makes sense, to keep compatibility with any choice_loader we could still decorate it if the choice filter wasnt injected initially.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expected this question :) My goal is enable this feature for all (more and more used) CallbackChoiceLoader in current projects, automatically.

Otherwise, the feature will be enabled only for core types (e.g. Intl form types, EntityType) and will require more code and then more maintenance (injecting the $options['choice_filter'] everywhere).

This was the simplest solution I found to achieve a better DX, so callback loaders don't need to do something to enable this option.

Copy link
Member Author

@yceruto yceruto Sep 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this would allow for custom choice loader to enable this feature without care about injecting $options['choice_filter'] everywhere the loader is used. We only care about the implementation as a plug-and-play interface.

Btw, there are many examples of interface with a setter into the core, I'm not sure it's a problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #28624 for a different approach

{
$this->choiceFilter = $choiceFilter;
}
}
18 changes: 18 additions & 0 deletions src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
}
5 changes: 3 additions & 2 deletions src/Symfony/Bridge/Doctrine/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": "",
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
*
* @author Jules Pietri <[email protected]>
*/
class CallbackChoiceLoader implements ChoiceLoaderInterface
class CallbackChoiceLoader implements ChoiceLoaderInterface, ChoiceFilterInterface
{
private $callback;

/**
* @var callable
*/
private $choiceFilter;

/**
* The loaded choice list.
*
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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 <[email protected]>
*/
interface ChoiceFilterInterface
{
/**
* @param callable $choiceFilter The callable returning a filtered array of choices
*/
public function setChoiceFilter(callable $choiceFilter);
}
27 changes: 27 additions & 0 deletions src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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'));
Expand Down Expand Up @@ -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']
Expand All @@ -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']);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
4 changes: 3 additions & 1 deletion src/Symfony/Component/Form/Tests/Fixtures/Author.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"options": {
"own": [
"choice_attr",
"choice_filter",
"choice_label",
"choice_loader",
"choice_name",
Expand Down
Loading