[Form] Add form type guesser for EnumType

This commit is contained in:
Matthias Schmidt
2025-08-01 17:52:03 +02:00
parent 1fd9c16f05
commit 97572da8ee
4 changed files with 323 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ CHANGELOG
---
* Add `input=date_point` to `DateTimeType`, `DateType` and `TimeType`
* Add support for guessing form type of enum properties
7.3
---

78
EnumFormTypeGuesser.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
final class EnumFormTypeGuesser implements FormTypeGuesserInterface
{
/**
* @var array<string, array<string, string|false>>
*/
private array $cache = [];
public function guessType(string $class, string $property): ?TypeGuess
{
if (!($enum = $this->getPropertyType($class, $property))) {
return null;
}
return new TypeGuess(EnumType::class, ['class' => ltrim($enum, '?')], Guess::HIGH_CONFIDENCE);
}
public function guessRequired(string $class, string $property): ?ValueGuess
{
if (!($enum = $this->getPropertyType($class, $property))) {
return null;
}
return new ValueGuess('?' !== $enum[0], Guess::HIGH_CONFIDENCE);
}
public function guessMaxLength(string $class, string $property): ?ValueGuess
{
return null;
}
public function guessPattern(string $class, string $property): ?ValueGuess
{
return null;
}
private function getPropertyType(string $class, string $property): string|false
{
if (isset($this->cache[$class][$property])) {
return $this->cache[$class][$property];
}
try {
$propertyReflection = new \ReflectionProperty($class, $property);
} catch (\ReflectionException) {
return $this->cache[$class][$property] = false;
}
$type = $propertyReflection->getType();
if (!$type instanceof \ReflectionNamedType || !enum_exists($type->getName())) {
$enum = false;
} else {
$enum = $type->getName();
if ($type->allowsNull()) {
$enum = '?'.$enum;
}
}
return $this->cache[$class][$property] = $enum;
}
}

View File

@@ -0,0 +1,208 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\EnumFormTypeGuesser;
use Symfony\Component\Form\Extension\Core\Type\EnumType as FormEnumType;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
use Symfony\Component\Form\Tests\Fixtures\BackedEnumFormTypeGuesserCaseEnum;
use Symfony\Component\Form\Tests\Fixtures\EnumFormTypeGuesserCase;
use Symfony\Component\Form\Tests\Fixtures\EnumFormTypeGuesserCaseEnum;
class EnumFormTypeGuesserTest extends TestCase
{
#[DataProvider('provideGuessTypeCases')]
public function testGuessType(?TypeGuess $expectedTypeGuess, string $class, string $property)
{
$typeGuesser = new EnumFormTypeGuesser();
$typeGuess = $typeGuesser->guessType($class, $property);
self::assertEquals($expectedTypeGuess, $typeGuess);
}
#[DataProvider('provideGuessRequiredCases')]
public function testGuessRequired(?ValueGuess $expectedValueGuess, string $class, string $property)
{
$typeGuesser = new EnumFormTypeGuesser();
$valueGuess = $typeGuesser->guessRequired($class, $property);
self::assertEquals($expectedValueGuess, $valueGuess);
}
public static function provideGuessTypeCases(): iterable
{
yield 'Undefined class' => [
null,
'UndefinedClass',
'undefinedProperty',
];
yield 'Undefined property' => [
null,
EnumFormTypeGuesserCase::class,
'undefinedProperty',
];
yield 'Undefined enum' => [
null,
EnumFormTypeGuesserCase::class,
'undefinedEnum',
];
yield 'Non-enum property' => [
null,
EnumFormTypeGuesserCase::class,
'string',
];
yield 'Enum property' => [
new TypeGuess(
FormEnumType::class,
[
'class' => EnumFormTypeGuesserCaseEnum::class,
],
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'enum',
];
yield 'Nullable enum property' => [
new TypeGuess(
FormEnumType::class,
[
'class' => EnumFormTypeGuesserCaseEnum::class,
],
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'nullableEnum',
];
yield 'Backed enum property' => [
new TypeGuess(
FormEnumType::class,
[
'class' => BackedEnumFormTypeGuesserCaseEnum::class,
],
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'backedEnum',
];
yield 'Nullable backed enum property' => [
new TypeGuess(
FormEnumType::class,
[
'class' => BackedEnumFormTypeGuesserCaseEnum::class,
],
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'nullableBackedEnum',
];
yield 'Enum union property' => [
null,
EnumFormTypeGuesserCase::class,
'enumUnion',
];
yield 'Enum intersection property' => [
null,
EnumFormTypeGuesserCase::class,
'enumIntersection',
];
}
public static function provideGuessRequiredCases(): iterable
{
yield 'Unknown class' => [
null,
'UndefinedClass',
'undefinedProperty',
];
yield 'Unknown property' => [
null,
EnumFormTypeGuesserCase::class,
'undefinedProperty',
];
yield 'Undefined enum' => [
null,
EnumFormTypeGuesserCase::class,
'undefinedEnum',
];
yield 'Non-enum property' => [
null,
EnumFormTypeGuesserCase::class,
'string',
];
yield 'Enum property' => [
new ValueGuess(
true,
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'enum',
];
yield 'Nullable enum property' => [
new ValueGuess(
false,
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'nullableEnum',
];
yield 'Backed enum property' => [
new ValueGuess(
true,
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'backedEnum',
];
yield 'Nullable backed enum property' => [
new ValueGuess(
false,
Guess::HIGH_CONFIDENCE,
),
EnumFormTypeGuesserCase::class,
'nullableBackedEnum',
];
yield 'Enum union property' => [
null,
EnumFormTypeGuesserCase::class,
'enumUnion',
];
yield 'Enum intersection property' => [
null,
EnumFormTypeGuesserCase::class,
'enumIntersection',
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Fixtures;
class EnumFormTypeGuesserCase
{
public string $string;
public UndefinedEnum $undefinedEnum;
public EnumFormTypeGuesserCaseEnum $enum;
public ?EnumFormTypeGuesserCaseEnum $nullableEnum;
public BackedEnumFormTypeGuesserCaseEnum $backedEnum;
public ?BackedEnumFormTypeGuesserCaseEnum $nullableBackedEnum;
public EnumFormTypeGuesserCaseEnum|BackedEnumFormTypeGuesserCaseEnum $enumUnion;
public EnumFormTypeGuesserCaseEnum&BackedEnumFormTypeGuesserCaseEnum $enumIntersection;
}
enum EnumFormTypeGuesserCaseEnum
{
case Foo;
case Bar;
}
enum BackedEnumFormTypeGuesserCaseEnum: string
{
case Foo = 'foo';
case Bar = 'bar';
}