Skip to content

Commit 3f14eac

Browse files
committed
Add a TypeGuesser that use typed property reflection
1 parent b02a689 commit 3f14eac

File tree

4 files changed

+203
-0
lines changed

4 files changed

+203
-0
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Change the signature of `FormConfigBuilderInterface::setDataMapper()` to `setDataMapper(?DataMapperInterface)`
1111
* Change the signature of `FormInterface::setParent()` to `setParent(?self)`
1212
* Add `PasswordHasherExtension` with support for `hash_property_path` option in `PasswordType`
13+
* Add a type guesser that uses reflection information
1314

1415
6.1
1516
---

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

0 commit comments

Comments
 (0)