diff --git a/Extractor/PhpDocExtractor.php b/Extractor/PhpDocExtractor.php index d04b824..86a033d 100644 --- a/Extractor/PhpDocExtractor.php +++ b/Extractor/PhpDocExtractor.php @@ -60,7 +60,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property public function __construct(DocBlockFactoryInterface $docBlockFactory = null, array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null) { if (!class_exists(DocBlockFactory::class)) { - throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed.', __CLASS__)); + throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed. Try running composer require "phpdocumentor/reflection-docblock".', __CLASS__)); } $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); diff --git a/Extractor/PhpStanExtractor.php b/Extractor/PhpStanExtractor.php index 429f432..47b3e94 100644 --- a/Extractor/PhpStanExtractor.php +++ b/Extractor/PhpStanExtractor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\PropertyInfo\Extractor; +use phpDocumentor\Reflection\Types\ContextFactory; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; @@ -59,6 +60,14 @@ final class PhpStanExtractor implements PropertyTypeExtractorInterface, Construc */ public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null) { + if (!class_exists(ContextFactory::class)) { + throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".', __CLASS__)); + } + + if (!class_exists(PhpDocParser::class)) { + throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpstan/phpdoc-parser" package is not installed. Try running composer require "phpstan/phpdoc-parser".', __CLASS__)); + } + $this->phpStanTypeHelper = new PhpStanTypeHelper(); $this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes; $this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes; diff --git a/Extractor/ReflectionExtractor.php b/Extractor/ReflectionExtractor.php index 2a1237e..b96b0ce 100644 --- a/Extractor/ReflectionExtractor.php +++ b/Extractor/ReflectionExtractor.php @@ -359,8 +359,12 @@ public function getWriteInfo(string $class, string $property, array $context = [ if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { $reflProperty = $reflClass->getProperty($property); + if (\PHP_VERSION_ID < 80100 || !$reflProperty->isReadOnly()) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic()); + } - return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic()); + $errors[] = [sprintf('The property "%s" in class "%s" is a promoted readonly property.', $property, $reflClass->getName())]; + $allowMagicSet = $allowMagicCall = false; } if ($allowMagicSet) { diff --git a/LICENSE b/LICENSE index 63af57a..6e3afce 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2023 Fabien Potencier +Copyright (c) 2015-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Tests/DependencyInjection/PropertyInfoPassTest.php b/Tests/DependencyInjection/PropertyInfoPassTest.php index aea826e..a1db482 100644 --- a/Tests/DependencyInjection/PropertyInfoPassTest.php +++ b/Tests/DependencyInjection/PropertyInfoPassTest.php @@ -42,7 +42,7 @@ public function testServicesAreOrderedAccordingToPriority($index, $tag) $this->assertEquals($expected, $definition->getArgument($index)); } - public function provideTags() + public static function provideTags() { return [ [0, 'property_info.list_extractor'], diff --git a/Tests/Extractor/PhpDocExtractorTest.php b/Tests/Extractor/PhpDocExtractorTest.php index 016334d..6985e2d 100644 --- a/Tests/Extractor/PhpDocExtractorTest.php +++ b/Tests/Extractor/PhpDocExtractorTest.php @@ -51,7 +51,7 @@ public function testParamTagTypeIsOmitted() $this->assertNull($this->extractor->getTypes(OmittedParamTagTypeDocBlock::class, 'omittedType')); } - public function invalidTypesProvider() + public static function invalidTypesProvider() { return [ 'pub' => ['pub', null, null], @@ -81,7 +81,7 @@ public function testExtractTypesWithNoPrefixes($property, array $type = null) $this->assertEquals($type, $noPrefixExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public function typesProvider() + public static function typesProvider() { return [ ['foo', null, 'Short description.', 'Long description.'], @@ -139,7 +139,7 @@ public function testExtractCollection($property, array $type = null, $shortDescr $this->testExtract($property, $type, $shortDescription, $longDescription); } - public function provideCollectionTypes() + public static function provideCollectionTypes() { return [ ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))], null, null], @@ -203,7 +203,7 @@ public function testExtractTypesWithCustomPrefixes($property, array $type = null $this->assertEquals($type, $customExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public function typesWithCustomPrefixesProvider() + public static function typesWithCustomPrefixesProvider() { return [ ['foo', null, 'Short description.', 'Long description.'], @@ -244,7 +244,7 @@ public function typesWithCustomPrefixesProvider() ]; } - public function typesWithNoPrefixesProvider() + public static function typesWithNoPrefixesProvider() { return [ ['foo', null, 'Short description.', 'Long description.'], @@ -290,7 +290,7 @@ public function testReturnNullOnEmptyDocBlock() $this->assertNull($this->extractor->getShortDescription(EmptyDocBlock::class, 'foo')); } - public function dockBlockFallbackTypesProvider() + public static function dockBlockFallbackTypesProvider() { return [ 'pub' => [ @@ -321,7 +321,7 @@ public function testPropertiesDefinedByTraits(string $property, Type $type) $this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property)); } - public function propertiesDefinedByTraitsProvider(): array + public static function propertiesDefinedByTraitsProvider(): array { return [ ['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], @@ -341,7 +341,7 @@ public function testMethodsDefinedByTraits(string $property, Type $type) $this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property)); } - public function methodsDefinedByTraitsProvider(): array + public static function methodsDefinedByTraitsProvider(): array { return [ ['methodInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], @@ -361,7 +361,7 @@ public function testPropertiesStaticType(string $class, string $property, Type $ $this->assertEquals([$type], $this->extractor->getTypes($class, $property)); } - public function propertiesStaticTypeProvider(): array + public static function propertiesStaticTypeProvider(): array { return [ [ParentDummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)], @@ -377,7 +377,7 @@ public function testPropertiesParentType(string $class, string $property, ?array $this->assertEquals($types, $this->extractor->getTypes($class, $property)); } - public function propertiesParentTypeProvider(): array + public static function propertiesParentTypeProvider(): array { return [ [ParentDummy::class, 'parentAnnotationNoParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'parent')]], @@ -398,7 +398,7 @@ public function testExtractConstructorTypes($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } - public function constructorTypesProvider() + public static function constructorTypesProvider() { return [ ['date', [new Type(Type::BUILTIN_TYPE_INT)]], @@ -418,7 +418,7 @@ public function testPseudoTypes($property, array $type) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypesDummy', $property)); } - public function pseudoTypesProvider(): array + public static function pseudoTypesProvider(): array { return [ ['classString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], @@ -441,7 +441,7 @@ public function testExtractPromotedProperty(string $property, ?array $types) $this->assertEquals($types, $this->extractor->getTypes(Php80Dummy::class, $property)); } - public function promotedPropertyProvider(): array + public static function promotedPropertyProvider(): array { return [ ['promoted', null], diff --git a/Tests/Extractor/PhpStanExtractorTest.php b/Tests/Extractor/PhpStanExtractorTest.php index 0078f98..99a5e3d 100644 --- a/Tests/Extractor/PhpStanExtractorTest.php +++ b/Tests/Extractor/PhpStanExtractorTest.php @@ -60,7 +60,7 @@ public function testParamTagTypeIsOmitted() $this->assertNull($this->extractor->getTypes(PhpStanOmittedParamTagTypeDocBlock::class, 'omittedType')); } - public function invalidTypesProvider() + public static function invalidTypesProvider() { return [ 'pub' => ['pub'], @@ -88,7 +88,7 @@ public function testExtractTypesWithNoPrefixes($property, array $type = null) $this->assertEquals($type, $noPrefixExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public function typesProvider() + public static function typesProvider() { return [ ['foo', null], @@ -141,7 +141,7 @@ public function testExtractCollection($property, array $type = null) $this->testExtract($property, $type); } - public function provideCollectionTypes() + public static function provideCollectionTypes() { return [ ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))]], @@ -199,7 +199,7 @@ public function testExtractTypesWithCustomPrefixes($property, array $type = null $this->assertEquals($type, $customExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public function typesWithCustomPrefixesProvider() + public static function typesWithCustomPrefixesProvider() { return [ ['foo', null], @@ -237,7 +237,7 @@ public function typesWithCustomPrefixesProvider() ]; } - public function typesWithNoPrefixesProvider() + public static function typesWithNoPrefixesProvider() { return [ ['foo', null], @@ -275,7 +275,7 @@ public function typesWithNoPrefixesProvider() ]; } - public function dockBlockFallbackTypesProvider() + public static function dockBlockFallbackTypesProvider() { return [ 'pub' => [ @@ -306,7 +306,7 @@ public function testPropertiesDefinedByTraits(string $property, Type $type) $this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property)); } - public function propertiesDefinedByTraitsProvider(): array + public static function propertiesDefinedByTraitsProvider(): array { return [ ['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], @@ -323,7 +323,7 @@ public function testPropertiesStaticType(string $class, string $property, Type $ $this->assertEquals([$type], $this->extractor->getTypes($class, $property)); } - public function propertiesStaticTypeProvider(): array + public static function propertiesStaticTypeProvider(): array { return [ [ParentDummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)], @@ -339,7 +339,7 @@ public function testPropertiesParentType(string $class, string $property, ?array $this->assertEquals($types, $this->extractor->getTypes($class, $property)); } - public function propertiesParentTypeProvider(): array + public static function propertiesParentTypeProvider(): array { return [ [ParentDummy::class, 'parentAnnotationNoParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'parent')]], @@ -355,7 +355,7 @@ public function testExtractConstructorTypes($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } - public function constructorTypesProvider() + public static function constructorTypesProvider() { return [ ['date', [new Type(Type::BUILTIN_TYPE_INT)]], @@ -374,7 +374,7 @@ public function testExtractorUnionTypes(string $property, ?array $types) $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType', $property)); } - public function unionTypesProvider(): array + public static function unionTypesProvider(): array { return [ ['a', [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)]], @@ -395,7 +395,7 @@ public function testPseudoTypes($property, array $type) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\PhpStanPseudoTypesDummy', $property)); } - public function pseudoTypesProvider(): array + public static function pseudoTypesProvider(): array { return [ ['classString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], @@ -445,7 +445,7 @@ public function testExtractorIntRangeType(string $property, ?array $types) $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy', $property)); } - public function intRangeTypeProvider(): array + public static function intRangeTypeProvider(): array { return [ ['a', [new Type(Type::BUILTIN_TYPE_INT)]], @@ -462,7 +462,7 @@ public function testExtractPhp80Type(string $class, $property, array $type = nul $this->assertEquals($type, $this->extractor->getTypes($class, $property, [])); } - public function php80TypesProvider() + public static function php80TypesProvider() { return [ [Php80Dummy::class, 'promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]], diff --git a/Tests/Extractor/ReflectionExtractorTest.php b/Tests/Extractor/ReflectionExtractorTest.php index 7c364e4..2bcb196 100644 --- a/Tests/Extractor/ReflectionExtractorTest.php +++ b/Tests/Extractor/ReflectionExtractorTest.php @@ -204,7 +204,7 @@ public function testExtractors($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, [])); } - public function typesProvider() + public static function typesProvider() { return [ ['a', null], @@ -231,7 +231,7 @@ public function testExtractPhp7Type(string $class, string $property, array $type $this->assertEquals($type, $this->extractor->getTypes($class, $property, [])); } - public function php7TypesProvider() + public static function php7TypesProvider() { return [ [Php7Dummy::class, 'foo', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], @@ -252,7 +252,7 @@ public function testExtractPhp71Type($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy', $property, [])); } - public function php71TypesProvider() + public static function php71TypesProvider() { return [ ['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], @@ -271,7 +271,7 @@ public function testExtractPhp80Type($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy', $property, [])); } - public function php80TypesProvider() + public static function php80TypesProvider() { return [ ['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], @@ -293,7 +293,7 @@ public function testExtractPhp81Type($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy', $property, [])); } - public function php81TypesProvider() + public static function php81TypesProvider() { return [ ['nothing', null], @@ -316,7 +316,7 @@ public function testExtractPhp82Type($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy', $property, [])); } - public function php82TypesProvider(): iterable + public static function php82TypesProvider(): iterable { yield ['nil', null]; yield ['false', [new Type(Type::BUILTIN_TYPE_FALSE)]]; @@ -335,7 +335,7 @@ public function testExtractWithDefaultValue($property, $type) $this->assertEquals($type, $this->extractor->getTypes(DefaultValue::class, $property, [])); } - public function defaultValueProvider() + public static function defaultValueProvider() { return [ ['defaultInt', [new Type(Type::BUILTIN_TYPE_INT, false)]], @@ -357,7 +357,7 @@ public function testIsReadable($property, $expected) ); } - public function getReadableProperties() + public static function getReadableProperties() { return [ ['bar', false], @@ -388,7 +388,7 @@ public function testIsWritable($property, $expected) ); } - public function getWritableProperties() + public static function getWritableProperties() { return [ ['bar', false], @@ -442,7 +442,7 @@ public function testIsInitializable(string $class, string $property, bool $expec $this->assertSame($expected, $this->extractor->isInitializable($class, $property)); } - public function getInitializableProperties(): array + public static function getInitializableProperties(): array { return [ [Php71Dummy::class, 'string', true], @@ -466,7 +466,7 @@ public function testExtractTypeConstructor(string $class, string $property, arra $this->assertNull($this->extractor->getTypes($class, $property, ['enable_constructor_extraction' => false])); } - public function constructorTypesProvider(): array + public static function constructorTypesProvider(): array { return [ // php71 dummy has following constructor: __construct(string $string, int $intPrivate) @@ -522,7 +522,7 @@ public function testGetReadAccessor($class, $property, $found, $type, $name, $vi $this->assertSame($static, $readAcessor->isStatic()); } - public function readAccessorProvider(): array + public static function readAccessorProvider(): array { return [ [Dummy::class, 'bar', true, PropertyReadInfo::TYPE_PROPERTY, 'bar', PropertyReadInfo::VISIBILITY_PRIVATE, false], @@ -580,7 +580,7 @@ public function testGetWriteMutator($class, $property, $allowConstruct, $found, } } - public function writeMutatorProvider(): array + public static function writeMutatorProvider(): array { return [ [Dummy::class, 'bar', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'bar', null, null, PropertyWriteInfo::VISIBILITY_PRIVATE, false], @@ -604,6 +604,15 @@ public function writeMutatorProvider(): array ]; } + public function testGetWriteInfoReadonlyProperties() + { + $writeMutatorConstructor = $this->extractor->getWriteInfo(Php81Dummy::class, 'foo', ['enable_constructor_extraction' => true]); + $writeMutatorWithoutConstructor = $this->extractor->getWriteInfo(Php81Dummy::class, 'foo', ['enable_constructor_extraction' => false]); + + $this->assertSame(PropertyWriteInfo::TYPE_CONSTRUCTOR, $writeMutatorConstructor->getType()); + $this->assertSame(PropertyWriteInfo::TYPE_NONE, $writeMutatorWithoutConstructor->getType()); + } + /** * @dataProvider extractConstructorTypesProvider */ @@ -612,7 +621,7 @@ public function testExtractConstructorTypes(string $property, array $type = null $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } - public function extractConstructorTypesProvider(): array + public static function extractConstructorTypesProvider(): array { return [ ['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], diff --git a/composer.json b/composer.json index 1eff92d..f2e7957 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "keywords": [ "property", "type", - "PHPDoc", + "phpdoc", "symfony", "validator", "doctrine"