Skip to content

Commit 866f54e

Browse files
committed
Add a high level API for classes for easier code generation
1 parent b577efd commit 866f54e

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed

README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,84 @@ PHP code generation based on AST.
77
```bash
88
$ composer require open-code-modeling/php-code-ast --dev
99
```
10+
11+
## Usage
12+
13+
Let's start with a straightforward example of generating a class with the `ClassFactory`:
14+
15+
```php
16+
<?php
17+
18+
$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::ONLY_PHP7);
19+
$printer = new PhpParser\PrettyPrinter\Standard(['shortArraySyntax' => true]);
20+
21+
$ast = $parser->parse('');
22+
23+
$classFactory = OpenCodeModeling\CodeAst\Factory\ClassFactory::fromScratch('TestClass', 'My\\Awesome\\Service');
24+
$classFactory
25+
->setFinal(true)
26+
->setExtends('BaseClass')
27+
->setNamespaceUse('Foo\\Bar')
28+
->setImplements('\\Iterator', 'Bar');
29+
30+
$nodeTraverser = new PhpParser\NodeTraverser();
31+
32+
$classFactory->injectVisitors($nodeTraverser);
33+
34+
print_r($printer->prettyPrintFile($nodeTraverser->traverse($ast)));
35+
```
36+
37+
Will print the following output:
38+
39+
```php
40+
<?php
41+
42+
declare (strict_types=1);
43+
namespace My\Awesome\Service;
44+
45+
use Foo\Bar;
46+
final class TestClass extends BaseClass implements \Iterator, Bar
47+
{
48+
}
49+
```
50+
51+
All this is done via PHP AST and it checks if the AST token is already defined. So it will not override your code.
52+
53+
You can also use the code and PHP AST visitor classes to generate code via the low level API.
54+
To add a method `toInt()` to the class above you can add this via the `ClassMethod` visitor.
55+
56+
```php
57+
<?php
58+
59+
$method = new OpenCodeModeling\CodeAst\Code\MethodGenerator(
60+
'toInt',
61+
[],
62+
OpenCodeModeling\CodeAst\Code\MethodGenerator::FLAG_PUBLIC,
63+
new OpenCodeModeling\CodeAst\Code\BodyGenerator($this->parser, 'return $this->myValue;')
64+
);
65+
$method->setReturnType('int');
66+
67+
$nodeTraverser->addVisitor(new OpenCodeModeling\CodeAst\NodeVisitor\ClassMethod($method));
68+
69+
print_r($printer->prettyPrintFile($nodeTraverser->traverse($ast)));
70+
```
71+
72+
This will print the following output.
73+
74+
```php
75+
<?php
76+
77+
declare (strict_types=1);
78+
namespace My\Awesome\Service;
79+
80+
use Foo\Bar;
81+
final class TestClass extends BaseClass implements \Iterator, Bar
82+
{
83+
public function toInt() : int
84+
{
85+
return $this->myValue;
86+
}
87+
}
88+
```
89+
90+
Now, change the body of the `toInt()` method to something else. You will see that your changes will *NOT* be overwritten.

