Skip to content

Commit 93dac3e

Browse files
committed
[Form] Added a "choice_filter" option to ChoiceType
1 parent 269c4a2 commit 93dac3e

18 files changed

+552
-35
lines changed

UPGRADE-5.1.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Form
2323
is deprecated. The method will be added to the interface in 6.0.
2424
* Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method
2525
is deprecated. The method will be added to the interface in 6.0.
26+
* Not defining a third `callable|null $filter` argument to `ChoiceListFactoryInterface::createListFromChoices` and `ChoiceListFactoryInterface::createListFromLoader` methods is deprecated.
27+
The argument will de defined in 6.0.
2628

2729
FrameworkBundle
2830
---------------

UPGRADE-6.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Form
2121

2222
* Added the `getIsEmptyCallback()` method to the `FormConfigInterface`.
2323
* Added the `setIsEmptyCallback()` method to the `FormConfigBuilderInterface`.
24+
* Added a third `callable|null $filter` argument to `ChoiceListFactoryInterface::createListFromChoices` and `ChoiceListFactoryInterface::createListFromLoader` methods.
2425

2526
FrameworkBundle
2627
---------------

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.1.0
55
-----
66

7+
* Added a `choice_filter` option to `ChoiceType`
78
* Added a `ChoiceList` facade to leverage explicit choice list caching based on options
89
* Added an `AbstractChoiceLoader` to simplify implementations and handle global optimizations
910
* The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured.

src/Symfony/Component/Form/ChoiceList/ChoiceList.php

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

1414
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
1515
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
16+
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
1617
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
1718
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
1819
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
@@ -66,6 +67,16 @@ public static function value($formType, $value, $vary = null): ChoiceValue
6667
return new ChoiceValue($formType, $value, $vary);
6768
}
6869

70+
/**
71+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
72+
* @param callable $filter Any pseudo callable to filter a choice list
73+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
74+
*/
75+
public static function filter(FormTypeInterface $formType, $filter, $vary = null): ChoiceFilter
76+
{
77+
return new ChoiceFilter($formType, $filter, $vary);
78+
}
79+
6980
/**
7081
* Decorates a "choice_label" option to make it cacheable.
7182
*
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Cache;
13+
14+
use Symfony\Component\Form\FormTypeInterface;
15+
16+
/**
17+
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
18+
* which configures a "choice_filter" option.
19+
*
20+
* @internal
21+
*
22+
* @author Jules Pietri <jules@heahprod.com>
23+
*/
24+
final class ChoiceFilter extends AbstractStaticOption
25+
{
26+
}

src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,24 +81,34 @@ public function getDecoratedFactory()
8181
/**
8282
* {@inheritdoc}
8383
*/
84-
public function createListFromChoices(iterable $choices, $value = null)
84+
public function createListFromChoices(iterable $choices, $value = null, $filter = null)
8585
{
8686
if ($choices instanceof \Traversable) {
8787
$choices = iterator_to_array($choices);
8888
}
8989

90-
// Only cache per value when needed. The value is not validated on purpose.
90+
$cache = true;
91+
// Only cache per value and filter when needed. The value is not validated on purpose.
9192
// The decorated factory may decide which values to accept and which not.
9293
if ($value instanceof Cache\ChoiceValue) {
9394
$value = $value->getOption();
9495
} elseif ($value) {
95-
return $this->decoratedFactory->createListFromChoices($choices, $value);
96+
$cache = false;
97+
}
98+
if ($filter instanceof Cache\ChoiceFilter) {
99+
$filter = $filter->getOption();
100+
} elseif ($filter) {
101+
$cache = false;
102+
}
103+
104+
if (!$cache) {
105+
return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
96106
}
97107

98-
$hash = self::generateHash([$choices, $value], 'fromChoices');
108+
$hash = self::generateHash([$choices, $value, $filter], 'fromChoices');
99109

100110
if (!isset($this->lists[$hash])) {
101-
$this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value);
111+
$this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
102112
}
103113

