diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 79bf63d40712d..665eeb9416d0a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -83,6 +83,7 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Glob; +use Symfony\Component\Form\EnumFormTypeGuesser; use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormTypeExtensionInterface; @@ -880,6 +881,10 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont { $loader->load('form.php'); + if (!class_exists(EnumFormTypeGuesser::class)) { + $container->removeDefinition('form.type_guesser.enum_type'); + } + if (null === $config['form']['csrf_protection']['enabled']) { $this->writeConfigEnabled('form.csrf_protection', $config['csrf_protection']['enabled'], $config['form']['csrf_protection']); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php index 3c936a284b325..fe85f6c51d096 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php @@ -14,6 +14,7 @@ use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\EnumFormTypeGuesser; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ColorType; use Symfony\Component\Form\Extension\Core\Type\FileType; @@ -76,6 +77,9 @@ ->args([service('validator.mapping.class_metadata_factory')]) ->tag('form.type_guesser') + ->set('form.type_guesser.enum_type', EnumFormTypeGuesser::class) + ->tag('form.type_guesser') + ->alias('form.property_accessor', 'property_accessor') ->set('form.choice_list_factory.default', DefaultChoiceListFactory::class) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index b74d43e79d23f..f6ef659e58037 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -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 --- diff --git a/src/Symfony/Component/Form/EnumFormTypeGuesser.php b/src/Symfony/Component/Form/EnumFormTypeGuesser.php new file mode 100644 index 0000000000000..7a4054a36b36b --- /dev/null +++ b/src/Symfony/Component/Form/EnumFormTypeGuesser.php @@ -0,0 +1,73 @@ + + * + * 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> + */ + private array $cache = []; + + public function guessType(string $class, string $property): ?TypeGuess + { + if (!($propertyType = $this->getPropertyType($class, $property))) { + return null; + } + + return new TypeGuess(EnumType::class, ['class' => $propertyType->getName()], Guess::HIGH_CONFIDENCE); + } + + public function guessRequired(string $class, string $property): ?ValueGuess + { + if (!($propertyType = $this->getPropertyType($class, $property))) { + return null; + } + + return new ValueGuess(!$propertyType->allowsNull(), 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): \ReflectionNamedType|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())) { + $type = false; + } + + return $this->cache[$class][$property] = $type; + } +} diff --git a/src/Symfony/Component/Form/Tests/EnumFormTypeGuesserTest.php b/src/Symfony/Component/Form/Tests/EnumFormTypeGuesserTest.php new file mode 100644 index 0000000000000..f0a04c7f5f833 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/EnumFormTypeGuesserTest.php @@ -0,0 +1,208 @@ + + * + * 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', + ]; + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/EnumFormTypeGuesserCase.php b/src/Symfony/Component/Form/Tests/Fixtures/EnumFormTypeGuesserCase.php new file mode 100644 index 0000000000000..3028ec8c84194 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/EnumFormTypeGuesserCase.php @@ -0,0 +1,36 @@ + + * + * 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'; +}