src/Factory/ClassFactory.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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\Factory;
12+
13+
use OpenCodeModeling\CodeAst\Code\ClassGenerator;
14+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassExtends;
15+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassFile;
16+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassImplements;
17+
use OpenCodeModeling\CodeAst\NodeVisitor\ClassNamespace;
18+
use OpenCodeModeling\CodeAst\NodeVisitor\NamespaceUse;
19+
use OpenCodeModeling\CodeAst\NodeVisitor\StrictType;
20+
use PhpParser\NodeTraverser;
21+
use PhpParser\NodeVisitor;
22+
23+
final class ClassFactory
24+
{
25+
/** @var string|null */
26+
private $namespace;
27+
28+
/** @var string|null */
29+
private $name;
30+
31+
/** @var bool */
32+
private $strict;
33+
34+
/** @var bool */
35+
private $typed;
36+
37+
/** @var bool */
38+
private $final = false;
39+
40+
/** @var bool */
41+
private $abstract = false;
42+
43+
/** @var string */
44+
private $extends;
45+
46+
/** @var string[] */
47+
private $implements = [];
48+
49+
/** @var string[] */
50+
private $namespaceUse = [];
51+
52+
private function __construct()
53+
{
54+
}
55+
56+
public static function fromScratch(
57+
string $className,
58+
string $namespace = null,
59+
bool $typed = true,
60+
bool $strict = true
61+
): self {
62+
$self = new self();
63+
$self->name = $className;
64+
$self->namespace = $namespace;
65+
$self->typed = $typed;
66+
$self->strict = $strict;
67+
68+
return $self;
69+
}
70+
71+
public function injectVisitors(NodeTraverser $nodeTraverser): void
72+
{
73+
foreach ($this->generate() as $visitor) {
74+
$nodeTraverser->addVisitor($visitor);
75+
}
76+
}
77+
78+
public function setFinal(bool $final): self
79+
{
80+
$this->final = $final;
81+
82+
return $this;
83+
}
84+
85+
public function setAbstract(bool $abstract): self
86+
{
87+
$this->abstract = $abstract;
88+
89+
return $this;
90+
}
91+
92+
public function setExtends(string $extends): self
93+
{
94+
$this->extends = $extends;
95+
96+
return $this;
97+
}
98+
99+
public function setImplements(string ...$interfaces): self
100+
{
101+
$this->implements = $interfaces;
102+
103+
return $this;
104+
}
105+
106+
public function setNamespaceUse(string ...$namespaces): self
107+
{
108+
$this->namespaceUse = $namespaces;
109+
110+
return $this;
111+
}
112+
113+
/**
114+
* @return NodeVisitor[]
115+
*/
116+
public function generate(): array
117+
{
118+
/** @var NodeVisitor[] $visitors */
119+
$visitors = [];
120+
121+
if ($this->strict) {
122+
$visitors[] = new StrictType();
123+
}
124+
125+
if ($this->namespace) {
126+
$visitors[] = new ClassNamespace($this->namespace);
127+
}
128+
if ($this->namespaceUse) {
129+
$visitors[] = new NamespaceUse(...$this->namespaceUse);
130+
}
131+
132+
$visitors[] = new ClassFile($this->classGenerator());
133+
134+
if ($this->extends) {
135+
$visitors[] = new ClassExtends($this->extends);
136+
}
137+
if ($this->implements) {
138+
$visitors[] = new ClassImplements(...$this->implements);
139+
}
140+
141+
return $visitors;
142+
}
143+
144+
private function classGenerator(): ClassGenerator
145+
{
146+
$flags = 0;
147+
148+
if ($this->final) {
149+
$flags |= ClassGenerator::FLAG_FINAL;
150+
}
151+
if ($this->abstract) {
152+
$flags |= ClassGenerator::FLAG_ABSTRACT;
153+
}
154+
155+
return new ClassGenerator($this->name, $flags);
156+
}
157+
}

tests/Factory/ClassFactoryTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenCodeModelingTest\CodeAst\Factory;
6+
7+
use OpenCodeModeling\CodeAst\Factory\ClassFactory;
8+
use PhpParser\NodeTraverser;
9+
use PhpParser\Parser;
10+
use PhpParser\ParserFactory;
11+
use PhpParser\PrettyPrinter\Standard;
12+
use PHPUnit\Framework\TestCase;
13+
14+
final class ClassFactoryTest extends TestCase
15+
{
16+
/**
17+
* @var Parser
18+
*/
19+
private $parser;
20+
21+
/**
22+
* @var Standard
23+
*/
24+
private $printer;
25+
26+
public function setUp(): void
27+
{
28+
$this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
29+
$this->printer = new Standard(['shortArraySyntax' => true]);
30+
}
31+
32+
/**
33+
* @test
34+
*/
35+
public function it_generates_class_for_empty_file(): void
36+
{
37+
$ast = $this->parser->parse('');
38+
39+
$classFactory = ClassFactory::fromScratch('TestClass', 'My\\Awesome\\Service');
40+
$classFactory
41+
->setFinal(true)
42+
->setExtends('BaseClass')
43+
->setNamespaceUse('Foo\\Bar')
44+
->setImplements('\\Iterator', 'Bar');
45+
46+
$nodeTraverser = new NodeTraverser();
47+
$classFactory->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+
final class TestClass extends BaseClass implements \Iterator, Bar
57+
{
58+
}
59+
EOF;
60+
61+
$this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast)));
62+
}
63+
}

0 commit comments

Comments
 (0)