Skip to content

Commit 961e611

Browse files
committed
Factory & Extractor: added support for property hooks & asymmetric visibility
1 parent 643ec81 commit 961e611

9 files changed

+629
-13
lines changed

src/PhpGenerator/Extractor.php

+56-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Nette;
1313
use PhpParser;
14+
use PhpParser\Modifiers;
1415
use PhpParser\Node;
1516
use PhpParser\NodeFinder;
1617
use PhpParser\ParserFactory;
@@ -323,14 +324,37 @@ private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node):
323324
foreach ($node->props as $item) {
324325
$prop = $class->addProperty($item->name->toString());
325326
$prop->setStatic($node->isStatic());
326-
$prop->setVisibility($this->toVisibility($node->flags));
327+
$prop->setVisibility($this->toVisibility($node->flags), $this->toSetterVisibility($node->flags));
327328
$prop->setType($node->type ? $this->toPhp($node->type) : null);
328329
if ($item->default) {
329330
$prop->setValue($this->toValue($item->default));
330331
}
331332

332333
$prop->setReadOnly((method_exists($node, 'isReadonly') && $node->isReadonly()) || ($class instanceof ClassType && $class->isReadOnly()));
333334
$this->addCommentAndAttributes($prop, $node);
335+
336+
$prop->setAbstract((bool) ($node->flags & Node\Stmt\Class_::MODIFIER_ABSTRACT));
337+
$prop->setFinal((bool) ($node->flags & Node\Stmt\Class_::MODIFIER_FINAL));
338+
$this->addHooksToProperty($prop, $node);
339+
}
340+
}
341+
342+
343+
private function addHooksToProperty(Property|PromotedParameter $prop, Node\Stmt\Property|Node\Param $node): void
344+
{
345+
if (!class_exists(Node\PropertyHook::class)) {
346+
return;
347+
}
348+
349+
foreach ($node->hooks as $hookNode) {
350+
$hook = $prop->addHook($hookNode->name->toString());
351+
$hook->setFinal((bool) ($hookNode->flags & Modifiers::FINAL));
352+
$this->setupFunction($hook, $hookNode);
353+
if ($hookNode->body === null) {
354+
$hook->setAbstract();
355+
} elseif (!is_array($hookNode->body)) {
356+
$hook->setBody($this->getReformattedContents([$hookNode->body], 1), short: true);
357+
}
334358
}
335359
}
336360

@@ -380,7 +404,7 @@ private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node):
380404

381405

382406
private function addCommentAndAttributes(
383-
PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse $element,
407+
PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse|PropertyHook $element,
384408
Node $node,
385409
): void
386410
{
@@ -408,19 +432,29 @@ private function addCommentAndAttributes(
408432
}
409433

410434

411-
private function setupFunction(GlobalFunction|Method $function, Node\FunctionLike $node): void
435+
private function setupFunction(GlobalFunction|Method|PropertyHook $function, Node\FunctionLike $node): void
412436
{
413437
$function->setReturnReference($node->returnsByRef());
414-
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
438+
if (!$function instanceof PropertyHook) {
439+
$function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
440+
}
441+
415442
foreach ($node->getParams() as $item) {
416-
$visibility = $this->toVisibility($item->flags);
417-
$isReadonly = (bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY);
418-
$param = $visibility
419-
? ($function->addPromotedParameter($item->var->name))->setVisibility($visibility)->setReadonly($isReadonly)
420-
: $function->addParameter($item->var->name);
443+
$getVisibility = $this->toVisibility($item->flags);
444+
$setVisibility = $this->toSetterVisibility($item->flags);
445+
if ($getVisibility || $setVisibility) {
446+
$param = $function->addPromotedParameter($item->var->name)
447+
->setVisibility($getVisibility, $setVisibility)
448+
->setReadonly((bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY));
449+
$this->addHooksToProperty($param, $item);
450+
} else {
451+
$param = $function->addParameter($item->var->name);
452+
}
421453
$param->setType($item->type ? $this->toPhp($item->type) : null);
422454
$param->setReference($item->byRef);
423-
$function->setVariadic($item->variadic);
455+
if (!$function instanceof PropertyHook) {
456+
$function->setVariadic($item->variadic);
457+
}
424458
if ($item->default) {
425459
$param->setDefaultValue($this->toValue($item->default));
426460
}
@@ -491,6 +525,18 @@ private function toVisibility(int $flags): ?string
491525
}
492526

493527

528+
private function toSetterVisibility(int $flags): ?string
529+
{
530+
return match (true) {
531+
!class_exists(Node\PropertyHook::class) => null,
532+
(bool) ($flags & Modifiers::PUBLIC_SET) => ClassType::VisibilityPublic,
533+
(bool) ($flags & Modifiers::PROTECTED_SET) => ClassType::VisibilityProtected,
534+
(bool) ($flags & Modifiers::PRIVATE_SET) => ClassType::VisibilityPrivate,
535+
default => null,
536+
};
537+
}
538+
539+
494540
private function toPhp(Node $value): string
495541
{
496542
$dolly = clone $value;

src/PhpGenerator/Factory.php

+45-1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ public function fromParameterReflection(\ReflectionParameter $from): Parameter
204204
$param = (new PromotedParameter($from->name))
205205
->setVisibility($this->getVisibility($property))
206206
->setReadOnly(PHP_VERSION_ID >= 80100 && $property->isReadonly());
207+
$this->importHooks($property, $param);
207208
} else {
208209
$param = new Parameter($from->name);
209210
}
@@ -260,15 +261,58 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property
260261
$prop->setStatic($from->isStatic());
261262
$prop->setVisibility($this->getVisibility($from));
262263
$prop->setType((string) $from->getType());
263-
264264
$prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults));
265265
$prop->setReadOnly(PHP_VERSION_ID >= 80100 && $from->isReadOnly());
266266
$prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment()));
267267
$prop->setAttributes($this->getAttributes($from));
268+
269+
if (PHP_VERSION_ID >= 80400) {
270+
$this->importHooks($from, $prop);
271+
$isInterface = $from->getDeclaringClass()->isInterface();
272+
$prop->setFinal($from->isFinal() && !$prop->isPrivate('set'));
273+
$prop->setAbstract($from->isAbstract() && !$isInterface);
274+
}
268275
return $prop;
269276
}
270277

