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 29, 2024
1 parent b9b02a3 commit 9fa3236
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 4 deletions.
32 changes: 32 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
```

 <!---->

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(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) {
Expand Down
33 changes: 33 additions & 0 deletions src/PhpGenerator/PropertyAccessMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\PhpGenerator;

use Nette;


/**
* Property access mode.
*/
/*enum*/ final class PropertyAccessMode
{
use Nette\StaticClass;

public const Set = 'set';
public const Get = 'get';


/** @internal */
public static function from(string $value): string
{
return $value === self::Set || $value === self::Get
? $value
: throw new \ValueError("'$value' is not a valid value of access mode");
}
}
72 changes: 70 additions & 2 deletions src/PhpGenerator/Traits/PropertyLike.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,91 @@

namespace Nette\PhpGenerator\Traits;

use Nette\PhpGenerator\PropertyAccessMode;
use Nette\PhpGenerator\PropertyHook;
use Nette\PhpGenerator\PropertyHookType;
use Nette\PhpGenerator\Visibility;


/**
* @internal
*/
trait PropertyLike
{
use VisibilityAware;

/** @var array{'set' => ?string, 'get' => ?string} */
private array $visibility = [PropertyAccessMode::Set => null, PropertyAccessMode::Get => null];
private bool $readOnly = false;

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

/**
* Test: PropertyLike asymmetric visibility
*/

declare(strict_types=1);

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

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


$class = new ClassType('Demo');

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

0 comments on commit 9fa3236

Please sign in to comment.