Skip to content

Commit

Permalink
added ClassType::fromCode() & PhpFile::fromCode() [Closes #79]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Sep 18, 2021
1 parent 33946bc commit bc679a7
Show file tree
Hide file tree
Showing 16 changed files with 827 additions and 4 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"nette/utils": "^3.1.2"
},
"require-dev": {
"nette/tester": "^2.0",
"nikic/php-parser": "^4.4",
"nette/tester": "^2.4",
"nikic/php-parser": "^4.11",
"tracy/tracy": "^2.3",
"phpstan/phpstan": "^0.12"
},
Expand Down
24 changes: 24 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,30 @@ $class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class);
$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump');
```

Load class from file
--------------------

You can also load classes directly from a PHP file that is not already loaded or string of PHP code:

```php
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
<?php

class Demo
{
public $foo;
}
XX);
```

Loading the entire PHP file, which may contain multiple classes or even multiple namespaces:

```php
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
```

This requires `nikic/php-parser` to be installed.


Variables Dumper
----------------
Expand Down
6 changes: 6 additions & 0 deletions src/PhpGenerator/ClassType.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ public static function withBodiesFrom($class): self
}


public static function fromCode(string $code): self
{
return (new Factory)->fromClassCode($code);
}


public function __construct(string $name = null, PhpNamespace $namespace = null)
{
$this->setName($name);
Expand Down
246 changes: 244 additions & 2 deletions src/PhpGenerator/Extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,23 @@ final class Extractor

private $code;
private $statements;
private $printer;


public function __construct(string $code)
{
if (!class_exists(ParserFactory::class)) {
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser'.");
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser' 4.7 or newer.");
}
$this->printer = new PhpParser\PrettyPrinter\Standard;
$this->parseCode($code);
}


private function parseCode(string $code): void
{
$this->code = str_replace("\r\n", "\n", $code);
$lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]);
$lexer = new PhpParser\Lexer\Emulative(['usedAttributes' => ['startFilePos', 'endFilePos', 'comments']]);
$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
$stmts = $parser->parse($this->code);

Expand Down Expand Up @@ -167,6 +169,246 @@ private function performReplacements(string $s, array $replacements): string
}


public function extractAll(): PhpFile
{
$phpFile = new PhpFile;
$namespace = '';
$visitor = new class extends PhpParser\NodeVisitorAbstract {
public function enterNode(Node $node)
{
return ($this->callback)($node);
}
};

$visitor->callback = function (Node $node) use (&$class, &$namespace, $phpFile) {
if ($node instanceof Node\Stmt\Namespace_) {
$namespace = $node->name ? $node->name->toString() : '';
} elseif ($node instanceof Node\Stmt\Use_) {
$this->addUseToNamespace($node, $phpFile->addNamespace($namespace));
} elseif ($node instanceof Node\Stmt\Class_) {
if (!$node->name) {
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
$class = $this->addClassToFile($phpFile, $node);
} elseif ($node instanceof Node\Stmt\Interface_) {
$class = $this->addInterfaceToFile($phpFile, $node);
} elseif ($node instanceof Node\Stmt\Trait_) {
$class = $this->addTraitToFile($phpFile, $node);
} elseif ($node instanceof Node\Stmt\Enum_) {
$class = $this->addEnumToFile($phpFile, $node);
} elseif ($node instanceof Node\Stmt\Function_) {
$this->addFunctionToFile($phpFile, $node);
} elseif ($node instanceof Node\Stmt\TraitUse) {
$this->addTraitToClass($class, $node);
} elseif ($node instanceof Node\Stmt\Property) {
$this->addPropertyToClass($class, $node);
} elseif ($node instanceof Node\Stmt\ClassMethod) {
$this->addMethodToClass($class, $node);
} elseif ($node instanceof Node\Stmt\ClassConst) {
$this->addConstantToClass($class, $node);
} elseif ($node instanceof Node\Stmt\EnumCase) {
$this->addEnumCaseToClass($class, $node);
}
if ($node instanceof Node\FunctionLike) {
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
};

$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor($visitor);
$traverser->traverse($this->statements);
return $phpFile;
}


private function addUseToNamespace(Node\Stmt\Use_ $node, PhpNamespace $namespace): void
{
if ($node->type === $node::TYPE_NORMAL) {
foreach ($node->uses as $use) {
$namespace->addUse($use->name->toString(), $use->alias ? $use->alias->toString() : null);
}
}
}


private function addClassToFile(PhpFile $phpFile, Node\Stmt\Class_ $node): ClassType
{
$class = $phpFile->addClass($node->namespacedName->toString());
if ($node->extends) {
$class->setExtends($node->extends->toString());
}
foreach ($node->implements as $item) {
$class->addImplement($item->toString());
}
$class->setFinal($node->isFinal());
$class->setAbstract($node->isAbstract());
$this->addCommentAndAttributes($class, $node);
return $class;
}


private function addInterfaceToFile(PhpFile $phpFile, Node\Stmt\Interface_ $node): ClassType
{
$class = $phpFile->addInterface($node->namespacedName->toString());
foreach ($node->extends as $item) {
$class->addExtend($item->toString());
}
$this->addCommentAndAttributes($class, $node);
return $class;
}


private function addTraitToFile(PhpFile $phpFile, Node\Stmt\Trait_ $node): ClassType
{
$class = $phpFile->addTrait($node->namespacedName->toString());
$this->addCommentAndAttributes($class, $node);
return $class;
}


private function addEnumToFile(PhpFile $phpFile, Node\Stmt\Enum_ $node): ClassType
{
$class = $phpFile->addEnum($node->namespacedName->toString());
foreach ($node->implements as $item) {
$class->addImplement($item->toString());
}
$this->addCommentAndAttributes($class, $node);
return $class;
}


private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void
{
$function = $phpFile->addFunction($node->namespacedName->toString());
$this->setupFunction($function, $node);
}


private function addTraitToClass(ClassType $class, Node\Stmt\TraitUse $node): void
{
$res = [];
foreach ($node->adaptations as $item) {
$res[] = trim($this->toPhp($item), ';');
}
foreach ($node->traits as $trait) {
$class->addTrait($trait->toString(), $res);
$res = [];
}
}


private function addPropertyToClass(ClassType $class, Node\Stmt\Property $node): void
{
foreach ($node->props as $item) {
$prop = $class->addProperty($item->name->toString());
$prop->setStatic($node->isStatic());
if ($node->isPrivate()) {
$prop->setPrivate();
} elseif ($node->isProtected()) {
$prop->setProtected();
}
$prop->setType($node->type ? $this->toPhp($node->type) : null);
if ($item->default) {
$prop->setValue(new Literal($this->toPhp($item->default)));
}
$prop->setReadOnly(method_exists($node, 'isReadonly') && $node->isReadonly());
$this->addCommentAndAttributes($prop, $node);
}
}


private function addMethodToClass(ClassType $class, Node\Stmt\ClassMethod $node): void
{
$method = $class->addMethod($node->name->toString());
$method->setAbstract($node->isAbstract());
$method->setFinal($node->isFinal());
$method->setStatic($node->isStatic());
if ($node->isPrivate()) {
$method->setPrivate();
} elseif ($node->isProtected()) {
$method->setProtected();
}
$this->setupFunction($method, $node);
}


private function addConstantToClass(ClassType $class, Node\Stmt\ClassConst $node): void
{
foreach ($node->consts as $item) {
$const = $class->addConstant($item->name->toString(), new Literal($this->toPhp($item->value)));
if ($node->isPrivate()) {
$const->setPrivate();
} elseif ($node->isProtected()) {
$const->setProtected();
}
$const->setFinal(method_exists($node, 'isFinal') && $node->isFinal());
$this->addCommentAndAttributes($const, $node);
}
}


private function addEnumCaseToClass(ClassType $class, Node\Stmt\EnumCase $node)
{
$case = $class->addCase($node->name->toString(), $node->expr ? $node->expr->value : null);
$this->addCommentAndAttributes($case, $node);
}


private function addCommentAndAttributes($element, Node $node): void
{
if ($node->getDocComment()) {
$comment = $node->getDocComment()->getReformattedText();
$comment = Helpers::unformatDocComment($comment);
$element->setComment($comment);
}

foreach ($node->attrGroups ?? [] as $group) {
foreach ($group->attrs as $attribute) {
$args = [];
foreach ($attribute->args as $arg) {
$value = new Literal($this->toPhp($arg));
if ($arg->name) {
$args[$arg->name->toString()] = $value;
} else {
$args[] = $value;
}
}
$element->addAttribute($attribute->name->toString(), $args);
}
}
}


/**
* @param GlobalFunction|Method $function
*/
private function setupFunction($function, Node\FunctionLike $node): void
{
$function->setReturnReference($node->returnsByRef());
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
foreach ($node->params as $item) {
$param = $function->addParameter($item->var->name);
$param->setType($item->type ? $this->toPhp($item->type) : null);
$param->setReference($item->byRef);
$function->setVariadic($item->variadic);
if ($item->default) {
$param->setDefaultValue(new Literal($this->toPhp($item->default)));
}
$this->addCommentAndAttributes($param, $item);
}
$this->addCommentAndAttributes($function, $node);
if ($node->stmts) {
$function->setBody($this->getReformattedBody($node->stmts, 2));
}
}


private function toPhp($value): string
{
return $this->printer->prettyPrint([$value]);
}


private function getNodeContents(Node ...$nodes): string
{
$start = $nodes[0]->getStartFilePos();
Expand Down
17 changes: 17 additions & 0 deletions src/PhpGenerator/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,23 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
}


public function fromClassCode(string $code): ClassType
{
$classes = $this->fromCode($code)->getClasses();
if (!$classes) {
throw new Nette\InvalidStateException('The code does not contain any class.');
}
return reset($classes);
}


public function fromCode(string $code): PhpFile
{
$reader = new Extractor($code);
return $reader->extractAll();
}


private function getAttributes($from): array
{
if (PHP_VERSION_ID < 80000) {
Expand Down
6 changes: 6 additions & 0 deletions src/PhpGenerator/PhpFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ final class PhpFile
private $strictTypes = false;


public static function fromCode(string $code): self
{
return (new Factory)->fromCode($code);
}


public function addClass(string $name): ClassType
{
return $this
Expand Down
28 changes: 28 additions & 0 deletions tests/PhpGenerator/ClassType.fromCode.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

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

require __DIR__ . '/../bootstrap.php';


$class = ClassType::fromCode(file_get_contents(__DIR__ . '/fixtures/classes.php'));
Assert::type(ClassType::class, $class);
Assert::match(<<<'XX'
/**
* Interface
* @author John Doe
*/
interface Interface1
{
function func1();
}
XX
, (string) $class);


Assert::exception(function () {
ClassType::fromCode('');
}, Nette\InvalidStateException::class, 'The code does not contain any class.');
Loading

0 comments on commit bc679a7

Please sign in to comment.