From a0a1ff3958c3f43cf82eaf2935693b762a42cae1 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 27 Nov 2024 17:20:17 +0100 Subject: [PATCH] AsymmetricVisibility wip --- src/PhpGenerator/Printer.php | 14 +++- src/PhpGenerator/Traits/PropertyLike.php | 67 ++++++++++++++- .../Property.asymmetric-visiblity.phpt | 81 +++++++++++++++++++ 3 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 tests/PhpGenerator/Property.asymmetric-visiblity.phpt diff --git a/src/PhpGenerator/Printer.php b/src/PhpGenerator/Printer.php index 39336d15..a360d512 100644 --- a/src/PhpGenerator/Printer.php +++ b/src/PhpGenerator/Printer.php @@ -344,7 +344,7 @@ private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $fu $this->printDocComment($param) . ($attrs ? ($multiline ? substr($attrs, 0, -1) . "\n" : $attrs) : '') . ($param instanceof PromotedParameter - ? ($param->getVisibility() ?: 'public') . ($param->isReadOnly() && $param->getType() ? ' readonly' : '') . ' ' + ? $this->printPropertyVisibility($param) . ($param->isReadOnly() && $param->getType() ? ' readonly' : '') . ' ' : '') . ltrim($this->printType($param->getType(), $param->isNullable()) . ' ') . ($param->isReference() ? '&' : '') @@ -382,7 +382,7 @@ private function printProperty(Property $property, bool $readOnlyClass = false, $type = $property->getType(); $def = ($property->isAbstract() && !$isInterface ? 'abstract ' : '') . ($property->isFinal() ? 'final ' : '') - . ($property->getVisibility() ?: 'public') + . $this->printPropertyVisibility($property) . ($property->isStatic() ? ' static' : '') . (!$readOnlyClass && $property->isReadOnly() && $type ? ' readonly' : '') . ' ' @@ -402,6 +402,16 @@ private function printProperty(Property $property, bool $readOnlyClass = false, } + private function printPropertyVisibility(Property|PromotedParameter $param): string + { + $read = $param->getVisibility(write: false); + $write = $param->getVisibility(write: true); + return $write + ? ($read ? "$read $write(set)" : "$write(set)") + : $read ?? 'public'; + } + + protected function printType(?string $type, bool $nullable): string { if ($type === null) { diff --git a/src/PhpGenerator/Traits/PropertyLike.php b/src/PhpGenerator/Traits/PropertyLike.php index 165d724a..d3b409a1 100644 --- a/src/PhpGenerator/Traits/PropertyLike.php +++ b/src/PhpGenerator/Traits/PropertyLike.php @@ -9,6 +9,8 @@ namespace Nette\PhpGenerator\Traits; +use Nette; +use Nette\PhpGenerator\Modifier; use Nette\PhpGenerator\PropertyHook; @@ -17,14 +19,75 @@ */ trait PropertyLike { - use VisibilityAware; - + /** @var array{?string, ?string} */ + private array $visibility = [null, null]; private bool $readOnly = false; /** @var array */ private array $hooks = ['set' => null, 'get' => null]; + /** + * @param ?string $read public|protected|private + * @param ?string $write public|protected|private + */ + public function setVisibility(?string $read, ?string $write = null): static + { + if (!in_array($read, [Modifier::Public, Modifier::Protected, Modifier::Private, null], true) + || !in_array($write, [Modifier::Public, Modifier::Protected, Modifier::Private, null], true)) { + throw new Nette\InvalidArgumentException('Argument must be public|protected|private.'); + } + + $this->visibility = [$read, $write]; + return $this; + } + + + public function getVisibility(bool $write = false): ?string + { + return $this->visibility[$write]; + } + + + public function setPublic(bool $write = false): static + { + $this->visibility[$write] = Modifier::Public; + return $this; + } + + + public function isPublic(bool $write = false): bool + { + return in_array($this->visibility[$write], [Modifier::Public, null], true); + } + + + public function setProtected(bool $write = false): static + { + $this->visibility[$write] = Modifier::Protected; + return $this; + } + + + public function isProtected(bool $write = false): bool + { + return $this->visibility[$write] === Modifier::Protected; + } + + + public function setPrivate(bool $write = false): static + { + $this->visibility[$write] = Modifier::Private; + return $this; + } + + + public function isPrivate(bool $write = false): bool + { + return $this->visibility[$write] === Modifier::Private; + } + + public function setReadOnly(bool $state = true): static { $this->readOnly = $state; diff --git a/tests/PhpGenerator/Property.asymmetric-visiblity.phpt b/tests/PhpGenerator/Property.asymmetric-visiblity.phpt new file mode 100644 index 00000000..81c297a9 --- /dev/null +++ b/tests/PhpGenerator/Property.asymmetric-visiblity.phpt @@ -0,0 +1,81 @@ +addProperty('first') + ->setType('string'); +Assert::true($default->isPublic()); +Assert::true($default->isPublic(write: true)); +Assert::null($default->getVisibility()); +Assert::null($default->getVisibility(write: true)); + +// Public with private setter +$restricted = $class->addProperty('second') + ->setType('string') + ->setVisibility(null, 'private'); +Assert::true($restricted->isPublic()); +Assert::false($restricted->isPublic(write: true)); +Assert::true($restricted->isPrivate(write: true)); +Assert::null($restricted->getVisibility()); +Assert::same('private', $restricted->getVisibility(write: true)); + +// Public with protected setter using individual methods +$mixed = $class->addProperty('third') + ->setType('string') + ->setPublic() + ->setProtected(write: true); +Assert::true($mixed->isPublic()); +Assert::false($mixed->isPublic(write: true)); +Assert::true($mixed->isProtected(write: true)); +Assert::same('public', $mixed->getVisibility()); +Assert::same('protected', $mixed->getVisibility(write: true)); + +// Protected with private setter +$nested = $class->addProperty('fourth') + ->setType('string') + ->setProtected() + ->setPrivate(write: true); +Assert::false($nested->isPublic()); +Assert::true($nested->isProtected()); +Assert::true($nested->isPrivate(write: true)); +Assert::same('protected', $nested->getVisibility()); +Assert::same('private', $nested->getVisibility(write: true)); + +// Test invalid getter visibility +Assert::exception( + fn() => $default->setVisibility('invalid', 'public'), + Nette\InvalidArgumentException::class, + 'Argument must be public|protected|private.', +); + +// Test invalid setter visibility +Assert::exception( + fn() => $default->setVisibility('public', 'invalid'), + Nette\InvalidArgumentException::class, + 'Argument must be public|protected|private.', +); + + +same(<<<'XX' + class Demo + { + public string $first; + private(set) string $second; + public protected(set) string $third; + protected private(set) string $fourth; + } + + XX, (string) $class);