Skip to content

Commit fe96140

Browse files
authored
Merge pull request #43 from xp-framework/feature/asymmetric-visibility
Add support for asymmetric visibility for properties
2 parents fab6891 + bea9794 commit fe96140

File tree

5 files changed

+217
-33
lines changed

5 files changed

+217
-33
lines changed

src/main/php/lang/reflection/Modifiers.class.php

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,40 @@
11
<?php namespace lang\reflection;
22

3-
use lang\Value;
3+
use lang\{Value, IllegalArgumentException};
44

55
/**
66
* Type and member modifiers
77
*
88
* @test lang.reflection.unittest.ModifiersTest
99
*/
1010
class Modifiers implements Value {
11-
const IS_STATIC = MODIFIER_STATIC;
12-
const IS_ABSTRACT = MODIFIER_ABSTRACT;
13-
const IS_FINAL = MODIFIER_FINAL;
14-
const IS_PUBLIC = MODIFIER_PUBLIC;
15-
const IS_PROTECTED = MODIFIER_PROTECTED;
16-
const IS_PRIVATE = MODIFIER_PRIVATE;
17-
const IS_READONLY = 0x0080; // XP 10.13: MODIFIER_READONLY
18-
const IS_NATIVE = 0xF000;
11+
const IS_STATIC = MODIFIER_STATIC;
12+
const IS_ABSTRACT = MODIFIER_ABSTRACT;
13+
const IS_FINAL = MODIFIER_FINAL;
14+
const IS_PUBLIC = MODIFIER_PUBLIC;
15+
const IS_PROTECTED = MODIFIER_PROTECTED;
16+
const IS_PRIVATE = MODIFIER_PRIVATE;
17+
const IS_READONLY = MODIFIER_READONLY;
18+
const IS_PUBLIC_SET = 0x0400;
19+
const IS_PROTECTED_SET = 0x0800;
20+
const IS_PRIVATE_SET = 0x1000;
21+
const IS_NATIVE = 0x10000;
22+
23+
const GET_MASK = 0x0007; // PUBLIC | PROTECTED | PRIVATE
24+
const SET_MASK = 0x1c00; // PUBLIC_SET | PROTECTED_SET | PRIVATE_SET
1925

2026
private static $names= [
21-
'public' => self::IS_PUBLIC,
22-
'protected' => self::IS_PROTECTED,
23-
'private' => self::IS_PRIVATE,
24-
'static' => self::IS_STATIC,
25-
'final' => self::IS_FINAL,
26-
'abstract' => self::IS_ABSTRACT,
27-
'native' => self::IS_NATIVE,
28-
'readonly' => self::IS_READONLY,
27+
'public' => self::IS_PUBLIC,
28+
'protected' => self::IS_PROTECTED,
29+
'private' => self::IS_PRIVATE,
30+
'static' => self::IS_STATIC,
31+
'final' => self::IS_FINAL,
32+
'abstract' => self::IS_ABSTRACT,
33+
'native' => self::IS_NATIVE,
34+
'readonly' => self::IS_READONLY,
35+
'public(set)' => self::IS_PUBLIC_SET,
36+
'protected(set)' => self::IS_PROTECTED_SET,
37+
'private(set)' => self::IS_PRIVATE_SET,
2938
];
3039
private $bits;
3140

@@ -45,7 +54,7 @@ public function __construct($arg= 0, $visibility= true) {
4554
}
4655

4756
if ($visibility && 0 === ($this->bits & (self::IS_PROTECTED | self::IS_PRIVATE))) {
48-
$this->bits |= self::IS_PUBLIC;
57+
$this->bits|= self::IS_PUBLIC;
4958
}
5059
}
5160

