Skip to content

Commit 9fa3236

Browse files
committed
added support for asymmetric visibility
1 parent b9b02a3 commit 9fa3236

File tree

5 files changed

+227
-4
lines changed

5 files changed

+227
-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(PropertyAccessMode::Get);
408+
$set = $param->getVisibility(PropertyAccessMode::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) {
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\PhpGenerator;
11+
12+
use Nette;
13+
14+
15+
/**
16+
* Property access mode.
17+
*/
18+
/*enum*/ final class PropertyAccessMode
19+
{
20+
use Nette\StaticClass;
21+
22+
public const Set = 'set';
23+
public const Get = 'get';
24+
25+
26+
/** @internal */
27+
public static function from(string $value): string
28+
{
29+
return $value === self::Set || $value === self::Get
30+
? $value
31+
: throw new \ValueError("'$value' is not a valid value of access mode");
32+
}
33+
}

src/PhpGenerator/Traits/PropertyLike.php

+70-2
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,91 @@
99

1010
namespace Nette\PhpGenerator\Traits;
1111

12+
use Nette\PhpGenerator\PropertyAccessMode;
1213
use Nette\PhpGenerator\PropertyHook;
1314
use Nette\PhpGenerator\PropertyHookType;
15+
use Nette\PhpGenerator\Visibility;
1416

1517

1618
/**
1719
* @internal
1820
*/
1921
trait PropertyLike
2022
{
21-
use VisibilityAware;
22-
23+
/** @var array{'set' => ?string, 'get' => ?string} */
24+
private array $visibility = [PropertyAccessMode::Set => null, PropertyAccessMode::Get => null];
2325
private bool $readOnly = false;
2426

2527
/** @var array<string, ?PropertyHook> */
2628
private array $hooks = [PropertyHookType::Set => null, PropertyHookType::Get => null];
2729

2830

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

0 commit comments

Comments
 (0)