104114
return $this->lists[$hash];
@@ -107,7 +117,7 @@ public function createListFromChoices(iterable $choices, $value = null)
107117
/**
108118
* {@inheritdoc}
109119
*/
110-
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
120+
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null, $filter = null)
111121
{
112122
$cache = true;
113123

@@ -123,14 +133,20 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
123133
$cache = false;
124134
}
125135

136+
if ($filter instanceof Cache\ChoiceFilter) {
137+
$filter = $filter->getOption();
138+
} elseif ($filter) {
139+
$cache = false;
140+
}
141+
126142
if (!$cache) {
127-
return $this->decoratedFactory->createListFromLoader($loader, $value);
143+
return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
128144
}
129145

130-
$hash = self::generateHash([$loader, $value], 'fromLoader');
146+
$hash = self::generateHash([$loader, $value, $filter], 'fromLoader');
131147

132148
if (!isset($this->lists[$hash])) {
133-
$this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value);
149+
$this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
134150
}
135151

136152
return $this->lists[$hash];

src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ interface ChoiceListFactoryInterface
3131
* The callable receives the choice as only argument.
3232
* Null may be passed when the choice list contains the empty value.
3333
*
34+
* @param callable|null $filter The callable filtering the choices
35+
* (will be added in Symfony 6.0)
36+
*
3437
* @return ChoiceListInterface The choice list
3538
*/
36-
public function createListFromChoices(iterable $choices, callable $value = null);
39+
public function createListFromChoices(iterable $choices, callable $value = null/*, $filter = null*/);
3740

3841
/**
3942
* Creates a choice list that is loaded with the given loader.
@@ -42,9 +45,12 @@ public function createListFromChoices(iterable $choices, callable $value = null)
4245
* The callable receives the choice as only argument.
4346
* Null may be passed when the choice list contains the empty value.
4447
*
48+
* @param callable|null $filter The callable filtering the choices
49+
* (will be added in Symfony 6.0)
50+
*
4551
* @return ChoiceListInterface The choice list
4652
*/
47-
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null);
53+
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null/*, $filter = null*/);
4854

4955
/**
5056
* Creates a view for the given choice list.

src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
1616
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
1717
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
18+
use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
1819
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
1920
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
2021
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
@@ -29,16 +30,44 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface
2930
/**
3031
* {@inheritdoc}
3132
*/
32-
public function createListFromChoices(iterable $choices, callable $value = null)
33+
public function createListFromChoices(iterable $choices, callable $value = null, callable $filter = null)
3334
{
35+
if ($filter) {
36+
if ($choices instanceof \Traversable) {
37+
$choices = iterator_to_array($choices);
38+
}
39+
40+
foreach ($choices as $group => $choiceOrGroup) {
41+
if (!\is_array($choiceOrGroup)) {
42+
$choices = array_filter($choices, $filter);
43+
44+
break;
45+
}
46+
47+
// choices are structured, filter by group
48+
if ($filtered = array_filter($choiceOrGroup, $filter)) {
49+
$choices[$group] = $filtered;
50+
51+
continue;
52+
}
53+
54+
// also filter empty groups
55+
unset($choices[$group]);
56+
}
57+
}
58+
3459
return new ArrayChoiceList($choices, $value);
3560
}
3661

3762
/**
3863
* {@inheritdoc}
3964
*/
40-
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null)
65+
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null, callable $filter = null)
4166
{
67+
if ($filter) {
68+
$loader = new FilterChoiceLoaderDecorator($loader, $filter);
69+
}
70+
4271
return new LazyChoiceList($loader, $value);
4372
}
4473

