Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 834c609

Browse files
committed
feat: Enhance connectors to support Trait_ and add TraitUsageConnector for managing trait relationships
1 parent a99f937 commit 834c609

File tree

13 files changed

+670
-23
lines changed

13 files changed

+670
-23
lines changed

src/ClassDiagramRenderer/Node/Connector/CompositionConnector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function connect(Nodes $nodes): void
2626

2727
public static function parse(
2828
NodeFinder $nodeFinder,
29-
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_ $classLike,
29+
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_|Node\Stmt\Trait_ $classLike,
3030
ClassDiagramNode $classDiagramNode,
3131
): self
3232
{

src/ClassDiagramRenderer/Node/Connector/Connector.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,17 @@ public function __construct(protected string $nodeName, protected array $toConne
1616
}
1717

1818
abstract public function connect(Nodes $nodes): void;
19+
20+
public function nodeName(): string
21+
{
22+
return $this->nodeName;
23+
}
24+
25+
/**
26+
* @return string[]
27+
*/
28+
public function toConnectNodeNames(): array
29+
{
30+
return $this->toConnectNodeNames;
31+
}
1932
}

src/ClassDiagramRenderer/Node/Connector/DependencyConnector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function connect(Nodes $nodes): void
2626

2727
public static function parse(
2828
NodeFinder $nodeFinder,
29-
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_ $classLike,
29+
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_|Node\Stmt\Trait_ $classLike,
3030
ClassDiagramNode $classDiagramNode,
3131
): self
3232
{

src/ClassDiagramRenderer/Node/Connector/InheritanceConnector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private function createDefaultExtendsNode(ClassDiagramNode $extended, string $ex
3131
}
3232

3333
public static function parse(
34-
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_ $classLike,
34+
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_|Node\Stmt\Trait_ $classLike,
3535
ClassDiagramNode $classDiagramNode,
3636
): self
3737
{

src/ClassDiagramRenderer/Node/Connector/RealizationConnector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function connect(Nodes $nodes): void
2222
}
2323

2424
public static function parse(
25-
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_ $classLike,
25+
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_|Node\Stmt\Trait_ $classLike,
2626
ClassDiagramNode $classDiagramNode,
2727
): self
2828
{
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Connector;
5+
6+
use PhpParser\Node;
7+
use PhpParser\NodeFinder;
8+
use Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Nodes;
9+
use Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Node as ClassDiagramNode;
10+
use Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Trait_ as DiagramTrait;
11+
12+
class TraitUsageConnector extends Connector
13+
{
14+
public function connect(Nodes $nodes): void
15+
{
16+
$usingNode = $nodes->findByName($this->nodeName);
17+
if ($usingNode === null) {
18+
return;
19+
}
20+
21+
foreach ($this->toConnectNodeNames as $traitName) {
22+
$traitNode = $nodes->findByName($traitName) ?? new DiagramTrait($traitName);
23+
$usingNode->useTrait($traitNode);
24+
}
25+
}
26+
27+
public static function parse(
28+
NodeFinder $nodeFinder,
29+
Node\Stmt\Interface_|Node\Stmt\Class_|Node\Stmt\Enum_|Node\Stmt\Trait_ $classLike,
30+
ClassDiagramNode $classDiagramNode,
31+
): self {
32+
$traitNames = [];
33+
34+
$traitUses = $nodeFinder->findInstanceOf($classLike, Node\Stmt\TraitUse::class);
35+
foreach ($traitUses as $traitUse) {
36+
assert($traitUse instanceof Node\Stmt\TraitUse);
37+
foreach ($traitUse->traits as $name) {
38+
$traitNames[] = (string)$name->getLast();
39+
}
40+
}
41+
42+
$traitNames = array_values(array_unique($traitNames));
43+
44+
return new self($classDiagramNode->nodeName(), $traitNames);
45+
}
46+
}

src/ClassDiagramRenderer/Node/Node.php

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ public function useTrait(Node $trait): void
5757
$this->traits->add($trait);
5858
}
5959

60+
/**
61+
* Return trait-derived compositions and dependencies for this node.
62+
* @return array{array<string, Node>, array<string, Node>} [compositions, dependencies]
63+
*/
64+
public function traitAggregates(): array
65+
{
66+
$traitCompositions = [];
67+
$traitDependencies = [];
68+
$visitedTraits = [];
69+
foreach ($this->traits->getAllNodes() as $traitNode) {
70+
$this->collectTraitRelations($traitNode, $visitedTraits, $traitCompositions, $traitDependencies);
71+
}
72+
return [$traitCompositions, $traitDependencies];
73+
}
74+
6075
public function nodeName(): string
6176
{
6277
return $this->name;
@@ -69,23 +84,67 @@ public function relationships(): array
6984
{
7085
$extends = $this->extends->getAllNodes();
7186
$implements = $this->implements->getAllNodes();
72-
$properties = $this->properties->getAllNodes();
73-
$depends = array_filter($this->depends->getAllNodes(), function (string $key) use ($extends, $implements, $properties) {
74-
return !array_key_exists($key, $properties)
75-
&& !array_key_exists($key, $extends)
76-
&& !array_key_exists($key, $implements)
87+
$ownProperties = $this->properties->getAllNodes();
88+
$ownDepends = $this->depends->getAllNodes();
89+
90+
// Collect trait-derived relations (no mutation of own collections)
91+
$traitCompositions = [];
92+
$traitDependencies = [];
93+
$visitedTraits = [];
94+
foreach ($this->traits->getAllNodes() as $traitNode) {
95+
$this->collectTraitRelations($traitNode, $visitedTraits, $traitCompositions, $traitDependencies);
96+
}
97+
98+
// Final sets (properties win over dependencies)
99+
$finalProperties = $ownProperties + $traitCompositions; // keep own over trait
100+
101+
$depsUnion = $ownDepends + $traitDependencies; // keep own over trait
102+
$finalDepends = array_filter($depsUnion, function (string $key) use ($extends, $implements, $finalProperties) {
103+
return !array_key_exists($key, $finalProperties)
104+
&& !array_key_exists($key, $extends)
105+
&& !array_key_exists($key, $implements)
77106
&& $key !== $this->nodeName();
78107
}, ARRAY_FILTER_USE_KEY);
79108

80109
return [
81110
...array_values(array_map(fn(Node $extendsNode) => new Inheritance($this, $extendsNode), $extends)),
82111
...array_values(array_map(fn(Node $implementsNode) => new Realization($this, $implementsNode), $implements)),
83-
...array_values(array_map(fn(Node $propertyNode) => new Composition($this, $propertyNode), $properties)),
84-
...array_values(array_map(fn(Node $dependNode) => new Dependency($this, $dependNode), $depends)),
112+
...array_values(array_map(fn(Node $propertyNode) => new Composition($this, $propertyNode), $finalProperties)),
113+
...array_values(array_map(fn(Node $dependNode) => new Dependency($this, $dependNode), $finalDepends)),
85114
...$this->extraRelationships,
86115
];
87116
}
88117

118+
/**
119+
* Recursively collect composition/dependency from the given trait and nested traits.
120+
*
121+
* @param Node $traitNode
122+
* @param array $visited visited trait names
123+
* @param array $compositionsOut [name => Node]
124+
* @param array $dependenciesOut [name => Node]
125+
*/
126+
private function collectTraitRelations(Node $traitNode, array &$visited, array &$compositionsOut, array &$dependenciesOut): void
127+
{
128+
$traitName = $traitNode->nodeName();
129+
if (isset($visited[$traitName])) {
130+
return;
131+
}
132+
$visited[$traitName] = true;
133+
134+
// Direct compositions and dependencies declared in the trait
135+
foreach ($traitNode->properties->getAllNodes() as $name => $node) {
136+
$compositionsOut[$name] = $node;
137+
}
138+
foreach ($traitNode->depends->getAllNodes() as $name => $node) {
139+
$dependenciesOut[$name] = $node;
140+
}
141+
142+
// Nested trait uses
143+
foreach ($traitNode->traits->getAllNodes() as $nestedTrait) {
144+
$this->collectTraitRelations($nestedTrait, $visited, $compositionsOut, $dependenciesOut);
145+
}
146+
}
147+
89148
public static function sortNodes(array &$nodes): void
90149
{
91150
usort($nodes, function (Node $a, Node $b) {

tests/ClassDiagramRenderer/Node/Connector/CompositionConnectorTest.php

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
namespace Tasuku43\Tests\MermaidClassDiagram\ClassDiagramRenderer\Node\Connector;
55

6+
use PhpParser\Node\Stmt\Interface_;
7+
use PhpParser\Node\Stmt\Trait_;
68
use PHPUnit\Framework\TestCase;
7-
use PhpParser\Node;
8-
use PhpParser\Node\Name;
99
use PhpParser\Node\Stmt\Class_;
10-
use PhpParser\Node\Stmt\ClassMethod;
11-
use PhpParser\Node\Param;
1210
use PhpParser\NodeFinder;
11+
use PhpParser\ParserFactory;
12+
use PhpParser\PhpVersion;
1313
use Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Class_ as DiagramClass;
1414
use Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Connector\CompositionConnector;
1515
use Tasuku43\MermaidClassDiagram\ClassDiagramRenderer\Node\Nodes;
@@ -41,4 +41,85 @@ public function testConnect(): void
4141
$this->assertCount(1, $relationships);
4242
$this->assertStringContainsString('Container *-- Contained: composition', $relationships[0]->render());
4343
}
44+
45+
/**
46+
* Test the parse method collects compositions from AST
47+
*/
48+
public function testParse(): void
49+
{
50+
$code = <<<'PHP'
51+
<?php
52+
class Container {
53+
public function __construct(public \Contained $c) {}
54+
private \AnotherContained $p;
55+
}
56+
PHP;
57+
58+
$parser = (new ParserFactory)->createForVersion(PhpVersion::fromComponents(8, 1));
59+
$nodeFinder = new NodeFinder();
60+
$ast = $parser->parse($code);
61+
$classLike = $nodeFinder->findFirstInstanceOf($ast, Class_::class);
62+
63+
$containerNode = new DiagramClass('Container');
64+
$connector = CompositionConnector::parse($nodeFinder, $classLike, $containerNode);
65+
// Compare parsed connector state with expected
66+
$expected = new CompositionConnector('Container', ['AnotherContained', 'Contained']);
67+
$this->assertEquals($expected, $connector);
68+
}
69+
70+
public function testParseOnEnum(): void
71+
{
72+
$code = <<<'PHP'
73+
<?php
74+
enum E {
75+
private \Contained $c;
76+
}
77+
PHP;
78+
79+
$parser = (new ParserFactory)->createForVersion(PhpVersion::fromComponents(8, 1));
80+
$nodeFinder = new NodeFinder();
81+
$ast = $parser->parse($code);
82+
$enumLike = $nodeFinder->findFirstInstanceOf($ast, \PhpParser\Node\Stmt\Enum_::class);
83+
84+
$enumNode = new DiagramClass('E');
85+
$connector = CompositionConnector::parse($nodeFinder, $enumLike, $enumNode);
86+
$expected = new CompositionConnector('E', ['Contained']);
87+
$this->assertEquals($expected, $connector);
88+
}
89+
90+
public function testParseOnTrait(): void
91+
{
92+
$code = <<<'PHP'
93+
<?php
94+
trait T { private \Contained $p; }
95+
PHP;
96+
97+
$parser = (new ParserFactory)->createForVersion(PhpVersion::fromComponents(8, 1));
98+
$nodeFinder = new NodeFinder();
99+
$ast = $parser->parse($code);
100+
$traitLike = $nodeFinder->findFirstInstanceOf($ast, Trait_::class);
101+
102+
$tNode = new DiagramClass('T');
103+
$connector = CompositionConnector::parse($nodeFinder, $traitLike, $tNode);
104+
$expected = new CompositionConnector('T', ['Contained']);
105+
$this->assertEquals($expected, $connector);
106+
}
107+
108+
public function testParseOnInterface(): void
109+
{
110+
$code = <<<'PHP'
111+
<?php
112+
interface I { /* no properties allowed in interface */ }
113+
PHP;
114+
115+
$parser = (new ParserFactory)->createForVersion(PhpVersion::fromComponents(8, 1));
116+
$nodeFinder = new NodeFinder();
117+
$ast = $parser->parse($code);
118+
$iface = $nodeFinder->findFirstInstanceOf($ast, Interface_::class);
119+
120+
$iNode = new DiagramClass('I');
121+
$connector = CompositionConnector::parse($nodeFinder, $iface, $iNode);
122+
$expected = new CompositionConnector('I', []);
123+
$this->assertEquals($expected, $connector);
124+
}
44125
}

0 commit comments

Comments
 (0)