Skip to content

Commit 4580eba

Browse files
authored
Merge pull request #275 from thekid/rfc/340
Add support for virtual properties
2 parents 793fa96 + 123f4a4 commit 4580eba

File tree

7 files changed

+369
-48
lines changed

7 files changed

+369
-48
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php namespace lang;
2+
3+
use ReflectionProperty, ReflectionType, ReflectionClass, ReturnTypeWillChange;
4+
5+
/**
6+
* Virtual properties
7+
*
8+
* Note the "name" and "class" members are not available as they cannot
9+
* be assigned in inherited classes.
10+
*
11+
* @test net.xp_framework.unittest.reflection.VirtualMembersTest
12+
* @see https://github.com/xp-framework/rfc/issues/340
13+
* @see https://www.php.net/language.oop5.overloading#object.get
14+
* @see https://www.php.net/language.oop5.overloading#object.set
15+
* @see https://docs.phpdoc.org/latest/guide/references/phpdoc/tags/property.html
16+
*/
17+
class VirtualProperty extends ReflectionProperty {
18+
private $_class, $_name, $_meta;
19+
20+
/**
21+
* Creates a new virtual property
22+
*
23+
* @param \ReflectionClass $class
24+
* @param string $name
25+
* @param var[] $meta
26+
*/
27+
public function __construct($class, $name, $meta= [MODIFIER_PUBLIC, null]) {
28+
$this->_class= $class;
29+
$this->_name= $name;
30+
$this->_meta= $meta;
31+
}
32+
33+
/** Gets name */
34+
public function getName(): string { return $this->_name; }
35+
36+
/** Gets modifiers */
37+
public function getModifiers(): int { return $this->_meta[0]; }
38+
39+
/** Checks if property has a type */
40+
public function hasType(): bool { return isset($this->_meta[1]); }
41+
42+
/** Gets type */
43+
public function getType(): ReflectionType {
44+
return new class($this->_meta[1]) extends ReflectionType {
45+
private $_name;
46+
public function __construct($name) { $this->_name= $name; }
47+
public function getName(): string { return $this->_name; }
48+
public function allowsNull(): bool { return false; }
49+
public function __toString() { return $this->_name; }
50+
};
51+
}
52+
53+
/** Gets declaring class */
54+
public function getDeclaringClass(): ReflectionClass { return $this->_class; }
55+
56+
/** Gets doc comment */
57+
#[ReturnTypeWillChange]
58+
public function getDocComment() { return false; }
59+
60+
/** Gets whether a default value is available */
61+
public function hasDefaultValue(): bool { return false; }
62+
63+
/** Gets default value */
64+
#[ReturnTypeWillChange]
65+
public function getDefaultValue() { return null; }
66+
67+
/** Checks if property is a default property */
68+
public function isDefault(): bool { return false; }
69+
70+
/** Returns whether this property is public */
71+
public function isPublic(): bool { return MODIFIER_PUBLIC === ($this->_meta[0] & MODIFIER_PUBLIC); }
72+
73+
/** Returns whether this property is protected */
74+
public function isProtected(): bool { return MODIFIER_PROTECTED === ($this->_meta[0] & MODIFIER_PROTECTED); }
75+
76+
/** Returns whether this property is private */
77+
public function isPrivate(): bool { return MODIFIER_PRIVATE === ($this->_meta[0] & MODIFIER_PRIVATE); }
78+
79+
/** Returns whether this property is private */
80+
public function isStatic(): bool { return MODIFIER_STATIC === ($this->_meta[0] & MODIFIER_STATIC); }
81+
82+
/**
83+
* Returns all attributes declared on this class property as an array of ReflectionAttribute.
84+
*
85+
* @param ?string $name
86+
* @param int $flags
87+
* @return \ReflectionAttribute[]
88+
*/
89+
public function getAttributes(string $name= null, int $flags= 0): array {
90+
return [];
91+
}
92+
93+
/**
94+
* Sets accessible flag
95+
*
96+
* @param bool $flag
97+
* @return void
98+
*/
99+
#[ReturnTypeWillChange]
100+
public function setAccessible($flag) { /* NOOP */ }
101+
102+
/**
103+
* Checks whether a property is initialized.
104+
*
105+
* @param ?object $instance
106+
* @return bool
107+
*/
108+
public function isInitialized($instance= null): bool {
109+
return null !== $instance->__get($this->_name);
110+
}
111+
112+
/**
113+
* Gets this property's value
114+
*
115+
* @param ?object $instance
116+
* @return var
117+
*/
118+
#[ReturnTypeWillChange]
119+
public function getValue($instance= null) {
120+
return $instance->__get($this->_name);
121+
}
122+
123+
/**
124+
* Sets this property's value
125+
*
126+
* @param ?object $instance
127+
* @param var $value
128+
* @return void
129+
*/
130+
#[ReturnTypeWillChange]
131+
public function setValue($instance= null, $value= null) {
132+
$instance->__set($this->_name, $value);
133+
}
134+
135+
/**
136+
* Compares to property instances
137+
*
138+
* @param \ReflectionProperty $a
139+
* @param \ReflectionProperty $b
140+
* @return int
141+
*/
142+
public static function compare($a, $b) {
143+
if ($a instanceof self && $b instanceof self) {
144+
$r= $a->_class <=> $b->_class;
145+
return 0 === $r ? $a->_name <=> $b->_name : $r;
146+
} else if ($a instanceof self) {
147+
$r= $a->_class <=> $b->class;
148+
return 0 === $r ? $a->_name <=> $b->name : $r;
149+
} else if ($b instanceof self) {
150+
$r= $a->class <=> $b->_class;
151+
return 0 === $r ? $a->name <=> $b->_name : $r;
152+
} else {
153+
$r= $a->class <=> $b->class;
154+
return 0 === $r ? $a->name <=> $b->name : $r;
155+
}
156+
}
157+
}

