Skip to content

Commit 74ffdd5

Browse files
committed
added ClassType::fromCode() & PhpFile::fromCode() [Closes #79]
1 parent 0503161 commit 74ffdd5

16 files changed

+849
-4
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"nette/utils": "^3.1.2"
2020
},
2121
"require-dev": {
22-
"nette/tester": "^2.0",
23-
"nikic/php-parser": "^4.4",
22+
"nette/tester": "^2.4",
23+
"nikic/php-parser": "^4.11",
2424
"tracy/tracy": "^2.3",
2525
"phpstan/phpstan": "^0.12"
2626
},

readme.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,30 @@ $class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class);
708708
$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump');
709709
```
710710

711+
Load class from file
712+
--------------------
713+
714+
You can also load classes directly from a PHP file that is not already loaded or string of PHP code:
715+
716+
```php
717+
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
718+
<?php
719+
720+
class Demo
721+
{
722+
public $foo;
723+
}
724+
XX);
725+
```
726+
727+
Loading the entire PHP file, which may contain multiple classes or even multiple namespaces:
728+
729+
```php
730+
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
731+
```
732+
733+
This requires `nikic/php-parser` to be installed.
734+
711735

712736
Variables Dumper
713737
----------------

src/PhpGenerator/ClassType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ public static function withBodiesFrom($class): self
114114
}
115115

116116

117+
public static function fromCode(string $code): self
118+
{
119+
return (new Factory)->fromClassCode($code);
120+
}
121+
122+
117123
public function __construct(string $name = null, PhpNamespace $namespace = null)
118124
{
119125
$this->setName($name);

src/PhpGenerator/Extractor.php

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,26 @@ final class Extractor
2626

2727
private $code;
2828
private $statements;
29+
private $printer;
2930

3031

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

3941

4042
private function parseCode(string $code): void
4143
{
44+
if (substr($code, 0, 5) !== '<?php') {
45+
throw new Nette\InvalidStateException('The input string is not a PHP code.');
46+
}
4247
$this->code = str_replace("\r\n", "\n", $code);
43-
$lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]);
48+
$lexer = new PhpParser\Lexer\Emulative(['usedAttributes' => ['startFilePos', 'endFilePos', 'comments']]);
4449
$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
4550
$stmts = $parser->parse($this->code);
4651

@@ -167,6 +172,248 @@ private function performReplacements(string $s, array $replacements): string
167172
}
168173

169174

175+
public function extractAll(): PhpFile
176+
{
177+
$phpFile = new PhpFile;
178+
$namespace = '';
179+
$visitor = new class extends PhpParser\NodeVisitorAbstract {
180+
public function enterNode(Node $node)
181+
{
182+
return ($this->callback)($node);
183+
}
184+
};
185+
186+
$visitor->callback = function (Node $node) use (&$class, &$namespace, $phpFile) {
187+
if ($node instanceof Node\Stmt\DeclareDeclare && $node->key->name === 'strict_types') {
188+
$phpFile->setStrictTypes((bool) $node->value->value);
189+
} elseif ($node instanceof Node\Stmt\Namespace_) {
190+
$namespace = $node->name ? $node->name->toString() : '';
191+
} elseif ($node instanceof Node\Stmt\Use_) {
192+
$this->addUseToNamespace($node, $phpFile->addNamespace($namespace));
193+
} elseif ($node instanceof Node\Stmt\Class_) {
194+
if (!$node->name) {
195+
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
196+
}
197+
$class = $this->addClassToFile($phpFile, $node);
198+
} elseif ($node instanceof Node\Stmt\Interface_) {
199+
$class = $this->addInterfaceToFile($phpFile, $node);
200+
} elseif ($node instanceof Node\Stmt\Trait_) {
201+
$class = $this->addTraitToFile($phpFile, $node);
202+
} elseif ($node instanceof Node\Stmt\Enum_) {
203+
$class = $this->addEnumToFile($phpFile, $node);
204+
} elseif ($node instanceof Node\Stmt\Function_) {
205+
$this->addFunctionToFile($phpFile, $node);
206+
} elseif ($node instanceof Node\Stmt\TraitUse) {
207+
$this->addTraitToClass($class, $node);
208+
} elseif ($node instanceof Node\Stmt\Property) {
209+
$this->addPropertyToClass($class, $node);
210+
} elseif ($node instanceof Node\Stmt\ClassMethod) {
211+
$this->addMethodToClass($class, $node);
212+
} elseif ($node instanceof Node\Stmt\ClassConst) {
213+
$this->addConstantToClass($class, $node);
214+
} elseif ($node instanceof Node\Stmt\EnumCase) {
215+
$this->addEnumCaseToClass($class, $node);
216+
}
217+
if ($node instanceof Node\FunctionLike) {
218+
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
219+
}
220+
};
221+
222+
$traverser = new PhpParser\NodeTraverser;
223+
$traverser->addVisitor($visitor);
224+
$traverser->traverse($this->statements);
225+
return $phpFile;
226+
}
227+
228+
229+
private function addUseToNamespace(Node\Stmt\Use_ $node, PhpNamespace $namespace): void
230+
{
231+
if ($node->type === $node::TYPE_NORMAL) {
232+
foreach ($node->uses as $use) {
233+
$namespace->addUse($use->name->toString(), $use->alias ? $use->alias->toString() : null);
234+
}
235+
}
236+
}
237+
238+
239+
private function addClassToFile(PhpFile $phpFile, Node\Stmt\Class_ $node): ClassType
240+
{
241+
$class = $phpFile->addClass($node->namespacedName->toString());
242+
if ($node->extends) {
243+
$class->setExtends($node->extends->toString());
244+
}
245+
foreach ($node->implements as $item) {
246+
$class->addImplement($item->toString());
247+
}
248+
$class->setFinal($node->isFinal());
249+
$class->setAbstract($node->isAbstract());
250+
$this->addCommentAndAttributes($class, $node);
251+
return $class;
252+
}
253+
254+
255+
private function addInterfaceToFile(PhpFile $phpFile, Node\Stmt\Interface_ $node): ClassType
256+
{
257+
$class = $phpFile->addInterface($node->namespacedName->toString());
258+
foreach ($node->extends as $item) {
259+
$class->addExtend($item->toString());
260+
}
261+
$this->addCommentAndAttributes($class, $node);
262+
return $class;
263+
}
264+
265+
266+
private function addTraitToFile(PhpFile $phpFile, Node\Stmt\Trait_ $node): ClassType
267+
{
268+
$class = $phpFile->addTrait($node->namespacedName->toString());
269+
$this->addCommentAndAttributes($class, $node);
270+
return $class;
271+
}
272+
273+
274+
private function addEnumToFile(PhpFile $phpFile, Node\Stmt\Enum_ $node): ClassType
275+
{
276+
$class = $phpFile->addEnum($node->namespacedName->toString());
277+
foreach ($node->implements as $item) {
278+
$class->addImplement($item->toString());
279+
}
280+
$this->addCommentAndAttributes($class, $node);
281+
return $class;
282+
}
283+
284+
285+
private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void
286+
{
287+
$function = $phpFile->addFunction($node->namespacedName->toString());
288+
$this->setupFunction($function, $node);
289+
}
290+
291+
292+
private function addTraitToClass(ClassType $class, Node\Stmt\TraitUse $node): void
293+
{
294+
$res = [];
295+
foreach ($node->adaptations as $item) {
296+
$res[] = trim($this->toPhp($item), ';');
297+
}
298+
foreach ($node->traits as $trait) {
299+
$class->addTrait($trait->toString(), $res);
300+
$res = [];
301+
}
302+
}
303+
304+
305+
private function addPropertyToClass(ClassType $class, Node\Stmt\Property $node): void
306+
{
307+
foreach ($node->props as $item) {
308+
$prop = $class->addProperty($item->name->toString());
309+
$prop->setStatic($node->isStatic());
310+
if ($node->isPrivate()) {
311+
$prop->setPrivate();
312+
} elseif ($node->isProtected()) {
313+
$prop->setProtected();
314+
}
315+
$prop->setType($node->type ? $this->toPhp($node->type) : null);
316+
if ($item->default) {
317+
$prop->setValue(new Literal($this->toPhp($item->default)));
318+
}
319+
$prop->setReadOnly(method_exists($node, 'isReadonly') && $node->isReadonly());
320+
$this->addCommentAndAttributes($prop, $node);
321+
}
322+
}
323+
324+
325+
private function addMethodToClass(ClassType $class, Node\Stmt\ClassMethod $node): void
326+
{
327+
$method = $class->addMethod($node->name->toString());
328+
$method->setAbstract($node->isAbstract());
329+
$method->setFinal($node->isFinal());
330+
$method->setStatic($node->isStatic());
331+
if ($node->isPrivate()) {
332+
$method->setPrivate();
333+
} elseif ($node->isProtected()) {
334+
$method->setProtected();
335+
}
336+
$this->setupFunction($method, $node);
337+
}
338+
339+
340+
private function addConstantToClass(ClassType $class, Node\Stmt\ClassConst $node): void
341+
{
342+
foreach ($node->consts as $item) {
343+
$const = $class->addConstant($item->name->toString(), new Literal($this->toPhp($item->value)));
344+
if ($node->isPrivate()) {
345+
$const->setPrivate();
346+
} elseif ($node->isProtected()) {
347+
$const->setProtected();
348+
}
349+
$const->setFinal(method_exists($node, 'isFinal') && $node->isFinal());
350+
$this->addCommentAndAttributes($const, $node);
351+
}
352+
}
353+
354+
355+
private function addEnumCaseToClass(ClassType $class, Node\Stmt\EnumCase $node)
356+
{
357+
$case = $class->addCase($node->name->toString(), $node->expr ? $node->expr->value : null);
358+
$this->addCommentAndAttributes($case, $node);
359+
}
360+
361+
362+
private function addCommentAndAttributes($element, Node $node): void
363+
{
364+
if ($node->getDocComment()) {
365+
$comment = $node->getDocComment()->getReformattedText();
366+
$comment = Helpers::unformatDocComment($comment);
367+
$element->setComment($comment);
368+
}
369+
370+
foreach ($node->attrGroups ?? [] as $group) {
371+
foreach ($group->attrs as $attribute) {
372+
$args = [];
373+
foreach ($attribute->args as $arg) {
374+
$value = new Literal($this->toPhp($arg));
375+
if ($arg->name) {
376+
$args[$arg->name->toString()] = $value;
377+
} else {
378+
$args[] = $value;
379+
}
380+
}
381+
$element->addAttribute($attribute->name->toString(), $args);
382+
}
383+
}
384+
}
385+
386+
387+
/**
388+
* @param GlobalFunction|Method $function
389+
*/
390+
private function setupFunction($function, Node\FunctionLike $node): void
391+
{
392+
$function->setReturnReference($node->returnsByRef());
393+
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
394+
foreach ($node->params as $item) {
395+
$param = $function->addParameter($item->var->name);
396+
$param->setType($item->type ? $this->toPhp($item->type) : null);
397+
$param->setReference($item->byRef);
398+
$function->setVariadic($item->variadic);
399+
if ($item->default) {
400+
$param->setDefaultValue(new Literal($this->toPhp($item->default)));
401+
}
402+
$this->addCommentAndAttributes($param, $item);
403+
}
404+
$this->addCommentAndAttributes($function, $node);
405+
if ($node->stmts) {
406+
$function->setBody($this->getReformattedBody($node->stmts, 2));
407+
}
408+
}
409+
410+
411+
private function toPhp($value): string
412+
{
413+
return $this->printer->prettyPrint([$value]);
414+
}
415+
416+
170417
private function getNodeContents(Node ...$nodes): string
171418
{
172419
$start = $nodes[0]->getStartFilePos();

src/PhpGenerator/Factory.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,23 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
279279
}
280280

281281

282+
public function fromClassCode(string $code): ClassType
283+
{
284+
$classes = $this->fromCode($code)->getClasses();
285+
if (!$classes) {
286+
throw new Nette\InvalidStateException('The code does not contain any class.');
287+
}
288+
return reset($classes);
289+
}
290+
291+
292+
public function fromCode(string $code): PhpFile
293+
{
294+
$reader = new Extractor($code);
295+
return $reader->extractAll();
296+
}
297+
298+
282299
private function getAttributes($from): array
283300
{
284301
if (PHP_VERSION_ID < 80000) {

src/PhpGenerator/PhpFile.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ final class PhpFile
3232
private $strictTypes = false;
3333

3434

35+
public static function fromCode(string $code): self
36+
{
37+
return (new Factory)->fromCode($code);
38+
}
39+
40+
3541
public function addClass(string $name): ClassType
3642
{
3743
return $this
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\PhpGenerator\ClassType;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
$class = ClassType::fromCode(file_get_contents(__DIR__ . '/fixtures/classes.php'));
12+
Assert::type(ClassType::class, $class);
13+
Assert::match(<<<'XX'
14+
/**
15+
* Interface
16+
* @author John Doe
17+
*/
18+
interface Interface1
19+
{
20+
function func1();
21+
}
22+
XX
23+
, (string) $class);
24+
25+
26+
Assert::exception(function () {
27+
ClassType::fromCode('<?php');
28+
}, Nette\InvalidStateException::class, 'The code does not contain any class.');

0 commit comments

Comments
 (0)