Skip to content

Commit d201c9b

Browse files
committed
Factory & Extractor: added support for property hooks & asymmetric visibility
1 parent 9fa3236 commit d201c9b

File tree

7 files changed

+623
-11
lines changed

7 files changed

+623
-11
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) => Visibility::Public,
533+
(bool) ($flags & Modifiers::PROTECTED_SET) => Visibility::Protected,
534+
(bool) ($flags & Modifiers::PRIVATE_SET) => Visibility::Private,
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->addHooks($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->addHooks($from, $prop);
271+
$isInterface = $from->getDeclaringClass()->isInterface();
272+
$prop->setFinal($from->isFinal() && !$prop->isPrivate(PropertyAccessMode::Set));
273+
$prop->setAbstract($from->isAbstract() && !$isInterface);
274+
}
268275
return $prop;
269276
}
270277

271278

279+
private function addHooks(\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+
? Visibility::Private
288+
: ($from->isProtectedSet() ? Visibility::Protected : $getV);
289+
$defaultSetV = $from->isReadOnly() && $getV !== Visibility::Private
290+
? Visibility::Protected
291+
: $getV;
292+
if ($setV !== $defaultSetV) {
293+
$prop->setVisibility($getV === Visibility::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 */)');
+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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
class PropertyHookSignatures
2+
{
3+
public string $basic {
4+
get {
5+
}
6+
}
7+
8+
public string $fullGet {
9+
get {
10+
}
11+
}
12+
13+
protected string $refGet {
14+
&get {
15+
}
16+
}
17+
18+
protected string $finalGet {
19+
final get {
20+
}
21+
}
22+
23+
public string $basicSet {
24+
set {
25+
}
26+
}
27+
28+
public string $fullSet {
29+
set {
30+
}
31+
}
32+
33+
public string $setWithParam {
34+
set(string $foo) {
35+
}
36+
}
37+
38+
public string $setWithParam2 {
39+
set(string|int $value) {
40+
}
41+
}
42+
43+
public string $finalSet {
44+
final set {
45+
}
46+
}
47+
48+
public string $combined {
49+
set {
50+
}
51+
get {
52+
}
53+
}
54+
55+
final public string $combinedFinal {
56+
/** comment set */
57+
#[Set]
58+
set {
59+
}
60+
/** comment get */
61+
#[Get]
62+
get {
63+
}
64+
}
65+
66+
public string $virtualProp {
67+
set {
68+
}
69+
&get {
70+
}
71+
}
72+
}
73+
74+
abstract class AbstractHookSignatures
75+
{
76+
abstract public string $abstractGet { get; }
77+
abstract protected string $abstractSet { set; }
78+
abstract public string $abstractBoth { set; get; }
79+
80+
abstract public string $mixedGet {
81+
set {
82+
}
83+
get;
84+
}
85+
86+
abstract public string $mixedSet {
87+
set;
88+
get {
89+
}
90+
}
91+
}
92+
93+
interface InterfaceHookSignatures
94+
{
95+
public string $get { get; }
96+
97+
public string $set { #[Set]
98+
set; }
99+
public string $both { set; get; }
100+
public string $refGet { &get; }
101+
}
102+
103+
class AsymmetricVisibilitySignatures
104+
{
105+
private(set) string $first;
106+
protected(set) string $second;
107+
protected private(set) string $third;
108+
private(set) string $fourth;
109+
protected(set) string $fifth;
110+
public readonly string $implicit;
111+
private(set) readonly string $readFirst;
112+
private(set) readonly string $readSecond;
113+
protected readonly string $readThird;
114+
public(set) readonly string $readFourth;
115+
private(set) string $firstFinal;
116+
final protected(set) string $secondFinal;
117+
protected private(set) string $thirdFinal;
118+
private(set) string $fourthFinal;
119+
final protected(set) string $fifthFinal;
120+
}
121+
122+
class CombinedSignatures
123+
{
124+
protected(set) string $prop2 {
125+
final set {
126+
}
127+
get {
128+
}
129+
}
130+
131+
protected private(set) string $prop3 {
132+
set {
133+
}
134+
final get {
135+
}
136+
}
137+
}
138+
139+
class ConstructorAllSignatures
140+
{
141+
public function __construct(
142+
private(set) string $prop1,
143+
protected(set) string $prop2,
144+
protected private(set) string $prop3,
145+
private(set) string $prop4,
146+
protected(set) string $prop5,
147+
private(set) readonly string $readProp1,
148+
private(set) readonly string $readProp2,
149+
protected readonly string $readProp3,
150+
public(set) readonly string $readProp4,
151+
public string $hookProp1 {
152+
get {
153+
}
154+
},
155+
protected(set) string $mixedProp1 {
156+
set {
157+
}
158+
get {
159+
}
160+
},
161+
) {
162+
}
163+
}

0 commit comments

Comments
 (0)