Skip to content

Commit 1e540eb

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

File tree

6 files changed

+327
-0
lines changed

6 files changed

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