src/main/php/lang/XPClass.class.php

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -273,58 +273,109 @@ public function getConstructor(): Constructor {
273273
}
274274
throw new ElementNotFoundException('No constructor in class '.$this->name);
275275
}
276-
276+
277+
/**
278+
* Returns virtual properties
279+
*
280+
* @param ReflectionClass $reflect
281+
* @param bool $parents
282+
* @return [:var][]
283+
*/
284+
private function virtual($reflect, $parents= true) {
285+
$r= [];
286+
do {
287+
288+
// If meta information is already loaded, use property arguments
289+
if ($meta= \xp::$meta[self::nameOf($reflect->name)][0] ?? null) {
290+
foreach ($meta as $name => $property) {
291+
if ($arg= $property[DETAIL_ARGUMENTS] ?? null) {
292+
$r[$name]= [(int)$arg[0], $property[DETAIL_RETURNS] ?? 'mixed'];
293+
}
294+
}
295+
continue;
296+
}
297+
298+
// Parse doc comment
299+
$comment= $reflect->getDocComment();
300+
if (null === $comment) continue;
301+
302+
preg_match_all('/@property(\-read|\-write)? (.+) \$([^ ]+)/', $comment, $matches, PREG_SET_ORDER);
303+
$r= [];
304+
foreach ($matches as $match) {
305+
$r[$match[3]]= ['-read' === $match[1] ? MODIFIER_READONLY : 0, $match[2]];
306+
}
307+
} while ($parents && ($reflect= $reflect->getParentclass()));
308+
309+
return $r;
310+
}
311+
277312
/**
278313
* Retrieve a list of all member variables
279314
*
280-
* @return lang.reflect.Field[]
315+
* @return lang.reflect.Field[]
281316
*/
282317
public function getFields() {
318+
$reflect= $this->reflect();
319+
283320
$f= [];
284-
foreach ($this->reflect()->getProperties() as $p) {
321+
foreach ($reflect->getProperties() as $p) {
285322
if ('__id' === $p->name) continue;
286323
$f[]= new Field($this->_class, $p);
287324
}
325+
foreach ($this->virtual($reflect) as $name => $meta) {
326+
$f[]= new Field($this->_class, new VirtualProperty($reflect, $name, $meta));
327+
}
288328
return $f;
289329
}
290330

291331
/**
292332
* Retrieve a list of member variables declared in this class
293333
*
294-
* @return lang.reflect.Field[]
334+
* @return lang.reflect.Field[]
295335
*/
296336
public function getDeclaredFields() {
297-
$list= [];
298337
$reflect= $this->reflect();
338+
339+
$f= [];
299340
foreach ($reflect->getProperties() as $p) {
300341
if ('__id' === $p->name || $p->class !== $reflect->name) continue;
301-
$list[]= new Field($this->_class, $p);
342+
$f[]= new Field($this->_class, $p);
302343
}
303-
return $list;
344+
foreach ($this->virtual($reflect, false) as $name => $meta) {
345+
$f[]= new Field($this->_class, new VirtualProperty($reflect, $name, $meta));
346+
}
347+
return $f;
304348
}
305349

306350
/**
307351
* Retrieve a field by a specified name.
308352
*
309-
* @param string name
310-
* @return lang.reflect.Field
311-
* @throws lang.ElementNotFoundException
353+
* @param string $name
354+
* @return lang.reflect.Field
355+
* @throws lang.ElementNotFoundException
312356
*/
313357
public function getField($name): Field {
314-
if ($this->hasField($name)) {
315-
return new Field($this->_class, $this->reflect()->getProperty($name));
358+
$reflect= $this->reflect();
359+
if ($reflect->hasProperty($name)) {
360+
return new Field($this->_class, $reflect->getProperty($name));
361+
} else if ($meta= $this->virtual($reflect)[$name] ?? null) {
362+
return new Field($this->_class, new VirtualProperty($reflect, $name, $meta));
316363
}
364+
317365
throw new ElementNotFoundException('No such field "'.$name.'" in class '.$this->name);
318366
}
319367

320368
/**
321-
* Checks whether this class has a field named "$field" or not.
369+
* Checks whether this class has a field with a given name
322370
*
323-
* @param string field the fields's name
324-
* @return bool TRUE if field exists
371+
* @param string $name
372+
* @return bool TRUE if field exists
325373
*/
326-
public function hasField($field): bool {
327-
return '__id' == $field ? false : $this->reflect()->hasProperty($field);
374+
public function hasField($name): bool {
375+
return '__id' === $name ? false : ($reflect= $this->reflect()) &&
376+
$reflect->hasProperty($name) ||
377+
isset($this->virtual($reflect)[$name])
378+
;
328379
}
329380

330381
/**

src/main/php/lang/reflect/Field.class.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public function getType(): Type {
5757
$r= $details[DETAIL_RETURNS] ?? $details[DETAIL_ANNOTATIONS]['type'] ?? null;
5858
return $r ? ltrim($r, '&') : null;
5959
};
60-
$t= PHP_VERSION_ID >= 70400 ? $this->_reflect->getType() : null;
60+
61+
$t= PHP_VERSION_ID >= 70400 || '' === $this->_reflect->name ? $this->_reflect->getType() : null;
6162
return Type::resolve($t, $this->resolve(), $api) ?? Type::$VAR;
6263
}
6364

@@ -71,7 +72,7 @@ public function getTypeName(): string {
7172
'integer' => 'int',
7273
];
7374

74-
$t= PHP_VERSION_ID >= 70400 ? $this->_reflect->getType() : null;
75+
$t= PHP_VERSION_ID >= 70400 || '' === $this->_reflect->name ? $this->_reflect->getType() : null;
7576
if (null === $t) {
7677

7778
// Check for type in api documentation
@@ -116,7 +117,7 @@ public function getTypeName(): string {
116117
*/
117118
public function getTypeRestriction() {
118119
try {
119-
return Type::resolve(PHP_VERSION_ID >= 70400 ? $this->_reflect->getType() : null, $this->resolve());
120+
return Type::resolve(PHP_VERSION_ID >= 70400 || '' === $this->_reflect->name ? $this->_reflect->getType() : null, $this->resolve());
120121
} catch (ClassLoadingException $e) {
121122
throw new ClassNotFoundException(sprintf(
122123
'Typehint for %s::%s()\'s parameter "%s" cannot be resolved: %s',

0 commit comments

Comments
 (0)