Skip to content

Commit

Permalink
added option to load methods a functions with bodies [Closes #59][Closes
Browse files Browse the repository at this point in the history
 #4]
  • Loading branch information
dg committed May 6, 2020
1 parent a5ddce8 commit 40d6435
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 3 deletions.
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
},
"require-dev": {
"nette/tester": "^2.0",
"nikic/php-parser": "^4.4",
"tracy/tracy": "^2.3",
"phpstan/phpstan": "^0.12"
},
"suggest": {
"nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()"
},
"autoload": {
"classmap": ["src/"]
},
Expand Down
10 changes: 10 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,16 @@ $closure = Nette\PhpGenerator\Closure::from(
);
```

Method bodies are empty by default. If you want to load them as well, use this way
(it requires `nikic/php-parser` to be installed):

```php
$class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class);

$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump');
```


Variables dumper
----------------

Expand Down
9 changes: 9 additions & 0 deletions src/PhpGenerator/ClassType.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ public static function from($class): self
}


/**
* @param string|object $class
*/
public static function withBodiesFrom($class): self
{
return (new Factory)->fromClassReflection(new \ReflectionClass($class), true);
}


public function __construct(string $name = null, PhpNamespace $namespace = null)
{
$this->setName($name);
Expand Down
79 changes: 76 additions & 3 deletions src/PhpGenerator/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
namespace Nette\PhpGenerator;

use Nette;
use PhpParser;
use PhpParser\Node;
use PhpParser\ParserFactory;


/**
Expand All @@ -19,7 +22,7 @@ final class Factory
{
use Nette\SmartObject;

public function fromClassReflection(\ReflectionClass $from): ClassType
public function fromClassReflection(\ReflectionClass $from, bool $withBodies = false): ClassType
{
$class = $from->isAnonymous()
? new ClassType
Expand Down Expand Up @@ -48,9 +51,14 @@ public function fromClassReflection(\ReflectionClass $from): ClassType
}
}
$class->setProperties($props);

$bodies = $withBodies ? $this->loadMethodBodies($from) : [];
foreach ($from->getMethods() as $method) {
if ($method->getDeclaringClass()->name === $from->name) {
$methods[] = $this->fromMethodReflection($method);
$methods[] = $m = $this->fromMethodReflection($method);
if (isset($bodies[$method->name])) {
$m->setBody($bodies[$method->name]);
}
}
}
$class->setMethods($methods);
Expand Down Expand Up @@ -84,7 +92,7 @@ public function fromMethodReflection(\ReflectionMethod $from): Method


/** @return GlobalFunction|Closure */
public function fromFunctionReflection(\ReflectionFunction $from)
public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false)
{
$function = $from->isClosure() ? new Closure : new GlobalFunction($from->name);
$function->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters()));
Expand All @@ -97,6 +105,7 @@ public function fromFunctionReflection(\ReflectionFunction $from)
$function->setReturnType($from->getReturnType()->getName());
$function->setReturnNullable($from->getReturnType()->allowsNull());
}
$function->setBody($withBody ? $this->loadFunctionBody($from) : '');
return $function;
}

Expand Down Expand Up @@ -145,4 +154,68 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
$prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
return $prop;
}


private function loadMethodBodies(\ReflectionClass $from): array
{
if ($from->isAnonymous()) {
throw new Nette\NotSupportedException('Anonymous classes are not supported.');
}

[$code, $stmts] = $this->parse($from);
$nodeFinder = new PhpParser\NodeFinder;
$class = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) {
return $node instanceof Node\Stmt\Class_ && $node->namespacedName->toString() === $from->name;
});

$bodies = [];
foreach ($nodeFinder->findInstanceOf($class, Node\Stmt\ClassMethod::class) as $method) {
if ($method->stmts) {
$start = $method->stmts[0]->getAttribute('startFilePos');
$body = substr($code, $start, end($method->stmts)->getAttribute('endFilePos') - $start + 1);
$bodies[$method->name->toString()] = Helpers::indentPhp($body, -2);
}
}
return $bodies;
}


private function loadFunctionBody(\ReflectionFunction $from): string
{
if ($from->isClosure()) {
throw new Nette\NotSupportedException('Closures are not supported.');
}

[$code, $stmts] = $this->parse($from);
$function = (new PhpParser\NodeFinder)->findFirst($stmts, function (Node $node) use ($from) {
return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $from->name;
});

$start = $function->stmts[0]->getAttribute('startFilePos');
$body = substr($code, $start, end($function->stmts)->getAttribute('endFilePos') - $start + 1);
return Helpers::indentPhp($body, -1);
}


private function parse($from): array
{
$file = $from->getFileName();
if (!class_exists(ParserFactory::class)) {
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser'.");
} elseif (!$file) {
throw new Nette\InvalidStateException("Source code of $from->name not found.");
}

$lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]);
$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
$code = file_get_contents($file);
$code = str_replace("\r\n", "\n", $code);
$stmts = $parser->parse($code);

$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
$stmts = $traverser->traverse($stmts);

return [$code, $stmts];
}
}
6 changes: 6 additions & 0 deletions src/PhpGenerator/GlobalFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public static function from(string $function): self
}


public static function withBodyFrom(string $function): self
{
return (new Factory)->fromFunctionReflection(new \ReflectionFunction($function), true);
}


public function __toString(): string
{
try {
Expand Down
25 changes: 25 additions & 0 deletions tests/PhpGenerator/ClassType.from.bodies.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use Nette\PhpGenerator\ClassType;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';
require __DIR__ . '/fixtures/class-body.php';


Assert::exception(function () {
ClassType::withBodiesFrom(PDO::class);
}, Nette\InvalidStateException::class, 'Source code of PDO not found.');


Assert::exception(function () {
ClassType::withBodiesFrom(new class {
});
}, Nette\NotSupportedException::class, 'Anonymous classes are not supported.');


$res = ClassType::withBodiesFrom(Abc\Class7::class);
sameFile(__DIR__ . '/expected/ClassType.from.bodies.expect', (string) $res);
15 changes: 15 additions & 0 deletions tests/PhpGenerator/GlobalFunction.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ require __DIR__ . '/../bootstrap.php';
/** global */
function func(stdClass $a, $b = null)
{
echo 'hello';
return 1;
}


Expand All @@ -23,3 +25,16 @@ function func(stdClass $a, $b = null)
{
}
', (string) $function);


$function = GlobalFunction::withBodyFrom('func');
same(
'/**
* global
*/
function func(stdClass $a, $b = null)
{
echo \'hello\';
return 1;
}
', (string) $function);
92 changes: 92 additions & 0 deletions tests/PhpGenerator/expected/ClassType.from.bodies.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
abstract class Class7
{
abstract public function abstractFun();


public function emptyFun()
{
}


public function emptyFun2()
{
}


public function simple()
{
return 1;
}


public function simple2()
{
return 1;
}


public function long()
{
if ($member instanceof Method) {
$s = [1, 2, 3];
}
/*
$this->methods[$member->getName()] = $member;
*/
throw new Nette\InvalidArgumentException('Argument must be Method|Property|Constant.');
}


public function complex()
{
echo 1;
// single line comment

// spaces - indent
// spaces - indent

/* multi
line
comment */
if (
$a
&& $b + $c)
{}

/** multi
line
comment */

if ($member instanceof Method) {
$s1 = '
a
b
c
';
$s2 = "
a
{$b}
$c
";

$s3 = <<<DOC
a
{$b}
$c
DOC
;
$s3 = <<<'DOC'
a
b
c
DOC
;
?>
a
b
c
<?php
}
throw new Nette\InvalidArgumentException();
}
}
Loading

0 comments on commit 40d6435

Please sign in to comment.