Skip to content

Commit

Permalink
Factory & Extractor: added support for property hooks & asymmetric vi…
Browse files Browse the repository at this point in the history
…sibility
  • Loading branch information
dg committed Nov 28, 2024
1 parent 9fd279f commit f880dae
Show file tree
Hide file tree
Showing 9 changed files with 629 additions and 13 deletions.
66 changes: 56 additions & 10 deletions src/PhpGenerator/Extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use Nette;
use PhpParser;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\ParserFactory;
Expand Down Expand Up @@ -323,14 +324,37 @@ 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));
}

$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);
}
}
}

Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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;
Expand Down
46 changes: 45 additions & 1 deletion src/PhpGenerator/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 */)');
Expand Down
3 changes: 3 additions & 0 deletions src/PhpGenerator/Modifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
22 changes: 22 additions & 0 deletions tests/PhpGenerator/ClassType.from.84.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @phpVersion 8.4
*/

declare(strict_types=1);

use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\InterfaceType;

require __DIR__ . '/../bootstrap.php';
require __DIR__ . '/fixtures/classes.84.php';

$res[] = ClassType::from(Abc\PropertyHookSignatures::class);
$res[] = ClassType::from(Abc\AbstractHookSignatures::class);
$res[] = InterfaceType::from(Abc\InterfaceHookSignatures::class);
$res[] = ClassType::from(Abc\AsymmetricVisibilitySignatures::class);
$res[] = ClassType::from(Abc\CombinedSignatures::class);
$res[] = ClassType::from(Abc\ConstructorAllSignatures::class);

sameFile(__DIR__ . '/expected/ClassType.from.84.expect', implode("\n", $res));
5 changes: 5 additions & 0 deletions tests/PhpGenerator/Extractor.extractAll.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ sameFile(__DIR__ . '/expected/Extractor.classes.81.expect', (string) $file);
$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.82.php')))->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);

Expand Down
5 changes: 3 additions & 2 deletions tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
declare(strict_types=1);

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

require __DIR__ . '/../bootstrap.php';
Expand All @@ -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'));

Expand Down
Loading

0 comments on commit f880dae

Please sign in to comment.