Skip to content

Commit 448b249

Browse files
committed
Add a TypeGuesser that use typed property reflection
1 parent e788a49 commit 448b249

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Allow passing `TranslatableInterface` objects to the `ChoiceView` label
88
* Allow passing `TranslatableInterface` objects to the `help` option
9+
* Add a type guesser that uses reflection information.
910

1011
6.1
1112
---

src/Symfony/Component/Form/Extension/Core/CoreExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
1818
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
1919
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
20+
use Symfony\Component\Form\FormTypeGuesserInterface;
2021
use Symfony\Component\PropertyAccess\PropertyAccess;
2122
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2223
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -86,4 +87,9 @@ protected function loadTypeExtensions(): array
8687
new TransformationFailureExtension($this->translator),
8788
];
8889
}
90+
91+
protected function loadTypeGuesser(): ?FormTypeGuesserInterface
92+
{
93+
return new ReflectionTypeGuesser();
94+
}
8995
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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\Extension\Core;
13+
14+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
15+
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;
16+
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
17+
use Symfony\Component\Form\Extension\Core\Type\EnumType;
18+
use Symfony\Component\Form\Extension\Core\Type\FileType;
19+
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
20+
use Symfony\Component\Form\Extension\Core\Type\NumberType;
21+
use Symfony\Component\Form\Extension\Core\Type\TextType;
22+
use Symfony\Component\Form\Extension\Core\Type\UlidType;
23+
use Symfony\Component\Form\Extension\Core\Type\UuidType;
24+
use Symfony\Component\Form\FormTypeGuesserInterface;
25+
use Symfony\Component\Form\Guess\Guess;
26+
use Symfony\Component\Form\Guess\TypeGuess;
27+
use Symfony\Component\Form\Guess\ValueGuess;
28+
29+
class ReflectionTypeGuesser implements FormTypeGuesserInterface
30+
{
31+
public function guessType(string $class, string $property): ?TypeGuess
32+
{
33+
$type = $this->getReflectionType($class, $property);
34+
35+
if (!($type instanceof \ReflectionNamedType)) {
36+
return null;
37+
}
38+
39+
$name = $type->getName();
40+
41+
if (enum_exists($name)) {
42+
return new TypeGuess(EnumType::class, ['class' => $name], Guess::MEDIUM_CONFIDENCE);
43+
}
44+
45+
return match ($name) {
46+
// PHP types
47+
'bool' => new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE),
48+
'float' => new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE),
49+
'int' => new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE),
50+
'string' => new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE),
51+
52+
// PHP classes
53+
'DateTime' => new TypeGuess(DateTimeType::class, [], Guess::LOW_CONFIDENCE),
54+
'DateTimeImmutable' => new TypeGuess(DateTimeType::class, ['input' => 'datetime_immutable'], Guess::LOW_CONFIDENCE),
55+
'DateInterval' => new TypeGuess(DateIntervalType::class, [], Guess::MEDIUM_CONFIDENCE),
56+
'DateTimeZone' => new TypeGuess(TimezoneType::class, ['input' => 'datetimezone'], Guess::MEDIUM_CONFIDENCE),
57+
'IntlTimeZone' => new TypeGuess(TimezoneType::class, ['input' => 'intltimezone'], Guess::MEDIUM_CONFIDENCE),
58+
59+
// Symfony classes
60+
'Symfony\Component\HttpFoundation\File\File' => new TypeGuess(FileType::class, [], Guess::MEDIUM_CONFIDENCE),
61+
'Symfony\Component\Uid\Ulid' => new TypeGuess(UlidType::class, [], Guess::MEDIUM_CONFIDENCE),
62+
'Symfony\Component\Uid\Uuid' => new TypeGuess(UuidType::class, [], Guess::MEDIUM_CONFIDENCE),
63+
64+
default => null,
65+
};
66+
}
67+
68+
public function guessRequired(string $class, string $property): ?ValueGuess
69+
{
70+
$type = $this->getReflectionType($class, $property);
71+
72+
if (!$type) {
73+
return null;
74+
}
75+
76+
return new ValueGuess(!$type->allowsNull(), Guess::MEDIUM_CONFIDENCE);
77+
}
78+
79+
public function guessMaxLength(string $class, string $property): ?ValueGuess
80+
{
81+
return null;
82+
}
83+
84+
public function guessPattern(string $class, string $property): ?ValueGuess
85+
{
86+
return null;
87+
}
88+
89+
private function getReflectionType(string $class, string $property): ?\ReflectionType
90+
{
91+
try {
92+
$reflection = new \ReflectionProperty($class, $property);
93+
} catch (\ReflectionException $e) {
94+
return null;
95+
}
96+
97+
return $reflection->getType();
98+
}
99+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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\Extension\Core;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Form\Extension\Core\ReflectionTypeGuesser;
16+
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
17+
use Symfony\Component\Form\Extension\Core\Type\EnumType;
18+
use Symfony\Component\Form\Extension\Core\Type\TextType;
19+
use Symfony\Component\Form\Extension\Core\Type\UuidType;
20+
use Symfony\Component\Form\Guess\Guess;
21+
use Symfony\Component\Form\Guess\TypeGuess;
22+
use Symfony\Component\Form\Guess\ValueGuess;
23+
use Symfony\Component\Form\Tests\Fixtures\Suit;
24+
use Symfony\Component\Uid\Uuid;
25+
26+
class ReflectionTypeGuesserTest extends TestCase
27+
{
28+
/**
29+
* @dataProvider guessTypeProvider
30+
*/
31+
public function testGuessType(string $property, TypeGuess $expected)
32+
{
33+
$guesser = new ReflectionTypeGuesser();
34+
35+
$this->assertEquals($expected, $guesser->guessType(ReflectionTypeGuesserTest_TestClass::class, $property));
36+
}
37+
38+
public function guessTypeProvider()
39+
{
40+
return [
41+
['uuid', new TypeGuess(UuidType::class, [], Guess::MEDIUM_CONFIDENCE)],
42+
['string', new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)],
43+
['nullable', new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)],
44+
['suit', new TypeGuess(EnumType::class, ['class' => Suit::class], Guess::MEDIUM_CONFIDENCE)],
45+
['date', new TypeGuess(DateTimeType::class, ['input' => 'datetime_immutable'], Guess::LOW_CONFIDENCE)],
46+
];
47+
}
48+
49+
/**
50+
* @dataProvider guessRequiredProvider
51+
*/
52+
public function testGuessRequired(string $property, ValueGuess $expected)
53+
{
54+
$guesser = new ReflectionTypeGuesser();
55+
56+
$this->assertEquals($expected, $guesser->guessRequired(ReflectionTypeGuesserTest_TestClass::class, $property));
57+
}
58+
59+
public function guessRequiredProvider()
60+
{
61+
return [
62+
['string', new ValueGuess(true, Guess::MEDIUM_CONFIDENCE)],
63+
['nullable', new ValueGuess(false, Guess::MEDIUM_CONFIDENCE)],
64+
['suit', new ValueGuess(true, Guess::MEDIUM_CONFIDENCE)],
65+
];
66+
}
67+
}
68+
69+
class ReflectionTypeGuesserTest_TestClass
70+
{
71+
private Uuid $uuid;
72+
73+
private string $string;
74+
75+
private ?string $nullable;
76+
77+
private Suit $suit;
78+
79+
private \DateTimeImmutable $date;
80+
}

0 commit comments

Comments
 (0)