diff --git a/src/PhpGenerator/Extractor.php b/src/PhpGenerator/Extractor.php index d5c876e1..59971f6f 100644 --- a/src/PhpGenerator/Extractor.php +++ b/src/PhpGenerator/Extractor.php @@ -11,6 +11,7 @@ use Nette; use PhpParser; +use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\NodeFinder; use PhpParser\ParserFactory; @@ -323,7 +324,7 @@ private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node): foreach ($node->props as $item) { $prop = $class->addProperty($item->name->toString()); $prop->setStatic($node->isStatic()); - $prop->setVisibility($this->toVisibility($node->flags)); + $prop->setVisibility($this->toVisibility($node->flags), $this->toSetterVisibility($node->flags)); $prop->setType($node->type ? $this->toPhp($node->type) : null); if ($item->default) { $prop->setValue($this->toValue($item->default)); @@ -331,6 +332,29 @@ private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node): $prop->setReadOnly((method_exists($node, 'isReadonly') && $node->isReadonly()) || ($class instanceof ClassType && $class->isReadOnly())); $this->addCommentAndAttributes($prop, $node); + + $prop->setAbstract((bool) ($node->flags & Node\Stmt\Class_::MODIFIER_ABSTRACT)); + $prop->setFinal((bool) ($node->flags & Node\Stmt\Class_::MODIFIER_FINAL)); + $this->addHooksToProperty($prop, $node); + } + } + + + private function addHooksToProperty(Property|PromotedParameter $prop, Node\Stmt\Property|Node\Param $node): void + { + if (!class_exists(Node\PropertyHook::class)) { + return; + } + + foreach ($node->hooks as $hookNode) { + $hook = $prop->addHook($hookNode->name->toString()); + $hook->setFinal((bool) ($hookNode->flags & Modifiers::FINAL)); + $this->setupFunction($hook, $hookNode); + if ($hookNode->body === null) { + $hook->setAbstract(); + } elseif (!is_array($hookNode->body)) { + $hook->setBody($this->getReformattedContents([$hookNode->body], 1), short: true); + } } } @@ -380,7 +404,7 @@ private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): private function addCommentAndAttributes( - PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse $element, + PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse|PropertyHook $element, Node $node, ): void { @@ -408,19 +432,29 @@ private function addCommentAndAttributes( } - private function setupFunction(GlobalFunction|Method $function, Node\FunctionLike $node): void + private function setupFunction(GlobalFunction|Method|PropertyHook $function, Node\FunctionLike $node): void { $function->setReturnReference($node->returnsByRef()); - $function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null); + if (!$function instanceof PropertyHook) { + $function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null); + } + foreach ($node->getParams() as $item) { - $visibility = $this->toVisibility($item->flags); - $isReadonly = (bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY); - $param = $visibility - ? ($function->addPromotedParameter($item->var->name))->setVisibility($visibility)->setReadonly($isReadonly) - : $function->addParameter($item->var->name); + $getVisibility = $this->toVisibility($item->flags); + $setVisibility = $this->toSetterVisibility($item->flags); + if ($getVisibility || $setVisibility) { + $param = $function->addPromotedParameter($item->var->name) + ->setVisibility($getVisibility, $setVisibility) + ->setReadonly((bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY)); + $this->addHooksToProperty($param, $item); + } else { + $param = $function->addParameter($item->var->name); + } $param->setType($item->type ? $this->toPhp($item->type) : null); $param->setReference($item->byRef); - $function->setVariadic($item->variadic); + if (!$function instanceof PropertyHook) { + $function->setVariadic($item->variadic); + } if ($item->default) { $param->setDefaultValue($this->toValue($item->default)); } @@ -491,6 +525,18 @@ private function toVisibility(int $flags): ?string } + private function toSetterVisibility(int $flags): ?string + { + return match (true) { + !class_exists(Node\PropertyHook::class) => null, + (bool) ($flags & Modifiers::PUBLIC_SET) => ClassType::VisibilityPublic, + (bool) ($flags & Modifiers::PROTECTED_SET) => ClassType::VisibilityProtected, + (bool) ($flags & Modifiers::PRIVATE_SET) => ClassType::VisibilityPrivate, + default => null, + }; + } + + private function toPhp(Node $value): string { $dolly = clone $value; diff --git a/src/PhpGenerator/Factory.php b/src/PhpGenerator/Factory.php index e535203d..41a2961c 100644 --- a/src/PhpGenerator/Factory.php +++ b/src/PhpGenerator/Factory.php @@ -204,6 +204,7 @@ public function fromParameterReflection(\ReflectionParameter $from): Parameter $param = (new PromotedParameter($from->name)) ->setVisibility($this->getVisibility($property)) ->setReadOnly(PHP_VERSION_ID >= 80100 && $property->isReadonly()); + $this->importHooks($property, $param); } else { $param = new Parameter($from->name); } @@ -260,15 +261,58 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property $prop->setStatic($from->isStatic()); $prop->setVisibility($this->getVisibility($from)); $prop->setType((string) $from->getType()); - $prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults)); $prop->setReadOnly(PHP_VERSION_ID >= 80100 && $from->isReadOnly()); $prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); $prop->setAttributes($this->getAttributes($from)); + + if (PHP_VERSION_ID >= 80400) { + $this->importHooks($from, $prop); + $isInterface = $from->getDeclaringClass()->isInterface(); + $prop->setFinal($from->isFinal() && !$prop->isPrivate('set')); + $prop->setAbstract($from->isAbstract() && !$isInterface); + } return $prop; } + private function importHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void + { + if (PHP_VERSION_ID < 80400) { + return; + } + + $getV = $this->getVisibility($from); + $setV = $from->isPrivateSet() + ? Modifier::Private + : ($from->isProtectedSet() ? Modifier::Protected : $getV); + $defaultSetV = $from->isReadOnly() && $getV !== Modifier::Private + ? Modifier::Protected + : $getV; + if ($setV !== $defaultSetV) { + $prop->setVisibility($getV === Modifier::Public ? null : $getV, $setV); + } + + foreach ($from->getHooks() as $type => $hook) { + $params = $hook->getParameters(); + if ( + count($params) === 1 + && $params[0]->getName() === 'value' + && $params[0]->getType() == $from->getType() // intentionally == + ) { + $params = []; + } + $prop->addHook($type) + ->setParameters(array_map([$this, 'fromParameterReflection'], $params)) + ->setAbstract($hook->isAbstract()) + ->setFinal($hook->isFinal()) + ->setReturnReference($hook->returnsReference()) + ->setComment(Helpers::unformatDocComment((string) $hook->getDocComment())) + ->setAttributes($this->getAttributes($hook)); + } + } + + public function fromObject(object $obj): Literal { return new Literal('new \\' . $obj::class . '(/* unknown */)'); diff --git a/src/PhpGenerator/Modifier.php b/src/PhpGenerator/Modifier.php index f27f789c..238b5cc0 100644 --- a/src/PhpGenerator/Modifier.php +++ b/src/PhpGenerator/Modifier.php @@ -22,4 +22,7 @@ final class Modifier public const Public = 'public'; public const Protected = 'protected'; public const Private = 'private'; + + public const Set = 'set'; + public const Get = 'get'; } diff --git a/tests/PhpGenerator/ClassType.from.84.phpt b/tests/PhpGenerator/ClassType.from.84.phpt new file mode 100644 index 00000000..984d9a85 --- /dev/null +++ b/tests/PhpGenerator/ClassType.from.84.phpt @@ -0,0 +1,22 @@ +extractAll(); sameFile(__DIR__ . '/expected/Extractor.classes.82.expect', (string) $file); +if (class_exists(PhpParser\Node\PropertyHook::class)) { + $file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.84.php')))->extractAll(); + sameFile(__DIR__ . '/expected/Extractor.classes.84.expect', (string) $file); +} + $file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/enum.php')))->extractAll(); sameFile(__DIR__ . '/expected/Extractor.enum.expect', (string) $file); diff --git a/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt index eb0ab2e2..baafea6f 100644 --- a/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt +++ b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Modifier; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -17,8 +18,8 @@ $class = new ClassType('Demo'); // Default visibility $default = $class->addProperty('first') ->setType('string'); -Assert::true($default->isPublic('get')); -Assert::true($default->isPublic('set')); +Assert::true($default->isPublic(Modifier::Get)); +Assert::true($default->isPublic(Modifier::Set)); Assert::null($default->getVisibility()); Assert::null($default->getVisibility('set')); diff --git a/tests/PhpGenerator/expected/ClassType.from.84.expect b/tests/PhpGenerator/expected/ClassType.from.84.expect new file mode 100644 index 00000000..f2ef803b --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.84.expect @@ -0,0 +1,163 @@ +class PropertyHookSignatures +{ + public string $basic { + get { + } + } + + public string $fullGet { + get { + } + } + + protected string $refGet { + &get { + } + } + + protected string $finalGet { + final get { + } + } + + public string $basicSet { + set { + } + } + + public string $fullSet { + set { + } + } + + public string $setWithParam { + set(string $foo) { + } + } + + public string $setWithParam2 { + set(string|int $value) { + } + } + + public string $finalSet { + final set { + } + } + + public string $combined { + set { + } + get { + } + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { + } + /** comment get */ + #[Get] + get { + } + } + + public string $virtualProp { + set { + } + &get { + } + } +} + +abstract class AbstractHookSignatures +{ + abstract public string $abstractGet { get; } + abstract protected string $abstractSet { set; } + abstract public string $abstractBoth { set; get; } + + abstract public string $mixedGet { + set { + } + get; + } + + abstract public string $mixedSet { + set; + get { + } + } +} + +interface InterfaceHookSignatures +{ + public string $get { get; } + + public string $set { #[Set] + set; } + public string $both { set; get; } + public string $refGet { &get; } +} + +class AsymmetricVisibilitySignatures +{ + private(set) string $first; + protected(set) string $second; + protected private(set) string $third; + private(set) string $fourth; + protected(set) string $fifth; + public readonly string $implicit; + private(set) readonly string $readFirst; + private(set) readonly string $readSecond; + protected readonly string $readThird; + public(set) readonly string $readFourth; + private(set) string $firstFinal; + final protected(set) string $secondFinal; + protected private(set) string $thirdFinal; + private(set) string $fourthFinal; + final protected(set) string $fifthFinal; +} + +class CombinedSignatures +{ + protected(set) string $prop2 { + final set { + } + get { + } + } + + protected private(set) string $prop3 { + set { + } + final get { + } + } +} + +class ConstructorAllSignatures +{ + public function __construct( + private(set) string $prop1, + protected(set) string $prop2, + protected private(set) string $prop3, + private(set) string $prop4, + protected(set) string $prop5, + private(set) readonly string $readProp1, + private(set) readonly string $readProp2, + protected readonly string $readProp3, + public(set) readonly string $readProp4, + public string $hookProp1 { + get { + } + }, + protected(set) string $mixedProp1 { + set { + } + get { + } + }, + ) { + } +} diff --git a/tests/PhpGenerator/expected/Extractor.classes.84.expect b/tests/PhpGenerator/expected/Extractor.classes.84.expect new file mode 100644 index 00000000..ba6cfc57 --- /dev/null +++ b/tests/PhpGenerator/expected/Extractor.classes.84.expect @@ -0,0 +1,162 @@ + 'x'; + } + + public string $fullGet { + get { + return 'x'; + } + } + + protected string $refGet { + &get { + return 'x'; + } + } + + protected string $finalGet { + final get => 'x'; + } + + public string $basicSet { + set => 'x'; + } + + public string $fullSet { + set { + } + } + + public string $setWithParam { + set(string $foo) { + } + } + + public string $setWithParam2 { + set(string|int $value) => ''; + } + + public string $finalSet { + final set { + } + } + + public string $combined { + set(string $value) { + } + get => 'x'; + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { + } + /** comment get */ + #[Get] + get => 'x'; + } + + public string $virtualProp { + set { + } + &get => 'x'; + } +} + +abstract class AbstractHookSignatures +{ + abstract public string $abstractGet { get; } + abstract protected string $abstractSet { set; } + abstract public string $abstractBoth { set; get; } + + abstract public string $mixedGet { + set => 'x'; + get; + } + + abstract public string $mixedSet { + set; + get => 'x'; + } +} + +interface InterfaceHookSignatures +{ + public string $get { get; } + + public string $set { #[Set] + set; } + public string $both { set; get; } + public string $refGet { &get; } +} + +class AsymmetricVisibilitySignatures +{ + public private(set) string $first; + public protected(set) string $second; + protected private(set) string $third; + private(set) string $fourth; + protected(set) string $fifth; + public readonly string $implicit; + public private(set) readonly string $readFirst; + private(set) readonly string $readSecond; + protected protected(set) readonly string $readThird; + public public(set) readonly string $readFourth; + final public private(set) string $firstFinal; + final public protected(set) string $secondFinal; + final protected private(set) string $thirdFinal; + final private(set) string $fourthFinal; + final protected(set) string $fifthFinal; +} + +class CombinedSignatures +{ + public protected(set) string $prop2 { + final set { + } + get { + return 'x'; + } + } + + protected private(set) string $prop3 { + set(string $value) { + } + final get => 'x'; + } +} + +class ConstructorAllSignatures +{ + public function __construct( + public private(set) string $prop1, + public protected(set) string $prop2, + protected private(set) string $prop3, + private(set) string $prop4, + protected(set) string $prop5, + public private(set) readonly string $readProp1, + private(set) readonly string $readProp2, + protected protected(set) readonly string $readProp3, + public public(set) readonly string $readProp4, + public string $hookProp1 { + get => 'x'; + }, + public protected(set) string $mixedProp1 { + set { + } + get { + return 'x'; + } + }, + ) { + } +} diff --git a/tests/PhpGenerator/fixtures/classes.84.php b/tests/PhpGenerator/fixtures/classes.84.php new file mode 100644 index 00000000..9b2367e5 --- /dev/null +++ b/tests/PhpGenerator/fixtures/classes.84.php @@ -0,0 +1,170 @@ + 'x'; + } + + public string $fullGet { + get { return 'x'; } + } + + protected string $refGet { + &get { return 'x'; } + } + + protected string $finalGet { + final get => 'x'; + } + + // Set variants + public string $basicSet { + set => 'x'; + } + + public string $fullSet { + set { } + } + + public string $setWithParam { + set(string $foo) { } + } + + public string $setWithParam2 { + set(string|int $value) => ''; + } + + public string $finalSet { + final set { } + } + + // Combinations + public string $combined { + set(string $value) { } + get => 'x'; + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { } + /** comment get */ + #[Get] + get => 'x'; + } + + public string $virtualProp { + set { } + &get => 'x'; + } +} + +// Abstract hooks +abstract class AbstractHookSignatures +{ + // Abstract variants + abstract public string $abstractGet { get; } + + abstract protected string $abstractSet { set; } + + abstract public string $abstractBoth { set; get; } + // Combination of abstract/concrete + abstract public string $mixedGet { + set => 'x'; + get; + } + + abstract public string $mixedSet { + set; + get => 'x'; + } +} + +// Interface with hooks +interface InterfaceHookSignatures +{ + public string $get { get; } + + public string $set { #[Set] set; } + + public string $both { set; get; } + + // Get can be forced as reference + public string $refGet { &get; } +} + +// Asymmetric visibility - all valid combinations +class AsymmetricVisibilitySignatures +{ + // Basic variants + public private(set) string $first; + public protected(set) string $second; + protected private(set) string $third; + private(set) string $fourth; + protected(set) string $fifth; + + // With readonly + public readonly string $implicit; + public private(set) readonly string $readFirst; + private(set) readonly string $readSecond; + protected protected(set) readonly string $readThird; + public public(set) readonly string $readFourth; + + // With final + final public private(set) string $firstFinal; + final public protected(set) string $secondFinal; + final protected private(set) string $thirdFinal; + final private(set) string $fourthFinal; + final protected(set) string $fifthFinal; +} + +// Combination of hooks and asymmetric visibility +class CombinedSignatures +{ + public protected(set) string $prop2 { + final set { } + get { return 'x'; } + } + + protected private(set) string $prop3 { + set(string $value) { } + final get => 'x'; + } +} + +// Constructor property promotion with asymmetric visibility +class ConstructorAllSignatures +{ + public function __construct( + // Basic asymmetric visibility + public private(set) string $prop1, + public protected(set) string $prop2, + protected private(set) string $prop3, + private(set) string $prop4, + protected(set) string $prop5, + + // With readonly + public private(set) readonly string $readProp1, + private(set) readonly string $readProp2, + protected protected(set) readonly string $readProp3, + public public(set) readonly string $readProp4, + + // With hooks + public string $hookProp1 { + get => 'x'; + }, + + // Combination of hooks and asymmetric visibility + public protected(set) string $mixedProp1 { + set { } + get { return 'x'; } + }, + ) {} +}