Skip to content

Commit 30ce63c

Browse files
committed
[Form] Add form type guesser for EnumType
1 parent 39c5025 commit 30ce63c

File tree

6 files changed

+360
-0
lines changed

6 files changed

+360
-0
lines changed

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

Lines changed: 5 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,10 @@
7677
->args([service('validator.mapping.class_metadata_factory')])
7778
->tag('form.type_guesser')
7879

80+
->set('form.type_guesser.enum_type', EnumFormTypeGuesser::class)
81+
->args([service('type_info.resolver')])
82+
->tag('form.type_guesser')
83+
7984
->alias('form.property_accessor', 'property_accessor')
8085

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

src/Symfony/Component/Form/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"symfony/security-core": "^6.4|^7.0|^8.0",
4242
"symfony/security-csrf": "^6.4|^7.0|^8.0",
4343
"symfony/translation": "^6.4.3|^7.0.3|^8.0",
44+
"symfony/type-info": "^7.4|^8.0",
4445
"symfony/var-dumper": "^6.4|^7.0|^8.0",
4546
"symfony/uid": "^6.4|^7.0|^8.0"
4647
},

0 commit comments

Comments
 (0)