Skip to content

Commit 16c3e02

Browse files
committed
[Form] add choice_filter option to ChoiceType
1 parent 2946932 commit 16c3e02

File tree

7 files changed

+373
-13
lines changed

7 files changed

+373
-13
lines changed

src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
2323
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
2424
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
25+
use Symfony\Component\Form\ChoiceList\Factory\FilteringFactoryDecorator;
2526
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
2627
use Symfony\Component\Form\Exception\RuntimeException;
2728
use Symfony\Component\Form\FormBuilderInterface;
@@ -111,10 +112,12 @@ public function getQueryBuilderPartsForCachingHash($queryBuilder)
111112
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
112113
{
113114
$this->registry = $registry;
114-
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
115-
new PropertyAccessDecorator(
116-
new DefaultChoiceListFactory(),
117-
$propertyAccessor
115+
$this->choiceListFactory = $choiceListFactory ?: new FilteringFactoryDecorator(
116+
new CachingFactoryDecorator(
117+
new PropertyAccessDecorator(
118+
new DefaultChoiceListFactory(),
119+
$propertyAccessor
120+
)
118121
)
119122
);
120123
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@
5959
<argument type="service" id="form.choice_list_factory.property_access"/>
6060
</service>
6161

62-
<service id="form.choice_list_factory" alias="form.choice_list_factory.cached" public="false"/>
62+
<service id="form.choice_list_factory.filtered" class="Symfony\Component\Form\ChoiceList\Factory\FilteringFactoryDecorator" public="false">
63+
<argument type="service" id="form.choice_list_factory.cached"/>
64+
</service>
65+
66+
<service id="form.choice_list_factory" alias="form.choice_list_factory.filtered" public="false"/>
6367

6468
<service id="form.type.form" class="Symfony\Component\Form\Extension\Core\Type\FormType">
6569
<argument type="service" id="form.property_accessor" />

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"symfony/dom-crawler": "~2.8|~3.0",
4444
"symfony/polyfill-intl-icu": "~1.0",
4545
"symfony/security": "~2.8|~3.0",
46-
"symfony/form": "~2.8|~3.0",
46+
"symfony/form": "~2.8|~3.1",
4747
"symfony/expression-language": "~2.8|~3.0",
4848
"symfony/process": "~2.8|~3.0",
4949
"symfony/serializer": "~2.8|^3.0",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\ChoiceList\Factory;
13+
14+
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
15+
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
16+
17+
/**
18+
* Creates filtered {@link ChoiceListInterface} instances.
19+
*
20+
* @author Jules Pietri <jules@heahprod.com>
21+
*/
22+
interface FilteredChoiceListFactoryInterface extends ChoiceListFactoryInterface
23+
{
24+
/**
25+
* Creates a filtered choice list for the given choices.
26+
*
27+
* The choices should be passed in the values of the choices array.
28+
*
29+
* The filter callable gets passed each choice and its resolved value
30+
* and should return true to keep the choice and false or null otherwise.
31+
*
32+
* Optionally, a callable can be passed for generating the choice values.
33+
* The callable receives the choice as only argument.
34+
*
35+
* @param array|\Traversable $choices The choices
36+
* @param null|callable $value The callable generating the choice
37+
* values
38+
* @param callable $filter The filter
39+
*
40+
* @return ChoiceListInterface The filtered choice list
41+
*/
42+
public function createFilteredListFromChoices($choices, $value = null, callable $filter);
43+
44+
/**
45+
* Creates a filtered choice list that is loaded with the given loader.
46+
*
47+
* The filter callable gets passed each choice and its resolved value
48+
* and should return true to keep the choice and false or null otherwise.
49+
*
50+
* Optionally, a callable can be passed for generating the choice values.
51+
* The callable receives the choice as only argument.
52+
*
53+
* @param ChoiceLoaderInterface $loader The choice loader
54+
* @param null|callable $value The callable generating the choice
55+
* values
56+
* @param callable $filter The filter
57+
*
58+
* @return ChoiceListInterface The filtered choice list
59+
*/
60+
public function createFilteredListFromLoader(ChoiceLoaderInterface $loader, $value = null, callable $filter);
61+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\ChoiceList\Factory;
13+
14+
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
15+
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
16+
17+
/**
18+
* Filter the choices before passing them to the decorated factory.
19+
*
20+
* @author Jules Pietri <jules@heahprod.com>
21+
*/
22+
class FilteringFactoryDecorator implements FilteredChoiceListFactoryInterface
23+
{
24+
/**
25+
* @var ChoiceListFactoryInterface
26+
*/
27+
private $decoratedFactory;
28+
29+
/**
30+
* @var array[]
31+
*/
32+
private $choicesByValues = array();
33+
34+
/**
35+
* Decorates the given factory.
36+
*
37+
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
38+
*/
39+
public function __construct(ChoiceListFactoryInterface $decoratedFactory)
40+
{
41+
$this->decoratedFactory = $decoratedFactory;
42+
}
43+
44+
/**
45+
* Returns the decorated factory.
46+
*
47+
* @return ChoiceListFactoryInterface The decorated factory
48+
*/
49+
public function getDecoratedFactory()
50+
{
51+
return $this->decoratedFactory;
52+
}
53+
54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function createListFromChoices($choices, $value = null)
58+
{
59+
return $this->decoratedFactory->createListFromChoices($choices, $value);
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
66+
{
67+
return $this->decoratedFactory->createListFromLoader($loader, $value);
68+
}
69+
70+
/**
71+
* {@inheritdoc}
72+
*/
73+
public function createFilteredListFromChoices($choices, $value = null, callable $filter)
74+
{
75+
// We need to apply the filter on a resolved choices array in case
76+
// the same choices are filtered many times. The original choice list
77+
// should be cached by the decorated factory
78+
$choiceList = $this->decoratedFactory->createListFromChoices($choices, $value);
79+
80+
// The filtered choice list should be cached by the decorated factory
81+
// if the same filter is applied on the same choices by values
82+
83+
return $this->decoratedFactory->createListFromChoices(self::filterChoices($choiceList->getChoices(), $filter));
84+
}
85+
86+
/**
87+
* {@inheritdoc}
88+
*/
89+
public function createFilteredListFromLoader(ChoiceLoaderInterface $loader, $value = null, callable $filter)
90+
{
91+
// Don't hash the filter since the original choices may have been loaded already
92+
// with a different filter if any.
93+
$hash = CachingFactoryDecorator::generateHash(array($loader, $value));
94+
95+
if (!isset($this->choicesByValues[$hash])) {
96+
// We need to load the choice list before filtering the choices
97+
$choiceList = $this->decoratedFactory->createListFromLoader($loader, $value);
98+
99+
// Cache the choices by values, in case they are filtered many times,
100+
// the original choice list should already have been cached by the
101+
// previous call.
102+
$this->choicesByValues[$hash] = $choiceList->getChoices();
103+
}
104+
105+
// The filtered choice list should be cached by the decorated factory
106+
// if the same filter is applied on the same choices by values
107+
108+
return $this->decoratedFactory->createListFromChoices(self::filterChoices($this->choicesByValues[$hash], $filter));
109+
}
110+
111+
/**
112+
* {@inheritdoc}
113+
*/
114+
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
115+
{
116+
$this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
117+
}
118+
119+
/**
120+
* Filters the choices.
121+
*
122+
* @param array $choices The choices by values to filter
123+
* @param callable $filter The filter
124+
*
125+
* @return array The filtered choices
126+
*/
127+
static private function filterChoices($choices, callable $filter)
128+
{
129+
foreach ($choices as $value => $choice) {
130+
if (call_user_func($filter, $choice, $value)) {
131+
continue;
132+
}
133+
unset($choices[$value]);
134+
}
135+
136+
return $choices;
137+
}
138+
}

src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Symfony\Component\Form\AbstractType;
1515
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
16+
use Symfony\Component\Form\ChoiceList\Factory\FilteredChoiceListFactoryInterface;
17+
use Symfony\Component\Form\ChoiceList\Factory\FilteringFactoryDecorator;
1618
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
1719
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
1820
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
@@ -45,9 +47,11 @@ class ChoiceType extends AbstractType
4547

4648
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
4749
{
48-
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
49-
new PropertyAccessDecorator(
50-
new DefaultChoiceListFactory()
50+
$this->choiceListFactory = $choiceListFactory ?: new FilteringFactoryDecorator(
51+
new CachingFactoryDecorator(
52+
new PropertyAccessDecorator(
53+
new DefaultChoiceListFactory()
54+
)
5155
)
5256
);
5357
}
@@ -315,6 +319,7 @@ public function configureOptions(OptionsResolver $resolver)
315319
'expanded' => false,
316320
'choices' => array(),
317321
'choices_as_values' => null, // deprecated since 3.1
322+
'choice_filter' => null,
318323
'choice_loader' => null,
319324
'choice_label' => null,
320325
'choice_name' => null,
@@ -339,6 +344,7 @@ public function configureOptions(OptionsResolver $resolver)
339344

340345
$resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable'));
341346
$resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string'));
347+
$resolver->setAllowedTypes('choice_filter', array('null', 'callable'));
342348
$resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'));
343349
$resolver->setAllowedTypes('choice_label', array('null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
344350
$resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
@@ -414,15 +420,28 @@ private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $ch
414420
private function createChoiceList(array $options)
415421
{
416422
if (null !== $options['choice_loader']) {
417-
return $this->choiceListFactory->createListFromLoader(
418-
$options['choice_loader'],
419-
$options['choice_value']
420-
);
423+
if (is_callable($options['choice_filter']) && $this->choiceListFactory instanceof FilteredChoiceListFactoryInterface) {
424+
return $this->choiceListFactory->createFilteredListFromLoader(
425+
$options['choice_loader'],
426+
$options['choice_value'],
427+
$options['choice_filter']
428+
);
429+
}
430+
431+
return $this->choiceListFactory->createListFromLoader($options['choice_loader'], $options['choice_value']);
421432
}
422433

423434
// Harden against NULL values (like in EntityType and ModelType)
424435
$choices = null !== $options['choices'] ? $options['choices'] : array();
425436

437+
if (!empty($choices) && is_callable($options['choice_filter']) && $this->choiceListFactory instanceof FilteredChoiceListFactoryInterface) {
438+
return $this->choiceListFactory->createFilteredListFromChoices(
439+
$choices,
440+
$options['choice_value'],
441+
$options['choice_filter']
442+
);
443+
}
444+
426445
return $this->choiceListFactory->createListFromChoices($choices, $options['choice_value']);
427446
}
428447

0 commit comments

Comments
 (0)