diff --git a/docs/format.md b/docs/format.md index 6ae9eda..dccf4ef 100644 --- a/docs/format.md +++ b/docs/format.md @@ -10,6 +10,11 @@ currentMenu: format * Static methods and functions are shown underlined * Abstract classes, abstract methods and interfaces names are shown in *italics* +## Traits and Attributes + +[Traits](https://www.php.net/manual/en/language.oop5.traits.php) and class [attributes](https://www.php.net/manual/en/language.attributes.overview.php)(annotations) will be shown with a [UML stereotype](https://www.uml-diagrams.org/stereotype.html). +Traits will be shown with the `<>` stereotype above its name, and attributes (annotations) will be shown with the `<>` stereotype above its name. + ## Relationships * **Associations** are solid lines without arrows diff --git a/docs/installation.md b/docs/installation.md index 9aedbe4..b97b0f3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,21 +8,19 @@ currentMenu: installation phUML can be installed by [Phive](https://phar.io/) - The PHAR Installation and Verification Environment. -``` +```bash phive install phuml ``` -Phive will generate a `.phive` and a `tools` directory which you may want to add to your `.gitignore` file. - ## Docker The official phUML Docker image can be found on [Docker Hub](https://hub.docker.com/r/montealegreluis/phuml/). ```bash -docker pull montealegreluis/phuml:5.2.0 +docker pull montealegreluis/phuml ``` -You can replace `5.2.0` with any of th available [tags](https://hub.docker.com/r/montealegreluis/phuml/tags?page=1&ordering=last_updated) +Here's the list of all the available Docker image [tags](https://hub.docker.com/r/montealegreluis/phuml/tags?page=1&ordering=last_updated) ## Composer diff --git a/docs/types.md b/docs/types.md index 8049021..343a47c 100644 --- a/docs/types.md +++ b/docs/types.md @@ -10,7 +10,7 @@ phUML can extract type information from doc blocks * It can extract scalar type hints via the `@param` tag * It can extract types from attributes via the `@var` tag -The class below will show type information for all of its attributes and methods +The class below will show type information for all of its properties and methods ```php constants = $constants; $this->attributes = $attributes; - $this->interfaces = $interfaces; $this->traits = $traits; } @@ -151,4 +148,9 @@ public function isAbstract(): bool { return array_filter($this->methods(), static fn (Method $method): bool => $method->isAbstract()) !== []; } + + public function isAttribute(): bool + { + return $this->isAttribute; + } } diff --git a/src/Parser/Code/Builders/AttributeAnalyzer.php b/src/Parser/Code/Builders/AttributeAnalyzer.php new file mode 100644 index 0000000..6a66338 --- /dev/null +++ b/src/Parser/Code/Builders/AttributeAnalyzer.php @@ -0,0 +1,21 @@ +attrGroups !== [] + && $class->attrGroups[0]->attrs !== [] + && (string) $class->attrGroups[0]->attrs[0]->name === Attribute::class; + } +} diff --git a/src/Parser/Code/Builders/ClassDefinitionBuilder.php b/src/Parser/Code/Builders/ClassDefinitionBuilder.php index eb0366b..576306f 100644 --- a/src/Parser/Code/Builders/ClassDefinitionBuilder.php +++ b/src/Parser/Code/Builders/ClassDefinitionBuilder.php @@ -27,7 +27,8 @@ final class ClassDefinitionBuilder public function __construct( private readonly MembersBuilder $membersBuilder, - private readonly UseStatementsBuilder $useStatementsBuilder + private readonly UseStatementsBuilder $useStatementsBuilder, + private readonly AttributeAnalyzer $analyzer ) { } @@ -42,7 +43,8 @@ public function build(Class_ $class): ClassDefinition $class->extends !== null ? new ClassDefinitionName((string) $class->extends) : null, $this->membersBuilder->attributes($class->stmts, $class->getMethod('__construct'), $useStatements), $this->buildInterfaces($class->implements), - $this->buildTraits($class->stmts) + $this->buildTraits($class->stmts), + $this->analyzer->isAttribute($class) ); } } diff --git a/src/Parser/Code/PhpCodeParser.php b/src/Parser/Code/PhpCodeParser.php index eb0d6bc..ab11163 100644 --- a/src/Parser/Code/PhpCodeParser.php +++ b/src/Parser/Code/PhpCodeParser.php @@ -15,6 +15,7 @@ use PhpParser\Parser; use PhpParser\ParserFactory; use PhUml\Code\Codebase; +use PhUml\Parser\Code\Builders\AttributeAnalyzer; use PhUml\Parser\Code\Builders\ClassDefinitionBuilder; use PhUml\Parser\Code\Builders\Filters\PrivateVisibilityFilter; use PhUml\Parser\Code\Builders\Filters\ProtectedVisibilityFilter; @@ -79,7 +80,7 @@ public static function fromConfiguration(CodeParserConfiguration $configuration) $filters = new VisibilityFilters($filters); $membersBuilder = new MembersBuilder($constantsBuilder, $attributesBuilder, $methodsBuilder, $filters); $useStatementsBuilder = new UseStatementsBuilder(); - $classBuilder = new ClassDefinitionBuilder($membersBuilder, $useStatementsBuilder); + $classBuilder = new ClassDefinitionBuilder($membersBuilder, $useStatementsBuilder, new AttributeAnalyzer()); $interfaceBuilder = new InterfaceDefinitionBuilder($membersBuilder, $useStatementsBuilder); $traitBuilder = new TraitDefinitionBuilder($membersBuilder, $useStatementsBuilder); diff --git a/src/resources/templates/partials/_name.html.twig b/src/resources/templates/partials/_name.html.twig index 673d6f5..fa786c0 100644 --- a/src/resources/templates/partials/_name.html.twig +++ b/src/resources/templates/partials/_name.html.twig @@ -1,5 +1,11 @@ + {% if isAttribute|default(false) %} + + <<attribute>> + +
+ {% endif %} {% if isAbstract %}{% endif %} diff --git a/src/resources/templates/uml/class.html.twig b/src/resources/templates/uml/class.html.twig index 4b0c378..359b35d 100644 --- a/src/resources/templates/uml/class.html.twig +++ b/src/resources/templates/uml/class.html.twig @@ -1,6 +1,6 @@ {% set tag %} - {% include 'partials/_name.html.twig' with {'definition': definition, 'isAbstract': definition.isAbstract, 'theme': theme} %} + {% include 'partials/_name.html.twig' with {'definition': definition, 'isAttribute': definition.isAttribute, 'isAbstract': definition.isAbstract, 'theme': theme} %} {% include style.attributes with {'definition': definition, 'theme' : theme} %} {% include style.methods with {'definition': definition, 'theme' : theme} %}
diff --git a/tests/resources/.code/classes/php/listener.php b/tests/resources/.code/classes/php/listener.php new file mode 100644 index 0000000..4b7f32f --- /dev/null +++ b/tests/resources/.code/classes/php/listener.php @@ -0,0 +1,5 @@ +parent = $parent; @@ -43,6 +45,12 @@ public function using(Name ...$traits): ClassBuilder return $this; } + public function withIsAttribute(): ClassBuilder + { + $this->isAttribute = true; + return $this; + } + public function build(): ClassDefinition { return new ClassDefinition( @@ -52,7 +60,8 @@ public function build(): ClassDefinition $this->parent, $this->attributes, $this->interfaces, - $this->traits + $this->traits, + $this->isAttribute ); } } diff --git a/tests/unit/Code/ClassDefinitionTest.php b/tests/unit/Code/ClassDefinitionTest.php index d021ed5..31c5c72 100644 --- a/tests/unit/Code/ClassDefinitionTest.php +++ b/tests/unit/Code/ClassDefinitionTest.php @@ -40,8 +40,7 @@ function it_has_access_to_its_constructor_parameters() ->withAPublicMethod('notAConstructor') ->withAPublicMethod('__construct', $firstParameter, $secondParameter) ->withAPublicMethod('NotAConstructorEither') - ->build() - ; + ->build(); $constructorParameters = $class->constructorParameters(); @@ -55,8 +54,7 @@ function it_knows_its_constructor_has_no_parameters_if_no_constructor_is_specifi $class = A::class('ClassWithoutConstructor') ->withAPublicMethod('notAConstructor') ->withAPublicMethod('notAConstructorEither') - ->build() - ; + ->build(); $constructorParameters = $class->constructorParameters(); @@ -72,8 +70,7 @@ function it_knows_the_interfaces_it_implements() ]; $classWithInterfaces = A::class('ClassWithInterfaces') ->implementing(...$interfaces) - ->build() - ; + ->build(); $classInterfaces = $classWithInterfaces->interfaces(); @@ -110,6 +107,16 @@ function it_fails_to_get_its_parent_class_if_none_exist() $interfaceWithParent->parent(); } + /** @test */ + function it_knows_if_it_is_an_attribute_class() + { + $attributeClass = new ClassDefinition(new Name('ADefinition'), isAttribute: true); + $regularClass = new ClassDefinition(new Name('ADefinition')); + + $this->assertTrue($attributeClass->isAttribute()); + $this->assertFalse($regularClass->isAttribute()); + } + protected function definition(array $methods = []): Definition { return new ClassDefinition(new Name('ADefinition'), $methods); diff --git a/tests/unit/Generators/StatisticsGeneratorTest.php b/tests/unit/Generators/StatisticsGeneratorTest.php index 3ab10fb..0b53cf4 100644 --- a/tests/unit/Generators/StatisticsGeneratorTest.php +++ b/tests/unit/Generators/StatisticsGeneratorTest.php @@ -60,7 +60,7 @@ function it_shows_the_statistics_of_a_directory_using_a_recursive_finder() General statistics ------------------ -Classes: 20 +Classes: 21 Interfaces: 0 Attributes: 24 (6 are typed) @@ -76,8 +76,8 @@ function it_shows_the_statistics_of_a_directory_using_a_recursive_finder() Average statistics ------------------ -Attributes per class: 1.2 -Functions per class: 4.35 +Attributes per class: 1.14 +Functions per class: 4.14 STATS; $configuration = A::statisticsGeneratorConfiguration()->recursive()->build(); diff --git a/tests/unit/Parser/Code/Builders/AttributeAnalyzerTest.php b/tests/unit/Parser/Code/Builders/AttributeAnalyzerTest.php new file mode 100644 index 0000000..6eb0a38 --- /dev/null +++ b/tests/unit/Parser/Code/Builders/AttributeAnalyzerTest.php @@ -0,0 +1,49 @@ + [ + new AttributeGroup([ + new Attribute(new Name('Attribute')), + ]), + ], + ]); + $annotatedClass = new Class_(new Identifier('AnAnnotatedClass'), [ + 'attrGroups' => [ + new AttributeGroup( + [ + new Attribute(new Name('Command')), + ] + ), + ], + ]); + $regularClass = new Class_(new Identifier('ARegularClass')); + $analyzer = new AttributeAnalyzer(); + + $isAttribute = $analyzer->isAttribute($attributeClass); + $isNotAttribute = $analyzer->isAttribute($regularClass); + $isAnnotatedBuNotAttribute = $analyzer->isAttribute($annotatedClass); + + $this->assertTrue($isAttribute, 'It should have detected this is an attribute class'); + $this->assertFalse($isNotAttribute, 'It should have detected this is not an attribute class'); + $this->assertFalse($isAnnotatedBuNotAttribute, 'It should have detected this is not an attribute class'); + } +} diff --git a/tests/unit/Parser/Code/Builders/ClassDefinitionBuilderTest.php b/tests/unit/Parser/Code/Builders/ClassDefinitionBuilderTest.php index f763b45..9cbd4b7 100644 --- a/tests/unit/Parser/Code/Builders/ClassDefinitionBuilderTest.php +++ b/tests/unit/Parser/Code/Builders/ClassDefinitionBuilderTest.php @@ -7,6 +7,8 @@ namespace PhUml\Parser\Code\Builders; +use PhpParser\Node\Attribute; +use PhpParser\Node\AttributeGroup; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; @@ -29,9 +31,8 @@ function it_builds_a_class_with_traits() ], ]); $parsedClass->namespacedName = new Name('AClassWithTraits'); - $builder = new ClassDefinitionBuilder(A::membersBuilder()->build(), new UseStatementsBuilder()); - $class = $builder->build($parsedClass); + $class = $this->builder->build($parsedClass); $expectedClassWithTraits = A::class('AClassWithTraits') ->using(new TraitName('ATrait'), new TraitName('AnotherTrait')) @@ -54,9 +55,8 @@ function it_builds_a_class_with_traits_from_multiple_use_statements() ], ]); $parsedClass->namespacedName = new Name('AClassWithTraits'); - $builder = new ClassDefinitionBuilder(A::membersBuilder()->build(), new UseStatementsBuilder()); - $class = $builder->build($parsedClass); + $class = $this->builder->build($parsedClass); $classWithTwoUseTraitStatements = A::class('AClassWithTraits') ->using( @@ -67,4 +67,34 @@ function it_builds_a_class_with_traits_from_multiple_use_statements() ->build(); $this->assertEquals($classWithTwoUseTraitStatements, $class); } + + /** @test */ + function it_builds_an_attribute_class() + { + $attributeClass = new Class_(new Identifier('AnAttributeClass'), [ + 'attrGroups' => [ + new AttributeGroup([ + new Attribute(new Name('Attribute')), + ]), + ], + ]); + $attributeClass->namespacedName = new Name('AnAttributeClass'); + + $class = $this->builder->build($attributeClass); + + $expectedAttributeClass = A::class('AnAttributeClass')->withIsAttribute()->build(); + $this->assertEquals($expectedAttributeClass, $class); + } + + /** @before */ + function let() + { + $this->builder = new ClassDefinitionBuilder( + A::membersBuilder()->build(), + new UseStatementsBuilder(), + new AttributeAnalyzer() + ); + } + + private ClassDefinitionBuilder $builder; } diff --git a/tests/unit/Parser/Code/Visitors/ClassVisitorTest.php b/tests/unit/Parser/Code/Visitors/ClassVisitorTest.php index f2a6c02..e805029 100644 --- a/tests/unit/Parser/Code/Visitors/ClassVisitorTest.php +++ b/tests/unit/Parser/Code/Visitors/ClassVisitorTest.php @@ -10,6 +10,7 @@ use PhpParser\Node\Stmt\Class_; use PHPUnit\Framework\TestCase; use PhUml\Code\Codebase; +use PhUml\Parser\Code\Builders\AttributeAnalyzer; use PhUml\Parser\Code\Builders\ClassDefinitionBuilder; use PhUml\Parser\Code\Builders\UseStatementsBuilder; use PhUml\TestBuilders\A; @@ -19,7 +20,11 @@ final class ClassVisitorTest extends TestCase /** @test */ function it_ignores_anonymous_classes() { - $builder = new ClassDefinitionBuilder(A::membersBuilder()->build(), new UseStatementsBuilder()); + $builder = new ClassDefinitionBuilder( + A::membersBuilder()->build(), + new UseStatementsBuilder(), + new AttributeAnalyzer() + ); $codebase = new Codebase(); $visitor = new ClassVisitor($builder, $codebase); $anonymousClass = new Class_(null);