Skip to content

Commit ec8a95b

Browse files
committed
added support for asymmetric visibility
1 parent 6de4f01 commit ec8a95b

File tree

4 files changed

+205
-4
lines changed

4 files changed

+205
-4
lines changed

readme.md

+32
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,38 @@ $class->addProperty('role')
713713
->setFinal();
714714
```
715715

716+
 <!---->
717+
718+
Asymmetric Visibility
719+
---------------------
720+
721+
PHP 8.4 introduces asymmetric visibility for properties. You can set different access levels for reading and writing.
722+
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'.
723+
724+
```php
725+
$class = new Nette\PhpGenerator\ClassType('Demo');
726+
727+
$class->addProperty('name')
728+
->setType('string')
729+
->setVisibility('public', 'private'); // public for read, private for write
730+
731+
$class->addProperty('id')
732+
->setType('int')
733+
->setProtected('set'); // protected for write
734+
735+
echo $class;
736+
```
737+
738+
This generates:
739+
740+
```php
741+
class Demo
742+
{
743+
public private(set) string $name;
744+
745+
protected(set) int $id;
746+
}
747+
```
716748

717749
 <!---->
718750

src/PhpGenerator/Printer.php

+12-2
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $fu
344344
$this->printDocComment($param)
345345
. ($attrs ? ($multiline ? substr($attrs, 0, -1) . "\n" : $attrs) : '')
346346
. ($param instanceof PromotedParameter
347-
? ($param->getVisibility() ?: 'public') . ($param->isReadOnly() && $param->getType() ? ' readonly' : '') . ' '
347+
? $this->printPropertyVisibility($param) . ($param->isReadOnly() && $param->getType() ? ' readonly' : '') . ' '
348348
: '')
349349
. ltrim($this->printType($param->getType(), $param->isNullable()) . ' ')
350350
. ($param->isReference() ? '&' : '')
@@ -382,7 +382,7 @@ private function printProperty(Property $property, bool $readOnlyClass = false,
382382
$type = $property->getType();
383383
$def = ($property->isAbstract() && !$isInterface ? 'abstract ' : '')
384384
. ($property->isFinal() ? 'final ' : '')
385-
. ($property->getVisibility() ?: 'public')
385+
. $this->printPropertyVisibility($property)
386386
. ($property->isStatic() ? ' static' : '')
387387
. (!$readOnlyClass && $property->isReadOnly() && $type ? ' readonly' : '')
388388
. ' '
@@ -402,6 +402,16 @@ private function printProperty(Property $property, bool $readOnlyClass = false,
402402
}
403403

404404

405+
private function printPropertyVisibility(Property|PromotedParameter $param): string
406+
{
407+
$get = $param->getVisibility('get');
408+
$set = $param->getVisibility('set');
409+
return $set
410+
? ($get ? "$get $set(set)" : "$set(set)")
411+
: $get ?? 'public';
412+
}
413+
414+
405415
protected function printType(?string $type, bool $nullable): string
406416
{
407417
if ($type === null) {

src/PhpGenerator/Traits/PropertyLike.php

+80-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace Nette\PhpGenerator\Traits;
1111

12+
use Nette;
13+
use Nette\PhpGenerator\Modifier;
1214
use Nette\PhpGenerator\PropertyHook;
1315

1416

@@ -17,14 +19,82 @@
1719
*/
1820
trait PropertyLike
1921
{
20-
use VisibilityAware;
21-
22+
/** @var array{'set' => ?string, 'get' => ?string} */
23+
private array $visibility = ['set' => null, 'get' => null];
2224
private bool $readOnly = false;
2325

2426
/** @var array<string, ?PropertyHook> */
2527
private array $hooks = ['set' => null, 'get' => null];
2628

2729

30+
/**
31+
* @param ?string $get public|protected|private
32+
* @param ?string $set public|protected|private
33+
*/
34+
public function setVisibility(?string $get, ?string $set = null): static
35+
{
36+
if (!in_array($get, [Modifier::Public, Modifier::Protected, Modifier::Private, null], true)
37+
|| !in_array($set, [Modifier::Public, Modifier::Protected, Modifier::Private, null], true)) {
38+
throw new Nette\InvalidArgumentException('Argument must be public|protected|private.');
39+
}
40+
41+
$this->visibility = ['set' => $set, 'get' => $get];
42+
return $this;
43+
}
44+
45+
46+
/** @param 'set'|'get' $mode */
47+
public function getVisibility(string $mode = 'get'): ?string
48+
{
49+
return $this->visibility[$this->checkMode($mode)];
50+
}
51+
52+
53+
/** @param 'set'|'get' $mode */
54+
public function setPublic(string $mode = 'get'): static
55+
{
56+
$this->visibility[$this->checkMode($mode)] = Modifier::Public;
57+
return $this;
58+
}
59+
60+
61+
/** @param 'set'|'get' $mode */
62+
public function isPublic(string $mode = 'get'): bool
63+
{
64+
return in_array($this->visibility[$this->checkMode($mode)], [Modifier::Public, null], true);
65+
}
66+
67+
68+
/** @param 'set'|'get' $mode */
69+
public function setProtected(string $mode = 'get'): static
70+
{
71+
$this->visibility[$this->checkMode($mode)] = Modifier::Protected;
72+
return $this;
73+
}
74+
75+
76+
/** @param 'set'|'get' $mode */
77+
public function isProtected(string $mode = 'get'): bool
78+
{
79+
return $this->visibility[$this->checkMode($mode)] === Modifier::Protected;
80+
}
81+
82+
83+
/** @param 'set'|'get' $mode */
84+
public function setPrivate(string $mode = 'get'): static
85+
{
86+
$this->visibility[$this->checkMode($mode)] = Modifier::Private;
87+
return $this;
88+
}
89+
90+
91+
/** @param 'set'|'get' $mode */
92+
public function isPrivate(string $mode = 'get'): bool
93+
{
94+
return $this->visibility[$this->checkMode($mode)] === Modifier::Private;
95+
}
96+
97+
2898
public function setReadOnly(bool $state = true): static
2999
{
30100
$this->readOnly = $state;
@@ -79,4 +149,12 @@ public function hasHook(string|\PropertyHookType $type): bool
79149
{
80150
return isset($this->hooks[$type]);
81151
}
152+
153+
154+
private function checkMode(string $mode): string
155+
{
156+
return $mode === Modifier::Set || $mode === Modifier::Get
157+
? $mode
158+
: throw new Nette\InvalidArgumentException('Argument must be set|get.');
159+
}
82160
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/**
4+
* Test: PropertyLike asymmetric visibility
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\PhpGenerator\ClassType;
10+
use Tester\Assert;
11+
12+
require __DIR__ . '/../bootstrap.php';
13+
14+
15+
$class = new ClassType('Demo');
16+
17+
// Default visibility
18+
$default = $class->addProperty('first')
19+
->setType('string');
20+
Assert::true($default->isPublic('get'));
21+
Assert::true($default->isPublic('set'));
22+
Assert::null($default->getVisibility());
23+
Assert::null($default->getVisibility('set'));
24+
25+
// Public with private setter
26+
$restricted = $class->addProperty('second')
27+
->setType('string')
28+
->setVisibility(null, 'private');
29+
Assert::true($restricted->isPublic());
30+
Assert::false($restricted->isPublic('set'));
31+
Assert::true($restricted->isPrivate('set'));
32+
Assert::null($restricted->getVisibility());
33+
Assert::same('private', $restricted->getVisibility('set'));
34+
35+
// Public with protected setter using individual methods
36+
$mixed = $class->addProperty('third')
37+
->setType('string')
38+
->setPublic()
39+
->setProtected('set');
40+
Assert::true($mixed->isPublic());
41+
Assert::false($mixed->isPublic('set'));
42+
Assert::true($mixed->isProtected('set'));
43+
Assert::same('public', $mixed->getVisibility());
44+
Assert::same('protected', $mixed->getVisibility('set'));
45+
46+
// Protected with private setter
47+
$nested = $class->addProperty('fourth')
48+
->setType('string')
49+
->setProtected()
50+
->setPrivate('set');
51+
Assert::false($nested->isPublic());
52+
Assert::true($nested->isProtected());
53+
Assert::true($nested->isPrivate('set'));
54+
Assert::same('protected', $nested->getVisibility());
55+
Assert::same('private', $nested->getVisibility('set'));
56+
57+
// Test invalid getter visibility
58+
Assert::exception(
59+
fn() => $default->setVisibility('invalid', 'public'),
60+
Nette\InvalidArgumentException::class,
61+
'Argument must be public|protected|private.',
62+
);
63+
64+
// Test invalid setter visibility
65+
Assert::exception(
66+
fn() => $default->setVisibility('public', 'invalid'),
67+
Nette\InvalidArgumentException::class,
68+
'Argument must be public|protected|private.',
69+
);
70+
71+
72+
same(<<<'XX'
73+
class Demo
74+
{
75+
public string $first;
76+
private(set) string $second;
77+
public protected(set) string $third;
78+
protected private(set) string $fourth;
79+
}
80+
81+
XX, (string) $class);

0 commit comments

Comments
 (0)