diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 62296047..70aab3e9 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -17,7 +17,6 @@ jobs: - "highest" - "locked" php-version: - - "7.4" - "8.0" - "8.1" operating-system: diff --git a/src/Generator/AttributeGenerator.php b/src/Generator/AttributeGenerator.php new file mode 100644 index 00000000..685711b5 --- /dev/null +++ b/src/Generator/AttributeGenerator.php @@ -0,0 +1,82 @@ +assemblers = $assembler; + } + + public function generate(): string + { + $generatedAttributes = array_map(fn(AttributeAssembler $attributeAssembler) => $attributeAssembler->assemble(), + $this->assemblers, + ); + + return implode(AbstractGenerator::LINE_FEED, $generatedAttributes); + } + + public static function fromPrototype(AttributePrototype ...$attributePrototype): self + { + $assemblers = []; + + foreach ($attributePrototype as $prototype) { + $assemblers[] = self::negotiateAssembler($prototype); + } + + return new self(...$assemblers); + } + + public static function fromReflection(ReflectionClass $reflectionClass): self + { + $attributes = $reflectionClass->getAttributes(); + $assemblers = []; + + foreach ($attributes as $attribute) { + $assembler = self::negotiateAssembler($attribute); + + $assemblers[] = $assembler; + } + + return new self(...$assemblers); + } + + public static function fromArray(array $definitions): self + { + $assemblers = []; + + foreach ($definitions as $definition) { + @list($attributeName, $attributeArguments) = $definition; + + $prototype = new AttributePrototype($attributeName, $attributeArguments ?? []); + + $assemblers[] = self::negotiateAssembler($prototype); + } + + return new self(...$assemblers); + } + + private static function negotiateAssembler(ReflectionAttribute|AttributePrototype $reflectionPrototype): AttributeAssembler + { + $hasArguments = !empty($reflectionPrototype->getArguments()); + + if ($hasArguments) { + return new AttributeWithArgumentsAssembler($reflectionPrototype); + } + + return new SimpleAttributeAssembler($reflectionPrototype); + } +} diff --git a/src/Generator/AttributeGenerator/AbstractAttributeAssembler.php b/src/Generator/AttributeGenerator/AbstractAttributeAssembler.php new file mode 100644 index 00000000..679a1fa0 --- /dev/null +++ b/src/Generator/AttributeGenerator/AbstractAttributeAssembler.php @@ -0,0 +1,24 @@ +attributePrototype->getName(); + } + + final protected function getArguments(): array + { + return $this->attributePrototype->getArguments(); + } +} diff --git a/src/Generator/AttributeGenerator/AttributeAssembler.php b/src/Generator/AttributeGenerator/AttributeAssembler.php new file mode 100644 index 00000000..22eb48d9 --- /dev/null +++ b/src/Generator/AttributeGenerator/AttributeAssembler.php @@ -0,0 +1,11 @@ +attributeName; + } + + public function getArguments(): array + { + return $this->arguments; + } +} diff --git a/src/Generator/AttributeGenerator/AttributeWithArgumentsAssembler.php b/src/Generator/AttributeGenerator/AttributeWithArgumentsAssembler.php new file mode 100644 index 00000000..b4db6a16 --- /dev/null +++ b/src/Generator/AttributeGenerator/AttributeWithArgumentsAssembler.php @@ -0,0 +1,47 @@ +getName(); + + $attributeDefinition = AttributePart::T_ATTR_START . $attributeName . AttributePart::T_ATTR_ARGUMENTS_LIST_START; + + $this->generateArguments($attributeDefinition); + + return $attributeDefinition . AttributePart::T_ATTR_END; + } + + private function generateArguments(string &$output): void + { + $argumentsList = []; + + foreach ($this->getArguments() as $argumentName => $argumentValue) { + $argumentsList[] = $argumentName . AttributePart::T_ATTR_ARGUMENTS_LIST_ASSIGN_OPERAND . $this->formatArgumentValue($argumentValue); + } + + $output .= implode(AttributePart::T_ATTR_ARGUMENTS_LIST_SEPARATOR, $argumentsList); + $output .= AttributePart::T_ATTR_ARGUMENTS_LIST_END; + } + + private function formatArgumentValue(mixed $argument): mixed + { + switch (true) { + case is_string($argument): + return "'$argument'"; + case is_bool($argument): + return $argument ? 'true' : 'false'; + case is_array($argument): + throw NestedAttributesAreNotSupportedException::create(); + default: + return $argument; + } + } +} diff --git a/src/Generator/AttributeGenerator/Exception/NestedAttributesAreNotSupportedException.php b/src/Generator/AttributeGenerator/Exception/NestedAttributesAreNotSupportedException.php new file mode 100644 index 00000000..c823b5e9 --- /dev/null +++ b/src/Generator/AttributeGenerator/Exception/NestedAttributesAreNotSupportedException.php @@ -0,0 +1,15 @@ +assertAttributeWithoutArguments(); + } + + public function assemble(): string + { + $attributeName = $this->getName(); + + return AttributePart::T_ATTR_START . $attributeName . AttributePart::T_ATTR_END; + } + + private function assertAttributeWithoutArguments(): void + { + $arguments = $this->getArguments(); + + if (!empty($arguments)) { + throw new NotEmptyArgumentListException('Argument list has to be empty'); + } + } +} diff --git a/src/Generator/ClassGenerator.php b/src/Generator/ClassGenerator.php index c596d4ec..fef219bf 100644 --- a/src/Generator/ClassGenerator.php +++ b/src/Generator/ClassGenerator.php @@ -2,6 +2,8 @@ namespace Laminas\Code\Generator; +use InvalidArgumentException; +use Laminas\Code\Generator\AttributeGenerator\AttributeBuilder; use Laminas\Code\Reflection\ClassReflection; use function array_diff; @@ -68,6 +70,8 @@ class ClassGenerator extends AbstractGenerator implements TraitUsageInterface /** @var TraitUsageGenerator Object to encapsulate trait usage logic */ protected TraitUsageGenerator $traitUsageGenerator; + private ?AttributeGenerator $attributeGenerator = null; + /** * Build a Code Generation Php Object from a Class Reflection * @@ -198,6 +202,13 @@ public static function fromArray(array $array) $docBlock = $value instanceof DocBlockGenerator ? $value : DocBlockGenerator::fromArray($value); $cg->setDocBlock($docBlock); break; + case 'attribute': + if (!($value instanceof AttributeGenerator)) { + throw new InvalidArgumentException(sprintf('Only %s is supported', AttributeGenerator::class)); + } + + $cg->setAttributes($value); + break; case 'flags': $cg->setFlags($value); break; @@ -228,17 +239,17 @@ public static function fromArray(array $array) * @psalm-param array $interfaces * @param PropertyGenerator[]|string[]|array[] $properties * @param MethodGenerator[]|string[]|array[] $methods - * @param DocBlockGenerator $docBlock */ public function __construct( - $name = null, + string $name = null, $namespaceName = null, $flags = null, $extends = null, array $interfaces = [], array $properties = [], array $methods = [], - $docBlock = null + DocBlockGenerator $docBlock = null, + AttributeGenerator $attributeGenerator = null, ) { $this->traitUsageGenerator = new TraitUsageGenerator($this); @@ -266,6 +277,9 @@ public function __construct( if ($docBlock !== null) { $this->setDocBlock($docBlock); } + if ($attributeGenerator) { + $this->setAttributes($attributeGenerator); + } } /** @@ -336,6 +350,13 @@ public function setDocBlock(DocBlockGenerator $docBlock) return $this; } + public function setAttributes(AttributeGenerator $attributeGenerator): self + { + $this->attributeGenerator = $attributeGenerator; + + return $this; + } + /** * @return ?DocBlockGenerator */ @@ -344,6 +365,11 @@ public function getDocBlock() return $this->docBlock; } + public function getAttributes(): ?AttributeGenerator + { + return $this->attributeGenerator; + } + /** * @param int[]|int $flags * @return static @@ -1063,6 +1089,10 @@ public function generate() $output .= $docBlock->generate(); } + if ($attributeGenerator = $this->getAttributes()) { + $output .= $attributeGenerator->generate() . self::LINE_FEED; + } + if ($this->isAbstract()) { $output .= 'abstract '; } elseif ($this->isFinal()) { diff --git a/src/Generator/ValueAssembler.php b/src/Generator/ValueAssembler.php new file mode 100644 index 00000000..495e0e36 --- /dev/null +++ b/src/Generator/ValueAssembler.php @@ -0,0 +1,10 @@ +giveGenerator($prototype); + + $result = $generator->generate(); + + $expectedResult = '#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]'; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function generate_many_single_attributes(): void + { + $prototype1 = new AttributePrototype('LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute'); + $prototype2 = new AttributePrototype('LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute'); + $generator = $this->giveGenerator($prototype1, $prototype2); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]\n#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]"; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function generate_single_attribute_with_arguments(): void + { + $prototype = new AttributePrototype( + 'LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments', + [ + 'boolArgument' => false, + 'stringArgument' => 'char chain', + 'intArgument' => 16, + ], + ); + $generator = $this->giveGenerator($prototype); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments(boolArgument: false, stringArgument: 'char chain', intArgument: 16)]"; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function generate_many_attributes_with_arguments(): void + { + $prototype1 = new AttributePrototype( + 'LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments', + [ + 'boolArgument' => false, + 'stringArgument' => 'char chain', + 'intArgument' => 16, + ], + ); + $prototype2 = new AttributePrototype('LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments'); + $generator = $this->giveGenerator($prototype1, $prototype2); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments(boolArgument: false, stringArgument: 'char chain', intArgument: 16)]\n#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments]"; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function mix_simple_attributes_with_attributes_with_arguments(): void + { + $prototype1 = new AttributePrototype( + 'LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments', + [ + 'stringArgument' => 'any string', + 'intArgument' => 1, + 'boolArgument' => true, + ]); + $prototype2 = new AttributePrototype('LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute'); + $generator = $this->giveGenerator($prototype1, $prototype2); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments(stringArgument: 'any string', intArgument: 1, boolArgument: true)]\n#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]"; + $this->assertSame($expectedResult, $result); + } + + private function giveGenerator(AttributePrototype ...$prototype): AttributeGenerator + { + return AttributeGenerator::fromPrototype(...$prototype); + } +} diff --git a/test/Generator/AttributeGeneratorByReflectionTest.php b/test/Generator/AttributeGeneratorByReflectionTest.php new file mode 100644 index 00000000..3b3a4e80 --- /dev/null +++ b/test/Generator/AttributeGeneratorByReflectionTest.php @@ -0,0 +1,94 @@ +giveGenerator($classWithSimpleAttribute); + + $result = $generator->generate(); + + $expectedResult = '#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]'; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function generate_many_single_attributes(): void + { + $classWithSimpleAttribute = new ClassWithTwoSameSimpleAttributes(); + $generator = $this->giveGenerator($classWithSimpleAttribute); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]\n#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]"; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function generate_single_attribute_with_arguments(): void + { + $classWithSimpleAttribute = new ClassWithArgumentWithAttributes(); + $generator = $this->giveGenerator($classWithSimpleAttribute); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments(boolArgument: false, stringArgument: 'char chain', intArgument: 16)]"; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function generate_many_attributes_with_arguments(): void + { + $classWithSimpleAttribute = new ClassWithManyArgumentsWithAttributes(); + $generator = $this->giveGenerator($classWithSimpleAttribute); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments(boolArgument: false, stringArgument: 'char chain', intArgument: 16)]\n#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments]"; + $this->assertSame($expectedResult, $result); + } + + /** + * @test + */ + public function mix_simple_attributes_with_attributes_with_arguments(): void + { + $classWithSimpleAttribute = new ClassWithSimpleAndArgumentedAttributes(); + $generator = $this->giveGenerator($classWithSimpleAttribute); + + $result = $generator->generate(); + + $expectedResult = "#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\AttributeWithArguments(stringArgument: 'any string', intArgument: 1, boolArgument: true)]\n#[LaminasTest\Code\Generator\Fixture\AttributeGenerator\SimpleAttribute]"; + $this->assertSame($expectedResult, $result); + } + + private function giveGenerator(object $class): AttributeGenerator + { + $reflection = new ReflectionClass($class); + + return AttributeGenerator::fromReflection($reflection); + } +} diff --git a/test/Generator/ClassGeneratorTest.php b/test/Generator/ClassGeneratorTest.php index e56f610c..ccf45c1e 100644 --- a/test/Generator/ClassGeneratorTest.php +++ b/test/Generator/ClassGeneratorTest.php @@ -3,6 +3,8 @@ namespace LaminasTest\Code\Generator; use DateTime; +use Laminas\Code\Generator\AttributeGenerator; +use Laminas\Code\Generator\AttributeGenerator\AttributePrototype; use Laminas\Code\Generator\ClassGenerator; use Laminas\Code\Generator\DocBlockGenerator; use Laminas\Code\Generator\Exception\ExceptionInterface; @@ -52,6 +54,14 @@ public function testClassDocBlockAccessors(): void self::assertSame($docBlockGenerator, $classGenerator->getDocBlock()); } + public function testClassAttributesAccessors(): void + { + $attributeGenerator = AttributeGenerator::fromArray([]); + $classGenerator = new ClassGenerator(); + $classGenerator->setAttributes($attributeGenerator); + self::assertSame($attributeGenerator, $classGenerator->getAttributes()); + } + public function testAbstractAccessors(): void { $classGenerator = new ClassGenerator(); @@ -516,6 +526,49 @@ public function testCreateFromArrayWithDocBlockFromArray(): void self::assertInstanceOf(DocBlockGenerator::class, $docBlock); } + public function testCreateFromGeneratorWithAttributes(): void + { + $attributeName = 'AnyAttribute'; + $attributeArguments = ['argument' => 2]; + $attributeGenerator = AttributeGenerator::fromPrototype(new AttributePrototype($attributeName, $attributeArguments)); + + $classGenerator = ClassGenerator::fromArray([ + 'name' => 'AnyClassName', + 'attribute' => $attributeGenerator, + ]); + + $expectedGenerator = $attributeGenerator; + $attributeGenerator = $classGenerator->getAttributes(); + self::assertInstanceOf(AttributeGenerator::class, $attributeGenerator); + self::assertEquals($expectedGenerator, $attributeGenerator); + } + + public function testGenerateAttributes(): void + { + $attributeGenerator = AttributeGenerator::fromPrototype( + new AttributePrototype('FirstAttribute', ['firstArgument' => 'abc', 'secondArgument' => 12]), + new AttributePrototype('FirstAttribute', ['firstArgument' => 'abc', 'secondArgument' => 13]), + new AttributePrototype('SecondAttribute'), + ); + $classGenerator = ClassGenerator::fromArray([ + 'name' => 'AnyClassName', + 'attribute' => $attributeGenerator, + ]); + + $generatedClass = $classGenerator->generate(); + + $expectedClassOutput = <<