@@ -58,7 +67,7 @@ public function __construct($arg= 0, $visibility= true) {
5867
private static function parse($names) {
5968
$bits= 0;
6069
foreach ($names as $name) {
61-
$bits |= self::$names[$name];
70+
$bits|= self::$names[$name];
6271
}
6372
return $bits;
6473
}
@@ -103,19 +112,55 @@ public function isAbstract() { return 0 !== ($this->bits & self::IS_ABSTRACT); }
103112
public function isFinal() { return 0 !== ($this->bits & self::IS_FINAL); }
104113

105114
/** @return bool */
106-
public function isPublic() { return 0 !== ($this->bits & self::IS_PUBLIC); }
115+
public function isNative() { return 0 !== ($this->bits & self::IS_NATIVE); }
107116

108117
/** @return bool */
109-
public function isProtected() { return 0 !== ($this->bits & self::IS_PROTECTED); }
118+
public function isReadonly() { return 0 !== ($this->bits & self::IS_READONLY); }
110119

111-
/** @return bool */
112-
public function isPrivate() { return 0 !== ($this->bits & self::IS_PRIVATE); }
120+
/**
121+
* Gets whether these modifiers are public in regard to the specified hook
122+
*
123+
* @param string $hook
124+
* @return bool
125+
* @throws lang.IllegalArgumentException
126+
*/
127+
public function isPublic($hook= 'get') {
128+
switch ($hook) {
129+
case 'get': return 0 !== ($this->bits & self::IS_PUBLIC);
130+
case 'set': return 0 !== ($this->bits & self::IS_PUBLIC_SET);
131+
default: throw new IllegalArgumentException('Unknown hook '.$hook);
132+
}
133+
}
113134

114-
/** @return bool */
115-
public function isNative() { return 0 !== ($this->bits & self::IS_NATIVE); }
135+
/**
136+
* Gets whether these modifiers are protected in regard to the specified hook
137+
*
138+
* @param string $hook
139+
* @return bool
140+
* @throws lang.IllegalArgumentException
141+
*/
142+
public function isProtected($hook= 'get') {
143+
switch ($hook) {
144+
case 'get': return 0 !== ($this->bits & self::IS_PROTECTED);
145+
case 'set': return 0 !== ($this->bits & self::IS_PROTECTED_SET);
146+
default: throw new IllegalArgumentException('Unknown hook '.$hook);
147+
}
148+
}
116149

117-
/** @return bool */
118-
public function isReadonly() { return 0 !== ($this->bits & self::IS_READONLY); }
150+
/**
151+
* Gets whether these modifiers are private in regard to the specified hook
152+
*
153+
* @param string $hook
154+
* @return bool
155+
* @throws lang.IllegalArgumentException
156+
*/
157+
public function isPrivate($hook= 'get') {
158+
switch ($hook) {
159+
case 'get': return 0 !== ($this->bits & self::IS_PRIVATE);
160+
case 'set': return 0 !== ($this->bits & self::IS_PRIVATE_SET);
161+
default: throw new IllegalArgumentException('Unknown hook '.$hook);
162+
}
163+
}
119164

120165
/**
121166
* Compares a given value to this modifiers instance

src/main/php/lang/reflection/Property.class.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php namespace lang\reflection;
22

33
use ReflectionException, ReflectionUnionType, Throwable;
4-
use lang\{Reflection, XPClass, Type, VirtualProperty, TypeUnion};
4+
use lang\{Reflection, XPClass, Type, VirtualProperty, TypeUnion, IllegalArgumentException};
55

66
/**
77
* Reflection for a single property
@@ -38,6 +38,32 @@ public function constraint() {
3838
return new Constraint($t ?? Type::$VAR, $present);
3939
}
4040

41+
/**
42+
* Gets whether these modifiers are public in regard to the specified hook
43+
*
44+
* @param ?string $hook Optionally, filter for specified hook only
45+
* @return lang.reflection.Modifiers
46+
* @throws lang.IllegalArgumentException
47+
*/
48+
public function modifiers($hook= null) {
49+
static $set= [
50+
Modifiers::IS_PUBLIC_SET => Modifiers::IS_PUBLIC,
51+
Modifiers::IS_PROTECTED_SET => Modifiers::IS_PROTECTED,
52+
Modifiers::IS_PRIVATE_SET => Modifiers::IS_PRIVATE,
53+
];
54+
55+
// Readonly implies protected(set)
56+
$bits= $this->reflect->getModifiers();
57+
$bits & Modifiers::IS_READONLY && $bits|= Modifiers::IS_PROTECTED_SET;
58+
59+
switch ($hook) {
60+
case null: return new Modifiers($bits);
61+
case 'get': return new Modifiers(($bits & ~Modifiers::SET_MASK) & Modifiers::GET_MASK);
62+
case 'set': return new Modifiers($set[$bits & Modifiers::SET_MASK] ?? $bits & Modifiers::GET_MASK);
63+
default: throw new IllegalArgumentException('Unknown hook '.$hook);
64+
}
65+
}
66+
4167
/**
4268
* Gets this property's value
4369
*
@@ -100,7 +126,9 @@ public function toString() {
100126
$name= $t->getName();
101127
}
102128

103-
return Modifiers::namesOf($this->reflect->getModifiers()).' '.$name.' $'.$this->reflect->getName();
129+
$bits= $this->reflect->getModifiers();
130+
$bits & Modifiers::IS_READONLY && $bits|= Modifiers::IS_PROTECTED_SET;
131+
return Modifiers::namesOf($bits).' '.$name.' $'.$this->reflect->getName();
104132
}
105133

106134
/**

src/test/php/lang/reflection/unittest/ModifiersTest.class.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ private function cases() {
1515
yield [Modifiers::IS_PRIVATE, 'private'];
1616
yield [Modifiers::IS_NATIVE, 'native'];
1717
yield [Modifiers::IS_READONLY, 'readonly'];
18+
yield [Modifiers::IS_PRIVATE_SET, 'private(set)'];
19+
yield [Modifiers::IS_PROTECTED_SET, 'protected(set)'];
20+
yield [Modifiers::IS_PUBLIC_SET, 'public(set)'];
1821
yield [Modifiers::IS_FINAL | Modifiers::IS_PUBLIC, 'public final'];
1922
yield [Modifiers::IS_ABSTRACT | Modifiers::IS_PUBLIC, 'public abstract'];
2023
yield [Modifiers::IS_ABSTRACT | Modifiers::IS_PROTECTED, 'protected abstract'];
@@ -83,6 +86,36 @@ public function isReadonly($input, $expected) {
8386
Assert::equals($expected, (new Modifiers($input))->isReadonly());
8487
}
8588

89+
#[Test, Values([['public(set)', true], ['public', true]])]
90+
public function isPublicGet($input, $expected) {
91+
Assert::equals($expected, (new Modifiers($input))->isPublic('get'));
92+
}
93+
94+
#[Test, Values([['protected(set)', false], ['protected', true]])]
95+
public function isProtectedGet($input, $expected) {
96+
Assert::equals($expected, (new Modifiers($input))->isProtected('get'));
97+
}
98+
99+
#[Test, Values([['private(set)', false], ['private', true]])]
100+
public function isPrivateGet($input, $expected) {
101+
Assert::equals($expected, (new Modifiers($input))->isPrivate('get'));
102+
}
103+
104+
#[Test, Values([['public(set)', true], ['public', false]])]
105+
public function isPublicSet($input, $expected) {
106+
Assert::equals($expected, (new Modifiers($input))->isPublic('set'));
107+
}
108+
109+
#[Test, Values([['protected(set)', true], ['protected', false]])]
110+
public function isProtectedSet($input, $expected) {
111+
Assert::equals($expected, (new Modifiers($input))->isProtected('set'));
112+
}
113+
114+
#[Test, Values([['private(set)', true], ['private', false]])]
115+
public function isPrivateSet($input, $expected) {
116+
Assert::equals($expected, (new Modifiers($input))->isPrivate('set'));
117+
}
118+
86119
#[Test]
87120
public function public_modifier_default_no_arg() {
88121
Assert::true((new Modifiers())->isPublic());

src/test/php/lang/reflection/unittest/PropertiesTest.class.php

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
<?php namespace lang\reflection\unittest;
22

3+
use ReflectionProperty;
34
use lang\reflection\{AccessingFailed, CannotAccess, Constraint, Modifiers};
4-
use lang\{Primitive, Type, TypeIntersection, TypeUnion, XPClass, IllegalStateException};
5-
use test\verify\Runtime;
5+
use lang\{Primitive, Type, TypeIntersection, TypeUnion, XPClass, IllegalArgumentException};
6+
use test\verify\{Condition, Runtime};
67
use test\{Action, Assert, Expect, Test, Values};
78

89
class PropertiesTest {
910
use TypeDefinition;
1011

12+
private static $ASYMMETRIC;
13+
14+
static function __static() {
15+
self::$ASYMMETRIC= method_exists(ReflectionProperty::class, 'isPrivateSet');
16+
}
17+
1118
#[Test]
1219
public function name() {
1320
Assert::equals('fixture', $this->declare('{ public $fixture; }')->property('fixture')->name());
@@ -27,6 +34,43 @@ public function modifiers() {
2734
);
2835
}
2936

37+
#[Test, Values(['public', 'protected', 'private'])]
38+
public function get_modifiers($modifier) {
39+
Assert::equals(
40+
new Modifiers($modifier),
41+
$this->declare('{ '.$modifier.' $fixture; }')->property('fixture')->modifiers('get')
42+
);
43+
}
44+
45+
#[Test, Values(['public', 'protected', 'private'])]
46+
public function set_modifiers($modifier) {
47+
Assert::equals(
48+
new Modifiers($modifier),
49+
$this->declare('{ '.$modifier.' $fixture; }')->property('fixture')->modifiers('set')
50+
);
51+
}
52+
53+
#[Test]
54+
public function get_modifiers_erases_static() {
55+
Assert::equals(
56+
new Modifiers('public'),
57+
$this->declare('{ public static int $fixture; }')->property('fixture')->modifiers('get')
58+
);
59+
}
60+
61+
#[Test]
62+
public function set_modifiers_erases_static() {
63+
Assert::equals(
64+
new Modifiers('public'),
65+
$this->declare('{ public static int $fixture; }')->property('fixture')->modifiers('set')
66+
);
67+
}
68+
69+
#[Test, Expect(IllegalArgumentException::class)]
70+
public function modifiers_unknown_hook() {
71+
$this->declare('{ private $fixture; }')->property('fixture')->modifiers('@unknown');
72+
}
73+
3074
#[Test]
3175
public function no_comment() {
3276
Assert::null($this->declare('{ private $fixture; }')->property('fixture')->comment());
@@ -227,4 +271,38 @@ public function set_accessing_failed_exceptions_target_member() {
227271
Assert::equals($t->property('fixture'), $expected->target());
228272
}
229273
}
274+
275+
#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public protected(set)', 'public private(set)', 'protected private(set)'])]
276+
public function asymmetric_visibility($modifiers) {
277+
$t= $this->declare('{ '.$modifiers.' int $fixture; }');
278+
Assert::equals(
279+
$modifiers.' int $fixture',
280+
$t->property('fixture')->toString()
281+
);
282+
}
283+
284+
#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public', 'protected', 'private'])]
285+
public function set_implicit_when_same_as_get($modifier) {
286+
$t= $this->declare('{ '.$modifier.' '.$modifier.'(set) int $fixture; }');
287+
Assert::equals(
288+
$modifier.' int $fixture',
289+
$t->property('fixture')->toString()
290+
);
291+
}
292+
293+
#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public', 'protected', 'private'])]
294+
public function asymmetric_get($modifier) {
295+
Assert::equals(
296+
new Modifiers($modifier),
297+
$this->declare('{ '.$modifier.' private(set) int $fixture; }')->property('fixture')->modifiers('get')
298+
);
299+
}
300+
301+
#[Test, Condition(assert: 'self::$ASYMMETRIC'), Values(['public', 'protected', 'private'])]
302+
public function asymmetric_set($modifier) {
303+
Assert::equals(
304+
new Modifiers($modifier),
305+
$this->declare('{ public '.$modifier.'(set) int $fixture; }')->property('fixture')->modifiers('set')
306+
);
307+
}
230308
}

src/test/php/lang/reflection/unittest/VirtualPropertiesTest.class.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ private function fixtures() {
2525

2626
#[Test, Values(from: 'fixtures')]
2727
public function readonly_modifier_shown_in_string_representation($type) {
28-
Assert::equals('public readonly string $fixture', $type->property('fixture')->toString());
28+
Assert::equals('public readonly protected(set) string $fixture', $type->property('fixture')->toString());
2929
}
3030

3131
#[Test, Values(from: 'fixtures')]
3232
public function virtual_property_included_in_list($type) {
3333
Assert::equals(
34-
['fixture' => 'public readonly'],
34+
['fixture' => 'public readonly protected(set)'],
3535
array_map(fn($p) => $p->modifiers()->names(), iterator_to_array($type->properties()))
3636
);
3737
}

0 commit comments

Comments
 (0)