diff --git a/.travis.yml b/.travis.yml index b279eac0215a8..d20a9be048d2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ matrix: env: deps=high - php: 7.3 env: deps=low + - php: 7.4snapshot fast_finish: true diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 9db346c21787f..f965429df9631 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.4.0 +----- + +* Added the ability to extract property type from the property declaration + 4.3.0 ----- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 83f14acb70760..88adb8ecf1862 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -109,6 +109,10 @@ public function getProperties($class, array $context = []) */ public function getTypes($class, $property, array $context = []) { + if ($fromDeclaredType = $this->extractFromDeclaredType($class, $property)) { + return $fromDeclaredType; + } + if ($fromMutator = $this->extractFromMutator($class, $property)) { return $fromMutator; } @@ -185,6 +189,28 @@ public function isInitializable(string $class, string $property, array $context return false; } + private function extractFromDeclaredType(string $class, string $property) + { + if (version_compare(\PHP_VERSION_ID, 70400, '<')) { + return null; + } + + try { + $reflectionClass = new \ReflectionClass($class); + $reflectionProperty = $reflectionClass->getProperty($property); + } catch (\ReflectionException $e) { + return null; + } + + if (!$reflectionProperty->hasType()) { + return null; + } + + $reflectionType = $reflectionProperty->getType(); + + return [$this->extractFromReflectionType($reflectionType, $reflectionProperty)]; + } + /** * @return Type[]|null */ @@ -287,7 +313,13 @@ private function extractFromDefaultValue(string $class, string $property) return [new Type(static::MAP_TYPES[$type] ?? $type)]; } - private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionMethod $reflectionMethod): Type + /** + * @param \ReflectionType $reflectionType + * @param \ReflectionMethod|\ReflectionProperty $reflector + * + * @return Type + */ + private function extractFromReflectionType(\ReflectionType $reflectionType, $reflector): Type { $phpTypeOrClass = $reflectionType->getName(); $nullable = $reflectionType->allowsNull(); @@ -298,19 +330,37 @@ private function extractFromReflectionType(\ReflectionType $reflectionType, \Ref $type = new Type(Type::BUILTIN_TYPE_NULL, $nullable); } elseif ($reflectionType->isBuiltin()) { $type = new Type($phpTypeOrClass, $nullable); + } elseif ($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty) { + $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflector)); } else { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflectionMethod)); + throw new \InvalidArgumentException( + '$reflector should be an instance of ReflectionMethod or ReflectionProperty, ' + .(\is_object($reflector) ? \get_class($reflector) : \gettype($reflector)).' given' + ); } return $type; } - private function resolveTypeName(string $name, \ReflectionMethod $reflectionMethod): string + /** + * @param string $name + * @param \ReflectionMethod|\ReflectionProperty $reflector + * + * @return string + */ + private function resolveTypeName(string $name, $reflector): string { + if (!($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty)) { + throw new \InvalidArgumentException( + '$reflector should be an instance of ReflectionMethod or ReflectionProperty, ' + .(\is_object($reflector) ? \get_class($reflector) : \gettype($reflector)).' given' + ); + } + if ('self' === $lcName = strtolower($name)) { - return $reflectionMethod->getDeclaringClass()->name; + return $reflector->getDeclaringClass()->name; } - if ('parent' === $lcName && $parent = $reflectionMethod->getDeclaringClass()->getParentClass()) { + if ('parent' === $lcName && $parent = $reflector->getDeclaringClass()->getParentClass()) { return $parent->name; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 7472d6ef21b00..d534831125aec 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -15,9 +15,12 @@ use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php74Dummy; use Symfony\Component\PropertyInfo\Type; /** @@ -228,6 +231,31 @@ public function defaultValueProvider() ]; } + /** + * @dataProvider declaredTypeProvider + */ + public function testExtractFromDeclaredType($property, $type) + { + if (version_compare(PHP_VERSION, '7.4.0-dev', '<')) { + $this->markTestSkipped('Extracting type from declared type is enabled only on PHP 7.4+'); + } + + $this->assertEquals($type, $this->extractor->getTypes(Php74Dummy::class, $property, [])); + } + + public function declaredTypeProvider() + { + return [ + ['int', [new Type(Type::BUILTIN_TYPE_INT, false)]], + ['string', [new Type(Type::BUILTIN_TYPE_STRING, true)]], + ['dummy', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]], + ['optionalDummy', [new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]], + ['iterable', [new Type(Type::BUILTIN_TYPE_ITERABLE, false)]], + ['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Php74Dummy::class)]], + ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], + ]; + } + /** * @dataProvider getReadableProperties */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php74Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php74Dummy.php new file mode 100644 index 0000000000000..3aa187e4d39f1 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php74Dummy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +/** + * Class Php74Dummy + * + * @author Tales Santos + */ +class Php74Dummy extends ParentDummy +{ + public int $int; + public ?string $string; + public Dummy $dummy; + public parent $parent; + public self $self; + public ?Dummy $optionalDummy; + public iterable $iterable; +}