src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ public function getDecoratedFactory()
5959
/**
6060
* {@inheritdoc}
6161
*
62-
* @param callable|string|PropertyPath|null $value The callable or path for
63-
* generating the choice values
62+
* @param callable|string|PropertyPath|null $value The callable or path for
63+
* generating the choice values
64+
* @param callable|string|PropertyPath|null $filter The callable or path for
65+
* filtering the choices
6466
*
6567
* @return ChoiceListInterface The choice list
6668
*/
67-
public function createListFromChoices(iterable $choices, $value = null)
69+
public function createListFromChoices(iterable $choices, $value = null, $filter = null)
6870
{
6971
if (\is_string($value)) {
7072
$value = new PropertyPath($value);
@@ -81,18 +83,33 @@ public function createListFromChoices(iterable $choices, $value = null)
8183
};
8284
}
8385

84-
return $this->decoratedFactory->createListFromChoices($choices, $value);
86+
if (\is_string($filter)) {
87+
$filter = new PropertyPath($filter);
88+
}
89+
90+
if ($filter instanceof PropertyPath) {
91+
$accessor = $this->propertyAccessor;
92+
$filter = static function ($choice) use ($accessor, $filter) {
93+
if (\is_object($choice) || \is_array($choice)) {
94+
return (bool) $accessor->getValue($choice, $filter);
95+
}
96+
};
97+
}
98+
99+
return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
85100
}
86101

87102
/**
88103
* {@inheritdoc}
89104
*
90-
* @param callable|string|PropertyPath|null $value The callable or path for
91-
* generating the choice values
105+
* @param callable|string|PropertyPath|null $value The callable or path for
106+
* generating the choice values
107+
* @param callable|string|PropertyPath|null $filter The callable or path for
108+
* filtering the choices
92109
*
93110
* @return ChoiceListInterface The choice list
94111
*/
95-
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
112+
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null, $filter = null)
96113
{
97114
if (\is_string($value)) {
98115
$value = new PropertyPath($value);
@@ -109,7 +126,20 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
109126
};
110127
}
111128

112-
return $this->decoratedFactory->createListFromLoader($loader, $value);
129+
if (\is_string($filter)) {
130+
$filter = new PropertyPath($filter);
131+
}
132+
133+
if ($filter instanceof PropertyPath) {
134+
$accessor = $this->propertyAccessor;
135+
$filter = static function ($choice) use ($accessor, $filter) {
136+
if (\is_object($choice) || \is_array($choice)) {
137+
return (bool) $accessor->getValue($choice, $filter);
138+
}
139+
};
140+
}
141+
142+
return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
113143
}
114144

115145
/**
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\Loader;
13+
14+
/**
15+
* A decorator to filter choices only when they are loaded or partially loaded.
16+
*
17+
* @author Jules Pietri <jules@heahprod.com>
18+
*/
19+
class FilterChoiceLoaderDecorator extends AbstractChoiceLoader
20+
{
21+
private $decoratedLoader;
22+
private $filter;
23+
24+
public function __construct(ChoiceLoaderInterface $loader, callable $filter)
25+
{
26+
$this->decoratedLoader = $loader;
27+
$this->filter = $filter;
28+
}
29+
30+
protected function loadChoices(): iterable
31+
{
32+
$list = $this->decoratedLoader->loadChoiceList();
33+
34+
if (array_values($list->getValues()) === array_values($structuredValues = $list->getStructuredValues())) {
35+
return array_filter(array_combine($list->getOriginalKeys(), $list->getChoices()), $this->filter);
36+
}
37+
38+
foreach ($structuredValues as $group => $values) {
39+
if ($filtered = array_filter($list->getChoicesForValues($values), $this->filter)) {
40+
$choices[$group] = $filtered;
41+
}
42+
// filter empty groups
43+
}
44+
45+
return $choices ?? [];
46+
}
47+
48+
/**
49+
* {@inheritdoc}
50+
*/
51+
public function loadChoicesForValues(array $values, callable $value = null): array
52+
{
53+
return array_filter($this->decoratedLoader->loadChoicesForValues($values, $value), $this->filter);
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function loadValuesForChoices(array $choices, callable $value = null): array
60+
{
61+
return $this->decoratedLoader->loadValuesForChoices(array_filter($choices, $this->filter), $value);
62+
}
63+
}

0 commit comments

Comments
 (0)