Skip to content

Commit 965496b

Browse files
committed
added option to load methods a functions with bodies [Closes #59][Closes #4]
1 parent 5eae391 commit 965496b

File tree

9 files changed

+326
-3
lines changed

9 files changed

+326
-3
lines changed

composer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
},
2222
"require-dev": {
2323
"nette/tester": "^2.0",
24+
"nikic/php-parser": "^4.4",
2425
"tracy/tracy": "^2.3",
2526
"phpstan/phpstan": "^0.12"
2627
},
28+
"suggest": {
29+
"nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()"
30+
},
2731
"autoload": {
2832
"classmap": ["src/"]
2933
},

readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,16 @@ $closure = Nette\PhpGenerator\Closure::from(
553553
);
554554
```
555555

556+
Method bodies are empty by default. If you want to load them as well, use this way
557+
(it requires `nikic/php-parser` to be installed):
558+
559+
```php
560+
$class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class);
561+
562+
$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump');
563+
```
564+
565+
556566
Variables dumper
557567
----------------
558568

src/PhpGenerator/ClassType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ public static function from($class): self
7676
}
7777

7878

79+
/**
80+
* @param string|object $class
81+
*/
82+
public static function withBodiesFrom($class): self
83+
{
84+
return (new Factory)->fromClassReflection(new \ReflectionClass($class), true);
85+
}
86+
87+
7988
public function __construct(string $name = null, PhpNamespace $namespace = null)
8089
{
8190
$this->setName($name);

src/PhpGenerator/Factory.php

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
namespace Nette\PhpGenerator;
1111

1212
use Nette;
13+
use PhpParser;
14+
use PhpParser\Node;
15+
use PhpParser\ParserFactory;
1316

1417

1518
/**
@@ -19,7 +22,7 @@ final class Factory
1922
{
2023
use Nette\SmartObject;
2124

22-
public function fromClassReflection(\ReflectionClass $from): ClassType
25+
public function fromClassReflection(\ReflectionClass $from, bool $withBodies = false): ClassType
2326
{
2427
$class = $from->isAnonymous()
2528
? new ClassType
@@ -48,9 +51,14 @@ public function fromClassReflection(\ReflectionClass $from): ClassType
4851
}
4952
}
5053
$class->setProperties($props);
54+
55+
$bodies = $withBodies ? $this->loadMethodBodies($from) : [];
5156
foreach ($from->getMethods() as $method) {
5257
if ($method->getDeclaringClass()->name === $from->name) {
53-
$methods[] = $this->fromMethodReflection($method);
58+
$methods[] = $m = $this->fromMethodReflection($method);
59+
if (isset($bodies[$method->name])) {
60+
$m->setBody($bodies[$method->name]);
61+
}
5462
}
5563
}
5664
$class->setMethods($methods);
@@ -91,7 +99,7 @@ public function fromMethodReflection(\ReflectionMethod $from): Method
9199

92100

93101
/** @return GlobalFunction|Closure */
94-
public function fromFunctionReflection(\ReflectionFunction $from)
102+
public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false)
95103
{
96104
$function = $from->isClosure() ? new Closure : new GlobalFunction($from->name);
97105
$function->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters()));
@@ -104,6 +112,7 @@ public function fromFunctionReflection(\ReflectionFunction $from)
104112
$function->setReturnType($from->getReturnType()->getName());
105113
$function->setReturnNullable($from->getReturnType()->allowsNull());
106114
}
115+
$function->setBody($withBody ? $this->loadFunctionBody($from) : '');
107116
return $function;
108117
}
109118

@@ -165,4 +174,70 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
165174
$prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
166175
return $prop;
167176
}
177+
178+
179+
private function loadMethodBodies(\ReflectionClass $from): array
180+
{
181+
if ($from->isAnonymous()) {
182+
throw new Nette\NotSupportedException('Anonymous classes are not supported.');
183+
}
184+
185+
[$code, $stmts] = $this->parse($from);
186+
$nodeFinder = new PhpParser\NodeFinder;
187+
$class = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) {
188+
return ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_) && $node->namespacedName->toString() === $from->name;
189+
});
190+
191+
$bodies = [];
192+
foreach ($nodeFinder->findInstanceOf($class, Node\Stmt\ClassMethod::class) as $method) {
193+
/** @var Node\Stmt\ClassMethod $method */
194+
if ($method->stmts) {
195+
$start = $method->stmts[0]->getAttribute('startFilePos');
196+
$body = substr($code, $start, end($method->stmts)->getAttribute('endFilePos') - $start + 1);
197+
$bodies[$method->name->toString()] = Helpers::indentPhp($body, -2);
198+
}
199+
}
200+
return $bodies;
201+
}
202+
203+
204+
private function loadFunctionBody(\ReflectionFunction $from): string
205+
{
206+
if ($from->isClosure()) {
207+
throw new Nette\NotSupportedException('Closures are not supported.');
208+
}
209+
210+
[$code, $stmts] = $this->parse($from);
211+
/** @var Node\Stmt\Function_ $function */
212+
$function = (new PhpParser\NodeFinder)->findFirst($stmts, function (Node $node) use ($from) {
213+
return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $from->name;
214+
});
215+
216+
$start = $function->stmts[0]->getAttribute('startFilePos');
217+
$body = substr($code, $start, end($function->stmts)->getAttribute('endFilePos') - $start + 1);
218+
return Helpers::indentPhp($body, -1);
219+
}
220+
221+
222+
private function parse($from): array
223+
{
224+
$file = $from->getFileName();
225+
if (!class_exists(ParserFactory::class)) {
226+
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser'.");
227+
} elseif (!$file) {
228+
throw new Nette\InvalidStateException("Source code of $from->name not found.");
229+
}
230+
231+
$lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]);
232+
$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
233+
$code = file_get_contents($file);
234+
$code = str_replace("\r\n", "\n", $code);
235+
$stmts = $parser->parse($code);
236+
237+
$traverser = new PhpParser\NodeTraverser;
238+
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
239+
$stmts = $traverser->traverse($stmts);
240+
241+
return [$code, $stmts];
242+
}
168243
}

src/PhpGenerator/GlobalFunction.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public static function from(string $function): self
3030
}
3131

3232

33+
public static function withBodyFrom(string $function): self
34+
{
35+
return (new Factory)->fromFunctionReflection(new \ReflectionFunction($function), true);
36+
}
37+
38+
3339
public function __toString(): string
3440
{
3541
try {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\PhpGenerator\ClassType;
6+
use Tester\Assert;
7+
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
require __DIR__ . '/fixtures/class-body.phpf';
11+
12+
13+
Assert::exception(function () {
14+
ClassType::withBodiesFrom(PDO::class);
15+
}, Nette\InvalidStateException::class, 'Source code of PDO not found.');
16+
17+
18+
Assert::exception(function () {
19+
ClassType::withBodiesFrom(new class {
20+
});
21+
}, Nette\NotSupportedException::class, 'Anonymous classes are not supported.');
22+
23+
24+
$res = ClassType::withBodiesFrom(Abc\Class7::class);
25+
sameFile(__DIR__ . '/expected/ClassType.from.bodies.expect', (string) $res);

tests/PhpGenerator/GlobalFunction.phpt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ require __DIR__ . '/../bootstrap.php';
1111
/** global */
1212
function func(stdClass $a, $b = null)
1313
{
14+
echo 'hello';
15+
return 1;
1416
}
1517

1618

@@ -23,3 +25,16 @@ function func(stdClass $a, $b = null)
2325
{
2426
}
2527
', (string) $function);
28+
29+
30+
$function = GlobalFunction::withBodyFrom('func');
31+
same(
32+
'/**
33+
* global
34+
*/
35+
function func(stdClass $a, $b = null)
36+
{
37+
echo \'hello\';
38+
return 1;
39+
}
40+
', (string) $function);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
abstract class Class7
2+
{
3+
abstract public function abstractFun();
4+
5+
6+
public function emptyFun()
7+
{
8+
}
9+
10+
11+
public function emptyFun2()
12+
{
13+
}
14+
15+
16+
public function simple()
17+
{
18+
return 1;
19+
}
20+
21+
22+
public function simple2()
23+
{
24+
return 1;
25+
}
26+
27+
28+
public function long()
29+
{
30+
if ($member instanceof Method) {
31+
$s = [1, 2, 3];
32+
}
33+
/*
34+
$this->methods[$member->getName()] = $member;
35+
*/
36+
throw new Nette\InvalidArgumentException('Argument must be Method|Property|Constant.');
37+
}
38+
39+
40+
public function complex()
41+
{
42+
echo 1;
43+
// single line comment
44+
45+
// spaces - indent
46+
// spaces - indent
47+
48+
/* multi
49+
line
50+
comment */
51+
if (
52+
$a
53+
&& $b + $c)
54+
{}
55+
56+
/** multi
57+
line
58+
comment */
59+
60+
if ($member instanceof Method) {
61+
$s1 = '
62+
a
63+
b
64+
c
65+
';
66+
$s2 = "
67+
a
68+
{$b}
69+
$c
70+
";
71+
72+
$s3 = <<<DOC
73+
a
74+
{$b}
75+
$c
76+
DOC
77+
;
78+
$s3 = <<<'DOC'
79+
a
80+
b
81+
c
82+
DOC
83+
;
84+
?>
85+
a
86+
b
87+
c
88+
<?php
89+
}
90+
throw new Nette\InvalidArgumentException();
91+
}
92+
}

0 commit comments

Comments
 (0)