Skip to content

Commit

Permalink
feat: Add support for PHP Attributes
Browse files Browse the repository at this point in the history
- Show `<<attribute>>` stereotype for Attribute classes (Annotations)
  • Loading branch information
MontealegreLuis committed Apr 26, 2022
1 parent 7158b92 commit df4c134
Show file tree
Hide file tree
Showing 20 changed files with 172 additions and 32 deletions.
5 changes: 5 additions & 0 deletions docs/format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<<trait>>` stereotype above its name, and attributes (annotations) will be shown with the `<<attribute>>` stereotype above its name.

## Relationships

* **Associations** are solid lines without arrows
Expand Down
8 changes: 3 additions & 5 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
Expand Down
16 changes: 9 additions & 7 deletions src/Code/ClassDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ final class ClassDefinition extends Definition implements HasAttributes, HasCons
use WithConstants;
use WithTraits;

/** @var Name[] */
private readonly array $interfaces;

/**
* @param Method[] $methods
* @param Constant[] $constants
Expand All @@ -42,15 +39,15 @@ public function __construct(
Name $name,
array $methods = [],
array $constants = [],
protected ?Name $parent = null,
private readonly ?Name $parent = null,
array $attributes = [],
array $interfaces = [],
array $traits = []
private readonly array $interfaces = [],
array $traits = [],
private readonly bool $isAttribute = false
) {
parent::__construct($name, $methods);
$this->constants = $constants;
$this->attributes = $attributes;
$this->interfaces = $interfaces;
$this->traits = $traits;
}

Expand Down Expand Up @@ -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;
}
}
21 changes: 21 additions & 0 deletions src/Parser/Code/Builders/AttributeAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
/**
* PHP version 8.1
*
* This source file is subject to the license that is bundled with this package in the file LICENSE.
*/

namespace PhUml\Parser\Code\Builders;

use Attribute;
use PhpParser\Node\Stmt\Class_;

final class AttributeAnalyzer
{
public function isAttribute(Class_ $class): bool
{
return $class->attrGroups !== []
&& $class->attrGroups[0]->attrs !== []
&& (string) $class->attrGroups[0]->attrs[0]->name === Attribute::class;
}
}
6 changes: 4 additions & 2 deletions src/Parser/Code/Builders/ClassDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
}

Expand All @@ -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)
);
}
}
3 changes: 2 additions & 1 deletion src/Parser/Code/PhpCodeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
6 changes: 6 additions & 0 deletions src/resources/templates/partials/_name.html.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<TR>
<TD BORDER="{{ theme.tableBorder }}" ALIGN="CENTER" BGCOLOR="{{ theme.title.background }}">
{% if isAttribute|default(false) %}
<FONT COLOR="{{ theme.stereotype.color }}" FACE="{{ theme.stereotype.font }}" POINT-SIZE="{{ theme.stereotype.fontSize }}">
&lt;&lt;attribute&gt;&gt;
</FONT>
<BR/>
{% endif %}
<B>
<FONT COLOR="{{ theme.title.color }}" FACE="{{ theme.title.font }}" POINT-SIZE="{{ theme.title.fontSize }}">
{% if isAbstract %}<I>{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion src/resources/templates/uml/class.html.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% set tag %}
<TABLE CELLSPACING="0" BORDER="0" ALIGN="LEFT">
{% 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} %}
</TABLE>
Expand Down
5 changes: 5 additions & 0 deletions tests/resources/.code/classes/php/listener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
#[Attribute]
class Listener {

}
Binary file modified tests/resources/images/graphviz-dot-classic-theme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/images/graphviz-dot-php-theme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/images/graphviz-dot-recursive.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/images/graphviz-neato-recursive.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion tests/src/TestBuilders/ClassBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ final class ClassBuilder extends DefinitionBuilder
/** @var Name[] */
private array $traits = [];

private bool $isAttribute = false;

public function extending(Name $parent): ClassBuilder
{
$this->parent = $parent;
Expand All @@ -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(
Expand All @@ -52,7 +60,8 @@ public function build(): ClassDefinition
$this->parent,
$this->attributes,
$this->interfaces,
$this->traits
$this->traits,
$this->isAttribute
);
}
}
19 changes: 13 additions & 6 deletions tests/unit/Code/ClassDefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();

Expand All @@ -72,8 +70,7 @@ function it_knows_the_interfaces_it_implements()
];
$classWithInterfaces = A::class('ClassWithInterfaces')
->implementing(...$interfaces)
->build()
;
->build();

$classInterfaces = $classWithInterfaces->interfaces();

Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/Generators/StatisticsGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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();
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/Parser/Code/Builders/AttributeAnalyzerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
/**
* PHP version 8.1
*
* This source file is subject to the license that is bundled with this package in the file LICENSE.
*/

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_;
use PHPUnit\Framework\TestCase;

final class AttributeAnalyzerTest extends TestCase
{
/** @test */
function it_detects_attribute_classes()
{
$attributeClass = new Class_(new Identifier('AnAttributeClass'), [
'attrGroups' => [
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');
}
}
38 changes: 34 additions & 4 deletions tests/unit/Parser/Code/Builders/ClassDefinitionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand All @@ -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'))
Expand All @@ -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(
Expand All @@ -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;
}
Loading

0 comments on commit df4c134

Please sign in to comment.