Skip to content

Commit 1fbb523

Browse files
committed
[Form] Add form type guesser for EnumType
1 parent f49dbb5 commit 1fbb523

File tree

6 files changed

+331
-0
lines changed

6 files changed

+331
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
use Symfony\Component\Filesystem\Filesystem;
8484
use Symfony\Component\Finder\Finder;
8585
use Symfony\Component\Finder\Glob;
86+
use Symfony\Component\Form\EnumFormTypeGuesser;
8687
use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension;
8788
use Symfony\Component\Form\Form;
8889
use Symfony\Component\Form\FormTypeExtensionInterface;
@@ -564,6 +565,10 @@ public function load(array $configs, ContainerBuilder $container): void
564565
if (!$this->readConfigEnabled('html_sanitizer', $container, $config['html_sanitizer']) || !class_exists(TextTypeHtmlSanitizerExtension::class)) {
565566
$container->removeDefinition('form.type_extension.form.html_sanitizer');
566567
}
568+
569+
if (!class_exists(EnumFormTypeGuesser::class)) {
570+
$container->removeDefinition('form.type_guesser.enum_type');
571+
}
567572
} else {
568573
$container->removeDefinition('console.command.form_debug');
569574
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
1515
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
1616
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
17+
use Symfony\Component\Form\EnumFormTypeGuesser;
1718
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
1819
use Symfony\Component\Form\Extension\Core\Type\ColorType;
1920
use Symfony\Component\Form\Extension\Core\Type\FileType;
@@ -76,6 +77,9 @@
7677
->args([service('validator.mapping.class_metadata_factory')])
7778
->tag('form.type_guesser')
7879

80+
->set('form.type_guesser.enum_type', EnumFormTypeGuesser::class)
81+
->tag('form.type_guesser')
82+
7983
->alias('form.property_accessor', 'property_accessor')
8084

8185
->set('form.choice_list_factory.default', DefaultChoiceListFactory::class)

src/Symfony/Component/Form/CHANGELOG.md

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

77
* Add `input=date_point` to `DateTimeType`, `DateType` and `TimeType`
8+
* Add support for guessing form type of enum properties
89

910
7.3
1011
---
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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;
13+
14+
use Symfony\Component\Form\Extension\Core\Type\EnumType as FormEnumType;
15+
use Symfony\Component\Form\Guess\Guess;
16+
use Symfony\Component\Form\Guess\TypeGuess;
17+
use Symfony\Component\Form\Guess\ValueGuess;
18+
19+
final class EnumFormTypeGuesser implements FormTypeGuesserInterface
20+
{
21+
private array $cache = [];
22+
23+
public function guessType(string $class, string $property): ?TypeGuess
24+
{
25+
if (null === ($propertyType = $this->getPropertyType($class, $property))) {
26+
return null;
27+
}
28+
29+
return new TypeGuess(FormEnumType::class, ['class' => $propertyType->getName()], Guess::HIGH_CONFIDENCE);
30+
}
31+
32+
public function guessRequired(string $class, string $property): ?ValueGuess
33+
{
34+
if (null === $propertyType = $this->getPropertyType($class, $property)) {
35+
return null;
36+
}
37+
38+
return new ValueGuess(!$propertyType->allowsNull(), Guess::HIGH_CONFIDENCE);
39+
}
40+
41+
public function guessMaxLength(string $class, string $property): ?ValueGuess
42+
{
43+
return null;
44+
}
45+
46+
public function guessPattern(string $class, string $property): ?ValueGuess
47+
{
48+
return null;
49+
}
50+
51+
private function getPropertyType(string $class, string $property): ?\ReflectionNamedType
52+
{
53+
if (isset($this->cache[$class]) && \array_key_exists($property, $this->cache[$class])) {
54+
return $this->cache[$class][$property];
55+
}
56+
57+
if (!isset($this->cache[$class])) {
58+
$this->cache[$class] = [];
59+
}
60+
61+
try {
62+
$classReflection = new \ReflectionClass($class);
63+
$propertyReflection = $classReflection->getProperty($property);
64+
} catch (\ReflectionException) {
65+
return $this->cache[$class][$property] = null;
66+
}
67+
68+
if (!$propertyReflection->getType() instanceof \ReflectionNamedType || !enum_exists($propertyReflection->getType()->getName())) {
69+
return $this->cache[$class][$property] = null;
70+
}
71+
72+
return $this->cache[$class][$property] = $propertyReflection->getType();
73+
}
74+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Form\EnumFormTypeGuesser;
16+
use Symfony\Component\Form\Extension\Core\Type\EnumType as FormEnumType;
17+
use Symfony\Component\Form\Guess\Guess;
18+
use Symfony\Component\Form\Guess\TypeGuess;
19+
use Symfony\Component\Form\Guess\ValueGuess;
20+
use Symfony\Component\Form\Tests\Fixtures\BackedEnumFormTypeGuesserCaseEnum;
21+
use Symfony\Component\Form\Tests\Fixtures\EnumFormTypeGuesserCase;
22+
use Symfony\Component\Form\Tests\Fixtures\EnumFormTypeGuesserCaseEnum;
23+
24+
final class EnumFormTypeGuesserTest extends TestCase
25+
{
26+
/**
27+
* @dataProvider provideGuessTypeCases
28+
*/
29+
public function testGuessType(?TypeGuess $expectedTypeGuess, string $class, string $property)
30+
{
31+
$typeGuesser = new EnumFormTypeGuesser();
32+
33+
$typeGuess = $typeGuesser->guessType($class, $property);
34+
35+
self::assertEquals($expectedTypeGuess, $typeGuess);
36+
}
37+
38+
/**
39+
* @dataProvider provideGuessRequiredCases
40+
*/
41+
public function testGuessRequired(?ValueGuess $expectedValueGuess, string $class, string $property)
42+
{
43+
$typeGuesser = new EnumFormTypeGuesser();
44+
45+
$valueGuess = $typeGuesser->guessRequired($class, $property);
46+
47+
self::assertEquals($expectedValueGuess, $valueGuess);
48+
}
49+
50+
public static function provideGuessTypeCases(): iterable
51+
{
52+
yield 'Undefined class' => [
53+
null,
54+
'UndefinedClass',
55+
'undefinedProperty',
56+
];
57+
58+
yield 'Undefined property' => [
59+
null,
60+
EnumFormTypeGuesserCase::class,
61+
'undefinedProperty',
62+
];
63+
64+
yield 'Undefined enum' => [
65+
null,
66+
EnumFormTypeGuesserCase::class,
67+
'undefinedEnum',
68+
];
69+
70+
yield 'Non-enum property' => [
71+
null,
72+
EnumFormTypeGuesserCase::class,
73+
'string',
74+
];
75+
76+
yield 'Enum property' => [
77+
new TypeGuess(
78+
FormEnumType::class,
79+
[
80+
'class' => EnumFormTypeGuesserCaseEnum::class,
81+
],
82+
Guess::HIGH_CONFIDENCE,
83+
),
84+
EnumFormTypeGuesserCase::class,
85+
'enum',
86+
];
87+
88+
yield 'Nullable enum property' => [
89+
new TypeGuess(
90+
FormEnumType::class,
91+
[
92+
'class' => EnumFormTypeGuesserCaseEnum::class,
93+
],
94+
Guess::HIGH_CONFIDENCE,
95+
),
96+
EnumFormTypeGuesserCase::class,
97+
'nullableEnum',
98+
];
99+
100+
yield 'Backed enum property' => [
101+
new TypeGuess(
102+
FormEnumType::class,
103+
[
104+
'class' => BackedEnumFormTypeGuesserCaseEnum::class,
105+
],
106+
Guess::HIGH_CONFIDENCE,
107+
),
108+
EnumFormTypeGuesserCase::class,
109+
'backedEnum',
110+
];
111+
112+
yield 'Nullable backed enum property' => [
113+
new TypeGuess(
114+
FormEnumType::class,
115+
[
116+
'class' => BackedEnumFormTypeGuesserCaseEnum::class,
117+
],
118+
Guess::HIGH_CONFIDENCE,
119+
),
120+
EnumFormTypeGuesserCase::class,
121+
'nullableBackedEnum',
122+
];
123+
124+
yield 'Enum union property' => [
125+
null,
126+
EnumFormTypeGuesserCase::class,
127+
'enumUnion',
128+
];
129+
130+
yield 'Enum intersection property' => [
131+
null,
132+
EnumFormTypeGuesserCase::class,
133+
'enumIntersection',
134+
];
135+
}
136+
137+
public static function provideGuessRequiredCases(): iterable
138+
{
139+
yield 'Unknown class' => [
140+
null,
141+
'UndefinedClass',
142+
'undefinedProperty',
143+
];
144+
145+
yield 'Unknown property' => [
146+
null,
147+
EnumFormTypeGuesserCase::class,
148+
'undefinedProperty',
149+
];
150+
151+
yield 'Undefined enum' => [
152+
null,
153+
EnumFormTypeGuesserCase::class,
154+
'undefinedEnum',
155+
];
156+
157+
yield 'Non-enum property' => [
158+
null,
159+
EnumFormTypeGuesserCase::class,
160+
'string',
161+
];
162+
163+
yield 'Enum property' => [
164+
new ValueGuess(
165+
true,
166+
Guess::HIGH_CONFIDENCE,
167+
),
168+
EnumFormTypeGuesserCase::class,
169+
'enum',
170+
];
171+
172+
yield 'Nullable enum property' => [
173+
new ValueGuess(
174+
false,
175+
Guess::HIGH_CONFIDENCE,
176+
),
177+
EnumFormTypeGuesserCase::class,
178+
'nullableEnum',
179+
];
180+
181+
yield 'Backed enum property' => [
182+
new ValueGuess(
183+
true,
184+
Guess::HIGH_CONFIDENCE,
185+
),
186+
EnumFormTypeGuesserCase::class,
187+
'backedEnum',
188+
];
189+
190+
yield 'Nullable backed enum property' => [
191+
new ValueGuess(
192+
false,
193+
Guess::HIGH_CONFIDENCE,
194+
),
195+
EnumFormTypeGuesserCase::class,
196+
'nullableBackedEnum',
197+
];
198+
199+
yield 'Enum union property' => [
200+
null,
201+
EnumFormTypeGuesserCase::class,
202+
'enumUnion',
203+
];
204+
205+
yield 'Enum intersection property' => [
206+
null,
207+
EnumFormTypeGuesserCase::class,
208+
'enumIntersection',
209+
];
210+
}
211+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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\Tests\Fixtures;
13+
14+
final class EnumFormTypeGuesserCase
15+
{
16+
public string $string;
17+
public UndefinedEnum $undefinedEnum;
18+
public EnumFormTypeGuesserCaseEnum $enum;
19+
public ?EnumFormTypeGuesserCaseEnum $nullableEnum;
20+
public BackedEnumFormTypeGuesserCaseEnum $backedEnum;
21+
public ?BackedEnumFormTypeGuesserCaseEnum $nullableBackedEnum;
22+
public EnumFormTypeGuesserCaseEnum|BackedEnumFormTypeGuesserCaseEnum $enumUnion;
23+
public EnumFormTypeGuesserCaseEnum&BackedEnumFormTypeGuesserCaseEnum $enumIntersection;
24+
}
25+
26+
enum EnumFormTypeGuesserCaseEnum
27+
{
28+
case Foo;
29+
case Bar;
30+
}
31+
32+
enum BackedEnumFormTypeGuesserCaseEnum: string
33+
{
34+
case Foo = 'foo';
35+
case Bar = 'bar';
36+
}

0 commit comments

Comments
 (0)