From 54c1b6bd6fd2c14c46f032c7f6a67d9e2ccfde89 Mon Sep 17 00:00:00 2001 From: Michael Petri Date: Wed, 24 Nov 2021 22:06:41 +0100 Subject: [PATCH] Added support for promoted parameters Signed-off-by: Michael Petri --- src/Generator/ClassGenerator.php | 44 ++++++--- src/Generator/MethodGenerator.php | 7 +- src/Generator/PromotedParameterGenerator.php | 98 +++++++++++++++++++ src/Reflection/MethodReflection.php | 29 +++++- src/Reflection/ParameterReflection.php | 38 +++++++ test/Generator/ClassGeneratorTest.php | 67 +++++++++++++ test/Generator/MethodGeneratorTest.php | 15 +++ .../TestAsset/ClassWithPromotedParameter.php | 11 +++ test/Reflection/MethodReflectionTest.php | 31 ++++++ test/Reflection/ParameterReflectionTest.php | 14 +++ .../TestAsset/ClassWithPromotedParameter.php | 12 +++ 11 files changed, 350 insertions(+), 16 deletions(-) create mode 100644 src/Generator/PromotedParameterGenerator.php create mode 100644 test/Generator/TestAsset/ClassWithPromotedParameter.php create mode 100644 test/Reflection/TestAsset/ClassWithPromotedParameter.php diff --git a/src/Generator/ClassGenerator.php b/src/Generator/ClassGenerator.php index cc185564..e8b981b5 100644 --- a/src/Generator/ClassGenerator.php +++ b/src/Generator/ClassGenerator.php @@ -2,6 +2,7 @@ namespace Laminas\Code\Generator; +use Laminas\Code\Generator\Exception\InvalidArgumentException; use Laminas\Code\Reflection\ClassReflection; use function array_diff; @@ -33,6 +34,7 @@ class ClassGenerator extends AbstractGenerator implements TraitUsageInterface public const IMPLEMENTS_KEYWORD = 'implements'; public const FLAG_ABSTRACT = 0x01; public const FLAG_FINAL = 0x02; + private const CONSTRUCTOR_NAME = '__construct'; protected ?FileGenerator $containingFileGenerator = null; @@ -140,7 +142,17 @@ public static function fromReflection(ClassReflection $classReflection) } if ($reflectionMethod->getDeclaringClass()->getName() == $className) { - $methods[] = MethodGenerator::fromReflection($reflectionMethod); + $method = MethodGenerator::fromReflection($reflectionMethod); + + if (self::CONSTRUCTOR_NAME === $method->getName()) { + foreach ($method->getParameters() as $parameter) { + if ($parameter instanceof PromotedParameterGenerator) { + $cg->removeProperty($parameter->getName()); + } + } + } + + $methods[] = $method; } } @@ -161,14 +173,14 @@ public static function fromReflection(ClassReflection $classReflection) * @configkey implementedinterfaces * @configkey properties * @configkey methods - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException * @param array $array * @return static */ public static function fromArray(array $array) { if (! isset($array['name'])) { - throw new Exception\InvalidArgumentException( + throw new InvalidArgumentException( 'Class generator requires that a name is provided for this object' ); } @@ -547,14 +559,14 @@ public function addConstantFromGenerator(PropertyGenerator $constant) $constantName = $constant->getName(); if (isset($this->constants[$constantName])) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'A constant by name %s already exists in this class.', $constantName )); } if (! $constant->isConst()) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'The value %s is not defined as a constant.', $constantName )); @@ -576,7 +588,7 @@ public function addConstantFromGenerator(PropertyGenerator $constant) public function addConstant($name, $value, bool $isFinal = false) { if (empty($name) || ! is_string($name)) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( '%s expects string for name', __METHOD__ )); @@ -645,7 +657,7 @@ public function addProperties(array $properties) public function addProperty($name, $defaultValue = null, $flags = PropertyGenerator::FLAG_PUBLIC) { if (! is_string($name)) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( '%s::%s expects string for name', static::class, __FUNCTION__ @@ -672,7 +684,7 @@ public function addPropertyFromGenerator(PropertyGenerator $property) $propertyName = $property->getName(); if (isset($this->properties[$propertyName])) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'A property by name %s already exists in this class.', $propertyName )); @@ -830,7 +842,7 @@ public function addMethod( $docBlock = null ) { if (! is_string($name)) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( '%s::%s expects string for name', static::class, __FUNCTION__ @@ -851,12 +863,20 @@ public function addMethodFromGenerator(MethodGenerator $method) $methodName = $method->getName(); if ($this->hasMethod($methodName)) { - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'A method by name %s already exists in this class.', $methodName )); } + if (self::CONSTRUCTOR_NAME !== $methodName) { + foreach ($method->getParameters() as $parameter) { + if ($parameter instanceof PromotedParameterGenerator) { + throw new InvalidArgumentException('Promoted parameter can only be added to constructor.'); + } + } + } + $this->methods[strtolower($methodName)] = $method; return $this; } @@ -1102,7 +1122,7 @@ public function generate() /** * @param mixed $value * @return void - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ private function validateConstantValue($value) { @@ -1116,7 +1136,7 @@ private function validateConstantValue($value) return; } - throw new Exception\InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'Expected value for constant, value must be a "scalar" or "null", "%s" found', gettype($value) )); diff --git a/src/Generator/MethodGenerator.php b/src/Generator/MethodGenerator.php index 20ba54f7..19ecb540 100644 --- a/src/Generator/MethodGenerator.php +++ b/src/Generator/MethodGenerator.php @@ -16,6 +16,7 @@ use function substr; use function trim; use function uasort; +use function var_dump; class MethodGenerator extends AbstractMemberGenerator { @@ -77,7 +78,11 @@ public static function copyMethodSignature(MethodReflection $reflectionMethod): $method->setName($reflectionMethod->getName()); foreach ($reflectionMethod->getParameters() as $reflectionParameter) { - $method->setParameter(ParameterGenerator::fromReflection($reflectionParameter)); + $method->setParameter( + $reflectionParameter->isPromoted() + ? PromotedParameterGenerator::fromReflection($reflectionParameter) + : ParameterGenerator::fromReflection($reflectionParameter) + ); } return $method; diff --git a/src/Generator/PromotedParameterGenerator.php b/src/Generator/PromotedParameterGenerator.php new file mode 100644 index 00000000..56cd02cf --- /dev/null +++ b/src/Generator/PromotedParameterGenerator.php @@ -0,0 +1,98 @@ +visibility = $visibility; + } + + /** @psalm-return non-empty-string */ + public function generate(): string + { + return $this->visibility . ' ' . parent::generate(); + } + + public static function fromReflection(ParameterReflection $reflectionParameter): self + { + if (! $reflectionParameter->isPromoted()) { + throw new RuntimeException( + sprintf('Can not create "%s" from unprompted reflection.', self::class) + ); + } + + $visibility = self::VISIBILITY_PUBLIC; + + if ($reflectionParameter->isProtectedPromoted()) { + $visibility = self::VISIBILITY_PROTECTED; + } elseif ($reflectionParameter->isPrivatePromoted()) { + $visibility = self::VISIBILITY_PRIVATE; + } + + return self::fromParameterGeneratorWithVisibility( + parent::fromReflection($reflectionParameter), + $visibility + ); + } + + /** @psalm-param PromotedParameterGenerator::VISIBILITY_* $visibility */ + public static function fromParameterGeneratorWithVisibility(ParameterGenerator $generator, string $visibility): self + { + $name = $generator->getName(); + $type = $generator->getType(); + + if ('' === $name) { + throw new \Laminas\Code\Generator\Exception\RuntimeException( + 'Name of promoted parameter must be non-empty-string.' + ); + } + + if ('' === $type) { + throw new \Laminas\Code\Generator\Exception\RuntimeException( + 'Type of promoted parameter must be non-empty-string.' + ); + } + + return new self( + $name, + $type, + $visibility, + $generator->getPosition(), + $generator->getPassedByReference() + ); + } +} diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 4cdba089..b026b703 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -5,6 +5,7 @@ use ReflectionMethod as PhpReflectionMethod; use ReturnTypeWillChange; +use function array_key_exists; use function array_shift; use function array_slice; use function class_exists; @@ -116,15 +117,37 @@ public function getPrototype($format = self::PROTOTYPE_AS_ARRAY) 'by_ref' => $parameter->isPassedByReference(), 'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, ]; + + if ($parameter->isPromoted()) { + $prototype['arguments'][$parameter->getName()]['promoted'] = true; + if ($parameter->isPublicPromoted()) { + $prototype['arguments'][$parameter->getName()]['visibility'] = 'public'; + } elseif ($parameter->isProtectedPromoted()) { + $prototype['arguments'][$parameter->getName()]['visibility'] = 'protected'; + } elseif ($parameter->isPrivatePromoted()) { + $prototype['arguments'][$parameter->getName()]['visibility'] = 'private'; + } + } } if ($format == self::PROTOTYPE_AS_STRING) { $line = $prototype['visibility'] . ' ' . $prototype['return'] . ' ' . $prototype['name'] . '('; $args = []; foreach ($prototype['arguments'] as $name => $argument) { - $argsLine = ($argument['type'] ? - $argument['type'] . ' ' - : '') . ($argument['by_ref'] ? '&' : '') . '$' . $name; + $argsLine = + ( + array_key_exists('visibility', $argument) + ? $argument['visibility'] . ' ' + : '' + ) . ( + $argument['type'] + ? $argument['type'] . ' ' + : '' + ) . ( + $argument['by_ref'] + ? '&' + : '' + ) . '$' . $name; if (! $argument['required']) { $argsLine .= ' = ' . var_export($argument['default'], true); } diff --git a/src/Reflection/ParameterReflection.php b/src/Reflection/ParameterReflection.php index 22ef6283..5e11ec3a 100644 --- a/src/Reflection/ParameterReflection.php +++ b/src/Reflection/ParameterReflection.php @@ -6,6 +6,7 @@ use ReflectionClass; use ReflectionMethod; use ReflectionParameter; +use ReflectionProperty; use ReturnTypeWillChange; use function method_exists; @@ -130,4 +131,41 @@ public function __toString() { return parent::__toString(); } + + /** @psalm-pure */ + public function isPromoted(): bool + { + if (! method_exists(parent::class, 'isPromoted')) { + return false; + } + + return (bool) parent::isPromoted(); + } + + public function isPublicPromoted(): bool + { + return $this->isPromoted() + && $this->getDeclaringClass() + ->getProperty($this->getName()) + ->getModifiers() + & ReflectionProperty::IS_PUBLIC; + } + + public function isProtectedPromoted(): bool + { + return $this->isPromoted() + && $this->getDeclaringClass() + ->getProperty($this->getName()) + ->getModifiers() + & ReflectionProperty::IS_PROTECTED; + } + + public function isPrivatePromoted(): bool + { + return $this->isPromoted() + && $this->getDeclaringClass() + ->getProperty($this->getName()) + ->getModifiers() + & ReflectionProperty::IS_PRIVATE; + } } diff --git a/test/Generator/ClassGeneratorTest.php b/test/Generator/ClassGeneratorTest.php index 60299962..13f0f7e1 100644 --- a/test/Generator/ClassGeneratorTest.php +++ b/test/Generator/ClassGeneratorTest.php @@ -9,8 +9,10 @@ use Laminas\Code\Generator\Exception\InvalidArgumentException; use Laminas\Code\Generator\GeneratorInterface; use Laminas\Code\Generator\MethodGenerator; +use Laminas\Code\Generator\PromotedParameterGenerator; use Laminas\Code\Generator\PropertyGenerator; use Laminas\Code\Reflection\ClassReflection; +use LaminasTest\Code\Generator\TestAsset\ClassWithPromotedParameter; use LaminasTest\Code\TestAsset\FooClass; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -22,6 +24,7 @@ use function fclose; use function fopen; use function key; +use function uniqid; /** * @group Laminas_Code_Generator @@ -1356,4 +1359,68 @@ class ClassWithFinalConst $output = $classGenerator->generate(); self::assertSame($expectedOutput, $output, $output); } + + /** @requires PHP >= 8.0 */ + public function testGenerateClassWithPromotedConstructorParameter(): void + { + $classGenerator = new ClassGenerator(); + $classGenerator->setName('ClassWithPromotedParameter'); + + $classGenerator->addMethod('__construct', [ + new PromotedParameterGenerator( + 'bar', + 'Foo', + PromotedParameterGenerator::VISIBILITY_PRIVATE, + ), + ]); + + $expectedOutput = <<generate()); + } + + /** @requires PHP >= 8.0 */ + public function testClassWithPromotedParameterFromReflection(): void + { + $classGenerator = ClassGenerator::fromReflection( + new ClassReflection(ClassWithPromotedParameter::class) + ); + + $expectedOutput = <<generate()); + } + + /** @requires PHP >= 8.0 */ + public function testFailToGenerateClassWithPromotedParameterOnNonConstructorMethod(): void + { + $classGenerator = new ClassGenerator(); + $classGenerator->setName('promotedParameterOnNonConstructorMethod'); + + $this->expectExceptionObject( + new InvalidArgumentException('Promoted parameter can only be added to constructor.') + ); + + $classGenerator->addMethod('thisIsNoConstructor', [ + new PromotedParameterGenerator('promotedParameter', 'string'), + ]); + } } diff --git a/test/Generator/MethodGeneratorTest.php b/test/Generator/MethodGeneratorTest.php index 1dbc8853..268f63db 100644 --- a/test/Generator/MethodGeneratorTest.php +++ b/test/Generator/MethodGeneratorTest.php @@ -133,6 +133,21 @@ protected function withParamsAndReturnType($mixed, array $array, ?callable $call { } +EOS; + self::assertSame($target, (string) $methodGenerator); + } + + /** @requires PHP >= 8.0 */ + public function testCopyMethodSignatureForPromotedParameter(): void + { + $ref = new MethodReflection(TestAsset\ClassWithPromotedParameter::class, '__construct'); + + $methodGenerator = MethodGenerator::copyMethodSignature($ref); + $target = <<<'EOS' + public function __construct(private string $promotedParameter) + { + } + EOS; self::assertSame($target, (string) $methodGenerator); } diff --git a/test/Generator/TestAsset/ClassWithPromotedParameter.php b/test/Generator/TestAsset/ClassWithPromotedParameter.php new file mode 100644 index 00000000..ff88964b --- /dev/null +++ b/test/Generator/TestAsset/ClassWithPromotedParameter.php @@ -0,0 +1,11 @@ += 8.0 */ + public function testGetPrototypeMethodForPromotedParameter(): void + { + $reflectionMethod = new MethodReflection( + TestAsset\ClassWithPromotedParameter::class, + '__construct' + ); + $prototype = [ + 'namespace' => 'LaminasTest\Code\Reflection\TestAsset', + 'class' => 'ClassWithPromotedParameter', + 'name' => '__construct', + 'visibility' => 'public', + 'return' => 'mixed', + 'arguments' => [ + 'promotedParameter' => [ + 'type' => 'string', + 'required' => true, + 'by_ref' => false, + 'default' => null, + 'promoted' => true, + 'visibility' => 'private', + ], + ], + ]; + self::assertEquals($prototype, $reflectionMethod->getPrototype()); + self::assertEquals( + 'public mixed __construct(private string $promotedParameter)', + $reflectionMethod->getPrototype(MethodReflection::PROTOTYPE_AS_STRING) + ); + } + /** * @group 5062 */ diff --git a/test/Reflection/ParameterReflectionTest.php b/test/Reflection/ParameterReflectionTest.php index 7d8c569d..11b12622 100644 --- a/test/Reflection/ParameterReflectionTest.php +++ b/test/Reflection/ParameterReflectionTest.php @@ -5,6 +5,7 @@ use Closure; use Laminas\Code\Reflection; use Laminas\Code\Reflection\ClassReflection; +use LaminasTest\Code\Reflection\TestAsset\ClassWithPromotedParameter; use LaminasTest\Code\TestAsset\ClassTypeHintedClass; use LaminasTest\Code\TestAsset\DocBlockOnlyHintsClass; use LaminasTest\Code\TestAsset\InternalHintsClass; @@ -227,4 +228,17 @@ public function docBlockHints() [DocBlockOnlyHintsClass::class, 'otherClassParameter', 'foo', 'InternalHintsClass'], ]; } + + public function testPromotedParameter(): void + { + $reflection = new Reflection\ParameterReflection( + [ClassWithPromotedParameter::class, '__construct'], + 'promotedParameter' + ); + + self::assertTrue($reflection->isPromoted()); + self::assertTrue($reflection->isPrivatePromoted()); + self::assertFalse($reflection->isProtectedPromoted()); + self::assertFalse($reflection->isPublicPromoted()); + } } diff --git a/test/Reflection/TestAsset/ClassWithPromotedParameter.php b/test/Reflection/TestAsset/ClassWithPromotedParameter.php new file mode 100644 index 00000000..5e6afcd6 --- /dev/null +++ b/test/Reflection/TestAsset/ClassWithPromotedParameter.php @@ -0,0 +1,12 @@ +