Skip to content

Commit 39c024a

Browse files
committed
Add high level API for interfaces - Close #15
1 parent 1294f5d commit 39c024a

File tree

3 files changed

+335
-3
lines changed

3 files changed

+335
-3
lines changed

src/Builder/InterfaceBuilder.php

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/php-code-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/php-code-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/php-code-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModeling\CodeAst\Builder;
12+
13+
use OpenCodeModeling\CodeAst\Code\InterfaceGenerator;
14+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassNamespace;
15+
use OpenCodeModeling\CodeAst\NodeVisitor\InterfaceExtends;
16+
use OpenCodeModeling\CodeAst\NodeVisitor\InterfaceFile;
17+
use OpenCodeModeling\CodeAst\NodeVisitor\NamespaceUse;
18+
use OpenCodeModeling\CodeAst\NodeVisitor\StrictType;
19+
use PhpParser\Node;
20+
use PhpParser\NodeTraverser;
21+
use PhpParser\NodeVisitor;
22+
23+
final class InterfaceBuilder
24+
{
25+
/** @var string|null */
26+
private $namespace;
27+
28+
/** @var string|null */
29+
private $name;
30+
31+
/** @var bool */
32+
private $strict = false;
33+
34+
/** @var bool */
35+
private $typed = false;
36+
37+
/** @var string[] */
38+
private $extends = [];
39+
40+
/** @var string[] */
41+
private $namespaceUse = [];
42+
43+
/** @var ClassConstBuilder[] */
44+
private $constants = [];
45+
46+
private function __construct()
47+
{
48+
}
49+
50+
public static function fromNodes(Node ...$nodes): self
51+
{
52+
$self = new self();
53+
54+
foreach ($nodes as $node) {
55+
$self->unpackNode($node);
56+
}
57+
58+
return $self;
59+
}
60+
61+
public static function fromScratch(
62+
string $interfaceName,
63+
string $namespace = null,
64+
bool $typed = true,
65+
bool $strict = true
66+
): self {
67+
$self = new self();
68+
$self->name = $interfaceName;
69+
$self->namespace = $namespace;
70+
$self->typed = $typed;
71+
$self->strict = $strict;
72+
73+
return $self;
74+
}
75+
76+
public function injectVisitors(NodeTraverser $nodeTraverser): void
77+
{
78+
foreach ($this->generate() as $visitor) {
79+
$nodeTraverser->addVisitor($visitor);
80+
}
81+
}
82+
83+
public function setExtends(string ...$extends): self
84+
{
85+
$this->extends = $extends;
86+
87+
return $this;
88+
}
89+
90+
public function setNamespaceUse(string ...$namespaces): self
91+
{
92+
$this->namespaceUse = $namespaces;
93+
94+
return $this;
95+
}
96+
97+
public function setConstants(ClassConstBuilder ...$constants): self
98+
{
99+
$this->constants = $constants;
100+
101+
return $this;
102+
}
103+
104+
public function getNamespace(): ?string
105+
{
106+
return $this->namespace;
107+
}
108+
109+
public function getName(): ?string
110+
{
111+
return $this->name;
112+
}
113+
114+
public function isStrict(): bool
115+
{
116+
return $this->strict;
117+
}
118+
119+
public function isTyped(): bool
120+
{
121+
return $this->typed;
122+
}
123+
124+
public function getExtends(): array
125+
{
126+
return $this->extends;
127+
}
128+
129+
/**
130+
* @return string[]
131+
*/
132+
public function getNamespaceUse(): array
133+
{
134+
return $this->namespaceUse;
135+
}
136+
137+
/**
138+
* @return ClassConstBuilder[]
139+
*/
140+
public function getConstants(): array
141+
{
142+
return $this->constants;
143+
}
144+
145+
/**
146+
* @return NodeVisitor[]
147+
*/
148+
public function generate(): array
149+
{
150+
/** @var NodeVisitor[] $visitors */
151+
$visitors = [];
152+
153+
if ($this->strict) {
154+
$visitors[] = new StrictType();
155+
}
156+
157+
if ($this->namespace) {
158+
$visitors[] = new ClassNamespace($this->namespace);
159+
}
160+
if ($this->namespaceUse) {
161+
$visitors[] = new NamespaceUse(...$this->namespaceUse);
162+
}
163+
164+
$visitors[] = new InterfaceFile(new InterfaceGenerator($this->name));
165+
166+
if ($this->extends) {
167+
$visitors[] = new InterfaceExtends(...$this->extends);
168+
}
169+
170+
if (\count($this->constants) > 0) {
171+
\array_push(
172+
$visitors,
173+
...\array_map(
174+
static function (ClassConstBuilder $const) {
175+
return $const->generate();
176+
},
177+
$this->constants
178+
)
179+
);
180+
}
181+
182+
return $visitors;
183+
}
184+
185+
private function unpackNode(Node $node): void
186+
{
187+
switch (true) {
188+
case $node instanceof Node\Stmt\Declare_:
189+
if ($node->declares[0]->key->name === 'strict_types') {
190+
$this->strict = true;
191+
}
192+
break;
193+
case $node instanceof Node\Stmt\Namespace_:
194+
$this->namespace = $node->name->toString();
195+
196+
foreach ($node->stmts as $stmt) {
197+
$this->unpackNode($stmt);
198+
}
199+
break;
200+
case $node instanceof Node\Stmt\Use_:
201+
foreach ($node->uses as $use) {
202+
$this->unpackNode($use);
203+
}
204+
break;
205+
case $node instanceof Node\Stmt\UseUse:
206+
$this->namespaceUse[] = $node->name instanceof Node\Name\FullyQualified
207+
? '\\' . $node->name->toString()
208+
: $node->name->toString();
209+
break;
210+
case $node instanceof Node\Stmt\Interface_:
211+
$this->name = $node->name->name;
212+
213+
$this->extends = \array_map(
214+
static function (Node\Name $name) {
215+
return $name instanceof Node\Name\FullyQualified
216+
? '\\' . $name->toString()
217+
: $name->toString();
218+
},
219+
$node->extends
220+
);
221+
222+
foreach ($node->stmts as $stmt) {
223+
$this->unpackNode($stmt);
224+
}
225+
break;
226+
case $node instanceof Node\Stmt\ClassConst:
227+
$this->constants[] = ClassConstBuilder::fromNode($node);
228+
break;
229+
default:
230+
break;
231+
}
232+
}
233+
}

