diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 68622612e30b3..b67d5fc7d53ec 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Change the signature of `FormConfigBuilderInterface::setDataMapper()` to `setDataMapper(?DataMapperInterface)` * Change the signature of `FormInterface::setParent()` to `setParent(?self)` * Add `PasswordHasherExtension` with support for `hash_property_path` option in `PasswordType` + * Add a type guesser that uses reflection information 6.1 --- diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index 951bf345c0c42..0e5b3a35c08de 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -17,6 +17,7 @@ use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension; +use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -86,4 +87,9 @@ protected function loadTypeExtensions(): array new TransformationFailureExtension($this->translator), ]; } + + protected function loadTypeGuesser(): ?FormTypeGuesserInterface + { + return new ReflectionTypeGuesser(); + } } diff --git a/src/Symfony/Component/Form/Extension/Core/ReflectionTypeGuesser.php b/src/Symfony/Component/Form/Extension/Core/ReflectionTypeGuesser.php new file mode 100644 index 0000000000000..3e724b7cf624c --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ReflectionTypeGuesser.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core; + +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\DateIntervalType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TimezoneType; +use Symfony\Component\Form\Extension\Core\Type\UlidType; +use Symfony\Component\Form\Extension\Core\Type\UuidType; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Guess\ValueGuess; + +class ReflectionTypeGuesser implements FormTypeGuesserInterface +{ + public function guessType(string $class, string $property): ?TypeGuess + { + $type = $this->getReflectionType($class, $property); + + if (!($type instanceof \ReflectionNamedType)) { + return null; + } + + $name = $type->getName(); + + if (enum_exists($name)) { + return new TypeGuess(EnumType::class, ['class' => $name], Guess::MEDIUM_CONFIDENCE); + } + + return match ($name) { + // PHP types + 'bool' => new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE), + 'float' => new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE), + 'int' => new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE), + 'string' => new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE), + + // PHP classes + 'DateTime' => new TypeGuess(DateTimeType::class, [], Guess::LOW_CONFIDENCE), + 'DateTimeImmutable' => new TypeGuess(DateTimeType::class, ['input' => 'datetime_immutable'], Guess::LOW_CONFIDENCE), + 'DateInterval' => new TypeGuess(DateIntervalType::class, [], Guess::MEDIUM_CONFIDENCE), + 'DateTimeZone' => new TypeGuess(TimezoneType::class, ['input' => 'datetimezone'], Guess::MEDIUM_CONFIDENCE), + 'IntlTimeZone' => new TypeGuess(TimezoneType::class, ['input' => 'intltimezone'], Guess::MEDIUM_CONFIDENCE), + + // Symfony classes + 'Symfony\Component\HttpFoundation\File\File' => new TypeGuess(FileType::class, [], Guess::MEDIUM_CONFIDENCE), + 'Symfony\Component\Uid\Ulid' => new TypeGuess(UlidType::class, [], Guess::MEDIUM_CONFIDENCE), + 'Symfony\Component\Uid\Uuid' => new TypeGuess(UuidType::class, [], Guess::MEDIUM_CONFIDENCE), + + default => null, + }; + } + + public function guessRequired(string $class, string $property): ?ValueGuess + { + $type = $this->getReflectionType($class, $property); + + if (!$type) { + return null; + } + + if ($type instanceof \ReflectionNamedType && 'bool' === $type->getName()) { + return new ValueGuess(false, Guess::MEDIUM_CONFIDENCE); + } + + return new ValueGuess(!$type->allowsNull(), Guess::MEDIUM_CONFIDENCE); + } + + public function guessMaxLength(string $class, string $property): ?ValueGuess + { + return null; + } + + public function guessPattern(string $class, string $property): ?ValueGuess + { + return null; + } + + private function getReflectionType(string $class, string $property): ?\ReflectionType + { + try { + $reflection = new \ReflectionProperty($class, $property); + } catch (\ReflectionException $e) { + return null; + } + + return $reflection->getType(); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ReflectionTypeGuesserTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ReflectionTypeGuesserTest.php new file mode 100644 index 0000000000000..66785fb2c1f5d --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ReflectionTypeGuesserTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\Extension\Core\ReflectionTypeGuesser; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\UuidType; +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Guess\ValueGuess; +use Symfony\Component\Form\Tests\Fixtures\Foo; +use Symfony\Component\Form\Tests\Fixtures\Suit; +use Symfony\Component\Uid\Uuid; + +class ReflectionTypeGuesserTest extends TestCase +{ + /** + * @dataProvider guessTypeProvider + */ + public function testGuessType(string $property, $expected) + { + $guesser = new ReflectionTypeGuesser(); + + $this->assertEquals($expected, $guesser->guessType(ReflectionTypeGuesserTest_TestClass::class, $property)); + } + + public function guessTypeProvider(): array + { + return [ + ['uuid', new TypeGuess(UuidType::class, [], Guess::MEDIUM_CONFIDENCE)], + ['string', new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)], + ['nullable', new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)], + ['suit', new TypeGuess(EnumType::class, ['class' => Suit::class], Guess::MEDIUM_CONFIDENCE)], + ['date', new TypeGuess(DateTimeType::class, ['input' => 'datetime_immutable'], Guess::LOW_CONFIDENCE)], + ['foo', null], + ['untyped', null], + ]; + } + + /** + * @dataProvider guessRequiredProvider + */ + public function testGuessRequired(string $property, $expected) + { + $guesser = new ReflectionTypeGuesser(); + + $this->assertEquals($expected, $guesser->guessRequired(ReflectionTypeGuesserTest_TestClass::class, $property)); + } + + public function guessRequiredProvider(): array + { + return [ + ['string', new ValueGuess(true, Guess::MEDIUM_CONFIDENCE)], + ['nullable', new ValueGuess(false, Guess::MEDIUM_CONFIDENCE)], + ['suit', new ValueGuess(true, Guess::MEDIUM_CONFIDENCE)], + ['foo', new ValueGuess(true, Guess::MEDIUM_CONFIDENCE)], + ['bool', new ValueGuess(false, Guess::MEDIUM_CONFIDENCE)], + ['untyped', null], + ]; + } +} + +class ReflectionTypeGuesserTest_TestClass +{ + private Uuid $uuid; + + private string $string; + + private ?string $nullable; + + private Suit $suit; + + private \DateTimeImmutable $date; + + private Foo $foo; + + private bool $bool; + + private $untyped; +}