From 9fd279f8c7d7bd209756c9b730b579a3cfda4faa Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 27 Nov 2024 17:20:17 +0100 Subject: [PATCH] added support for asymmetric visibility --- readme.md | 32 ++++++++ src/PhpGenerator/Printer.php | 14 +++- src/PhpGenerator/Traits/PropertyLike.php | 82 ++++++++++++++++++- .../PropertyLike.asymmetric-visiblity.phpt | 81 ++++++++++++++++++ 4 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt diff --git a/readme.md b/readme.md index b8030b14..ec05936a 100644 --- a/readme.md +++ b/readme.md @@ -712,6 +712,38 @@ $class->addProperty('role') ->setFinal(); ``` +  + +Asymmetric Visibility +--------------------- + +PHP 8.4 introduces asymmetric visibility for properties. You can set different access levels for reading and writing. +The visibility can be set using either the `setVisibility()` method with two parameters, or by using `setPublic()`, `setProtected()`, or `setPrivate()` with the `mode` parameter that specifies whether the visibility applies to getting or setting the property. The default mode is 'get'. + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); + +$class->addProperty('name') + ->setType('string') + ->setVisibility('public', 'private'); // public for read, private for write + +$class->addProperty('id') + ->setType('int') + ->setProtected('set'); // protected for write + +echo $class; +``` + +This generates: + +```php +class Demo +{ + public private(set) string $name; + + protected(set) int $id; +} +```   diff --git a/src/PhpGenerator/Printer.php b/src/PhpGenerator/Printer.php index 39336d15..eccebda5 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 + { + $get = $param->getVisibility('get'); + $set = $param->getVisibility('set'); + return $set + ? ($get ? "$get $set(set)" : "$set(set)") + : $get ?? '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 33709a97..c5fa39fe 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,82 @@ */ trait PropertyLike { - use VisibilityAware; - + /** @var array{'set' => ?string, 'get' => ?string} */ + private array $visibility = ['set' => null, 'get' => null]; private bool $readOnly = false; /** @var array */ private array $hooks = ['set' => null, 'get' => null]; + /** + * @param ?string $get public|protected|private + * @param ?string $set public|protected|private + */ + public function setVisibility(?string $get, ?string $set = null): static + { + if (!in_array($get, [Modifier::Public, Modifier::Protected, Modifier::Private, null], true) + || !in_array($set, [Modifier::Public, Modifier::Protected, Modifier::Private, null], true)) { + throw new Nette\InvalidArgumentException('Argument must be public|protected|private.'); + } + + $this->visibility = ['set' => $set, 'get' => $get]; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function getVisibility(string $mode = 'get'): ?string + { + return $this->visibility[$this->checkMode($mode)]; + } + + + /** @param 'set'|'get' $mode */ + public function setPublic(string $mode = 'get'): static + { + $this->visibility[$this->checkMode($mode)] = Modifier::Public; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function isPublic(string $mode = 'get'): bool + { + return in_array($this->visibility[$this->checkMode($mode)], [Modifier::Public, null], true); + } + + + /** @param 'set'|'get' $mode */ + public function setProtected(string $mode = 'get'): static + { + $this->visibility[$this->checkMode($mode)] = Modifier::Protected; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function isProtected(string $mode = 'get'): bool + { + return $this->visibility[$this->checkMode($mode)] === Modifier::Protected; + } + + + /** @param 'set'|'get' $mode */ + public function setPrivate(string $mode = 'get'): static + { + $this->visibility[$this->checkMode($mode)] = Modifier::Private; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function isPrivate(string $mode = 'get'): bool + { + return $this->visibility[$this->checkMode($mode)] === Modifier::Private; + } + + public function setReadOnly(bool $state = true): static { $this->readOnly = $state; @@ -79,4 +149,12 @@ public function hasHook(string|\PropertyHookType $type): bool { return isset($this->hooks[$type]); } + + + private function checkMode(string $mode): string + { + return $mode === Modifier::Set || $mode === Modifier::Get + ? $mode + : throw new Nette\InvalidArgumentException('Argument must be set|get.'); + } } diff --git a/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt new file mode 100644 index 00000000..eb0ab2e2 --- /dev/null +++ b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt @@ -0,0 +1,81 @@ +addProperty('first') + ->setType('string'); +Assert::true($default->isPublic('get')); +Assert::true($default->isPublic('set')); +Assert::null($default->getVisibility()); +Assert::null($default->getVisibility('set')); + +// Public with private setter +$restricted = $class->addProperty('second') + ->setType('string') + ->setVisibility(null, 'private'); +Assert::true($restricted->isPublic()); +Assert::false($restricted->isPublic('set')); +Assert::true($restricted->isPrivate('set')); +Assert::null($restricted->getVisibility()); +Assert::same('private', $restricted->getVisibility('set')); + +// Public with protected setter using individual methods +$mixed = $class->addProperty('third') + ->setType('string') + ->setPublic() + ->setProtected('set'); +Assert::true($mixed->isPublic()); +Assert::false($mixed->isPublic('set')); +Assert::true($mixed->isProtected('set')); +Assert::same('public', $mixed->getVisibility()); +Assert::same('protected', $mixed->getVisibility('set')); + +// Protected with private setter +$nested = $class->addProperty('fourth') + ->setType('string') + ->setProtected() + ->setPrivate('set'); +Assert::false($nested->isPublic()); +Assert::true($nested->isProtected()); +Assert::true($nested->isPrivate('set')); +Assert::same('protected', $nested->getVisibility()); +Assert::same('private', $nested->getVisibility('set')); + +// 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);