271278

279+
private function importHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void
280+
{
281+
if (PHP_VERSION_ID < 80400) {
282+
return;
283+
}
284+
285+
$getV = $this->getVisibility($from);
286+
$setV = $from->isPrivateSet()
287+
? Modifier::Private
288+
: ($from->isProtectedSet() ? Modifier::Protected : $getV);
289+
$defaultSetV = $from->isReadOnly() && $getV !== Modifier::Private
290+
? Modifier::Protected
291+
: $getV;
292+
if ($setV !== $defaultSetV) {
293+
$prop->setVisibility($getV === Modifier::Public ? null : $getV, $setV);
294+
}
295+
296+
foreach ($from->getHooks() as $type => $hook) {
297+
$params = $hook->getParameters();
298+
if (
299+
count($params) === 1
300+
&& $params[0]->getName() === 'value'
301+
&& $params[0]->getType() == $from->getType() // intentionally ==
302+
) {
303+
$params = [];
304+
}
305+
$prop->addHook($type)
306+
->setParameters(array_map([$this, 'fromParameterReflection'], $params))
307+
->setAbstract($hook->isAbstract())
308+
->setFinal($hook->isFinal())
309+
->setReturnReference($hook->returnsReference())
310+
->setComment(Helpers::unformatDocComment((string) $hook->getDocComment()))
311+
->setAttributes($this->getAttributes($hook));
312+
}
313+
}
314+
315+
272316
public function fromObject(object $obj): Literal
273317
{
274318
return new Literal('new \\' . $obj::class . '(/* unknown */)');

src/PhpGenerator/Modifier.php

+3
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ final class Modifier
2222
public const Public = 'public';
2323
public const Protected = 'protected';
2424
public const Private = 'private';
25+
26+
public const Set = 'set';
27+
public const Get = 'get';
2528
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/**
4+
* @phpVersion 8.4
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\PhpGenerator\ClassType;
10+
use Nette\PhpGenerator\InterfaceType;
11+
12+
require __DIR__ . '/../bootstrap.php';
13+
require __DIR__ . '/fixtures/classes.84.php';
14+
15+
$res[] = ClassType::from(Abc\PropertyHookSignatures::class);
16+
$res[] = ClassType::from(Abc\AbstractHookSignatures::class);
17+
$res[] = InterfaceType::from(Abc\InterfaceHookSignatures::class);
18+
$res[] = ClassType::from(Abc\AsymmetricVisibilitySignatures::class);
19+
$res[] = ClassType::from(Abc\CombinedSignatures::class);
20+
$res[] = ClassType::from(Abc\ConstructorAllSignatures::class);
21+
22+
sameFile(__DIR__ . '/expected/ClassType.from.84.expect', implode("\n", $res));

tests/PhpGenerator/Extractor.extractAll.phpt

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ sameFile(__DIR__ . '/expected/Extractor.classes.81.expect', (string) $file);
1717
$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.82.php')))->extractAll();
1818
sameFile(__DIR__ . '/expected/Extractor.classes.82.expect', (string) $file);
1919

20+
if (class_exists(PhpParser\Node\PropertyHook::class)) {
21+
$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.84.php')))->extractAll();
22+
sameFile(__DIR__ . '/expected/Extractor.classes.84.expect', (string) $file);
23+
}
24+
2025
$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/enum.php')))->extractAll();
2126
sameFile(__DIR__ . '/expected/Extractor.enum.expect', (string) $file);
2227

tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
declare(strict_types=1);
88

99
use Nette\PhpGenerator\ClassType;
10+
use Nette\PhpGenerator\Modifier;
1011
use Tester\Assert;
1112

1213
require __DIR__ . '/../bootstrap.php';
@@ -17,8 +18,8 @@ $class = new ClassType('Demo');
1718
// Default visibility
1819
$default = $class->addProperty('first')
1920
->setType('string');
20-
Assert::true($default->isPublic('get'));
21-
Assert::true($default->isPublic('set'));
21+
Assert::true($default->isPublic(Modifier::Get));
22+
Assert::true($default->isPublic(Modifier::Set));
2223
Assert::null($default->getVisibility());
2324
Assert::null($default->getVisibility('set'));
2425

0 commit comments

Comments
 (0)