Skip to content

Commit

Permalink
added option to load methods with bodies [Closes #59][Closes #4]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed May 6, 2020
1 parent dadfc70 commit f6db704
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 2 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"require-dev": {
"nette/tester": "^2.0",
"nikic/php-parser": "^4.4",
"tracy/tracy": "^2.3",
"phpstan/phpstan": "^0.12"
},
Expand Down
8 changes: 8 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,14 @@ $closure = Nette\PhpGenerator\Closure::from(
);
```

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

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


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
50 changes: 48 additions & 2 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 @@ -145,4 +153,42 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
$prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
return $prop;
}


private function loadMethodBodies(\ReflectionClass $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 ($from->isAnonymous()) {
throw new Nette\NotSupportedException('Anonymous classes are not supported.');
} elseif (!$file) {
throw new Nette\InvalidStateException("Source code of class $from->name not found.");
}

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

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

$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::unindent($body, 2);
}
}

return $bodies;
}
}
6 changes: 6 additions & 0 deletions src/PhpGenerator/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public static function unformatDocComment(string $comment): string
}


public static function unindent(string $s, int $level = 1): string
{
return preg_replace('#^(\t| ){' . $level . '}#m', '', $s);
}


public static function isIdentifier($value): bool
{
return is_string($value) && preg_match('#^' . self::PHP_IDENT . '$#D', $value);
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/classes.php';


Assert::exception(function () {
ClassType::withBodiesFrom(PDO::class);
}, Nette\InvalidStateException::class, 'Source code of class 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);
32 changes: 32 additions & 0 deletions tests/PhpGenerator/expected/ClassType.from.bodies.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
abstract class Class7
{
abstract public function abstractFun();


public function emptyFun()
{
}


public function emptyFun2()
{
}


public function simple()
{
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.');
}
}
28 changes: 28 additions & 0 deletions tests/PhpGenerator/fixtures/classes.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,31 @@ class Class6
private const THE_PRIVATE_CONSTANT = 9;
public const THE_PUBLIC_CONSTANT = 9;
}


abstract class Class7
{
abstract function abstractFun();

function emptyFun() {}

function emptyFun2() {
}

function simple()
{
return 1;
}

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

0 comments on commit f6db704

Please sign in to comment.