src/NodeVisitor/ClassConstant.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function afterTraverse(array $nodes): ?array
5151

5252
if ($node instanceof Namespace_) {
5353
foreach ($node->stmts as $stmt) {
54-
if ($stmt instanceof Class_) {
54+
if ($stmt instanceof Class_ || $stmt instanceof Node\Stmt\Interface_) {
5555
if ($this->checkConstantExists($stmt)) {
5656
return null;
5757
}
@@ -61,7 +61,7 @@ public function afterTraverse(array $nodes): ?array
6161
);
6262
}
6363
}
64-
} elseif ($node instanceof Class_) {
64+
} elseif ($node instanceof Class_ || $node instanceof Node\Stmt\Interface_) {
6565
if ($this->checkConstantExists($node)) {
6666
return null;
6767
}
@@ -75,7 +75,7 @@ public function afterTraverse(array $nodes): ?array
7575
return $newNodes;
7676
}
7777

78-
private function checkConstantExists(Class_ $node): bool
78+
private function checkConstantExists($node): bool
7979
{
8080
foreach ($node->stmts as $stmt) {
8181
if ($stmt instanceof Node\Stmt\ClassConst
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenCodeModelingTest\CodeAst\Builder;
6+
7+
use OpenCodeModeling\CodeAst\Builder\ClassConstBuilder;
8+
use OpenCodeModeling\CodeAst\Builder\InterfaceBuilder;
9+
use PhpParser\NodeTraverser;
10+
use PhpParser\Parser;
11+
use PhpParser\ParserFactory;
12+
use PhpParser\PrettyPrinter\Standard;
13+
use PHPUnit\Framework\TestCase;
14+
15+
final class InterfaceBuilderTest extends TestCase
16+
{
17+
/**
18+
* @var Parser
19+
*/
20+
private $parser;
21+
22+
/**
23+
* @var Standard
24+
*/
25+
private $printer;
26+
27+
public function setUp(): void
28+
{
29+
$this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
30+
$this->printer = new Standard(['shortArraySyntax' => true]);
31+
}
32+
33+
/**
34+
* @test
35+
*/
36+
public function it_generates_interface_for_empty_file(): void
37+
{
38+
$ast = $this->parser->parse('');
39+
40+
$interfaceFactory = InterfaceBuilder::fromScratch('TestInterface', 'My\\Awesome\\Service');
41+
$interfaceFactory
42+
->setExtends('BaseClass')
43+
->setNamespaceUse('Foo\\Bar')
44+
->setConstants(ClassConstBuilder::fromScratch('PRIV', 'private')->setPrivate());
45+
46+
$nodeTraverser = new NodeTraverser();
47+
$interfaceFactory->injectVisitors($nodeTraverser);
48+
49+
$expected = <<<'EOF'
50+
<?php
51+
52+
declare (strict_types=1);
53+
namespace My\Awesome\Service;
54+
55+
use Foo\Bar;
56+
interface TestInterface extends BaseClass
57+
{
58+
private const PRIV = 'private';
59+
}
60+
EOF;
61+
62+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
63+
}
64+
65+
/**
66+
* @test
67+
*/
68+
public function it_generates_interface_for_empty_file_from_template(): void
69+
{
70+
$expected = <<<'EOF'
71+
<?php
72+
73+
declare (strict_types=1);
74+
namespace My\Awesome\Service;
75+
76+
use Foo\Bar;
77+
interface TestInterface extends BaseClass
78+
{
79+
const FIRST = 1;
80+
private const PRIV = 'private';
81+
protected const PROT = 'protected';
82+
public const PUB = 'public';
83+
}
84+
EOF;
85+
86+
$ast = $this->parser->parse($expected);
87+
88+
$interfaceFactory = InterfaceBuilder::fromNodes(...$ast);
89+
90+
$this->assertSame('TestInterface', $interfaceFactory->getName());
91+
$this->assertSame(['BaseClass'], $interfaceFactory->getExtends());
92+
$this->assertTrue($interfaceFactory->isStrict());
93+
94+
$nodeTraverser = new NodeTraverser();
95+
$interfaceFactory->injectVisitors($nodeTraverser);
96+
97+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($this->parser->parse(''))));
98+
}
99+
}

0 commit comments

Comments
 (0)