Skip to content

Commit

Permalink
added support for asymmetric visibility
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Nov 28, 2024
1 parent a016f7f commit 33de521
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 4 deletions.
32 changes: 32 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
```

 <!---->

Expand Down
14 changes: 12 additions & 2 deletions src/PhpGenerator/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() ? '&' : '')
Expand Down Expand Up @@ -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' : '')
. ' '
Expand All @@ -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) {
Expand Down
82 changes: 80 additions & 2 deletions src/PhpGenerator/Traits/PropertyLike.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace Nette\PhpGenerator\Traits;

use Nette;
use Nette\PhpGenerator\Modifier;
use Nette\PhpGenerator\PropertyHook;


Expand All @@ -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<string, ?PropertyHook> */
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;
Expand Down Expand Up @@ -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.');
}
}
81 changes: 81 additions & 0 deletions tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/**
* Test: PropertyLike asymmetric visibility
*/

declare(strict_types=1);

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

require __DIR__ . '/../bootstrap.php';


$class = new ClassType('Demo');

// Default visibility
$default = $class->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);

0 comments on commit 33de521

Please sign in to comment.