From 9fa32362fb2de516691c1113473caf300858efdb 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/PropertyAccessMode.php | 33 ++++++++ src/PhpGenerator/Traits/PropertyLike.php | 72 ++++++++++++++++- .../PropertyLike.asymmetric-visiblity.phpt | 80 +++++++++++++++++++ 5 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/PhpGenerator/PropertyAccessMode.php create mode 100644 tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt diff --git a/readme.md b/readme.md index b8155370..ae6ef327 100644 --- a/readme.md +++ b/readme.md @@ -713,6 +713,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..ab3a9aee 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(PropertyAccessMode::Get); + $set = $param->getVisibility(PropertyAccessMode::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/PropertyAccessMode.php b/src/PhpGenerator/PropertyAccessMode.php new file mode 100644 index 00000000..ac53aeb8 --- /dev/null +++ b/src/PhpGenerator/PropertyAccessMode.php @@ -0,0 +1,33 @@ + ?string, 'get' => ?string} */ + private array $visibility = [PropertyAccessMode::Set => null, PropertyAccessMode::Get => null]; private bool $readOnly = false; /** @var array */ private array $hooks = [PropertyHookType::Set => null, PropertyHookType::Get => null]; + /** + * @param 'public'|'protected'|'private'|null $get + * @param 'public'|'protected'|'private'|null $set + */ + public function setVisibility(?string $get, ?string $set = null): static + { + $this->visibility = [ + PropertyAccessMode::Set => $set === null ? $set : Visibility::from($set), + PropertyAccessMode::Get => $get === null ? $get : Visibility::from($get), + ]; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function getVisibility(string $mode = PropertyAccessMode::Get): ?string + { + return $this->visibility[PropertyAccessMode::from($mode)]; + } + + + /** @param 'set'|'get' $mode */ + public function setPublic(string $mode = PropertyAccessMode::Get): static + { + $this->visibility[PropertyAccessMode::from($mode)] = Visibility::Public; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function isPublic(string $mode = PropertyAccessMode::Get): bool + { + return in_array($this->visibility[PropertyAccessMode::from($mode)], [Visibility::Public, null], true); + } + + + /** @param 'set'|'get' $mode */ + public function setProtected(string $mode = PropertyAccessMode::Get): static + { + $this->visibility[PropertyAccessMode::from($mode)] = Visibility::Protected; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function isProtected(string $mode = PropertyAccessMode::Get): bool + { + return $this->visibility[PropertyAccessMode::from($mode)] === Visibility::Protected; + } + + + /** @param 'set'|'get' $mode */ + public function setPrivate(string $mode = PropertyAccessMode::Get): static + { + $this->visibility[PropertyAccessMode::from($mode)] = Visibility::Private; + return $this; + } + + + /** @param 'set'|'get' $mode */ + public function isPrivate(string $mode = PropertyAccessMode::Get): bool + { + return $this->visibility[PropertyAccessMode::from($mode)] === Visibility::Private; + } + + public function setReadOnly(bool $state = true): static { $this->readOnly = $state; diff --git a/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt new file mode 100644 index 00000000..bc32b071 --- /dev/null +++ b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt @@ -0,0 +1,80 @@ +addProperty('first') + ->setType('string'); +Assert::true($default->isPublic(PropertyAccessMode::Get)); +Assert::true($default->isPublic(PropertyAccessMode::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'), + ValueError::class, +); + +// Test invalid setter visibility +Assert::exception( + fn() => $default->setVisibility('public', 'invalid'), + ValueError::class, +); + + +same(<<<'XX' + class Demo + { + public string $first; + private(set) string $second; + public protected(set) string $third; + protected private(set) string $fourth; + } + + XX, (string) $class);