Skip to content

Commit e833b03

Browse files
committed
feat(zend, reflection): expose direct-ancestor type arguments on ReflectionClass
Signed-off-by: azjezz <azjezz@protonmail.com>
1 parent e674e30 commit e833b03

14 files changed

Lines changed: 446 additions & 9 deletions

UPGRADING

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ PHP 8.6 UPGRADE NOTES
262262
ReflectionFunction, ReflectionMethod, closures, and arrow functions).
263263
. Added ReflectionClass::isGeneric() and
264264
ReflectionClass::getGenericParameters().
265+
. Added ReflectionClass::getGenericArgumentsForParentClass(),
266+
ReflectionClass::getGenericArgumentsForParentInterface(string $name),
267+
and ReflectionClass::getGenericArgumentsForUsedTrait(string $name) for
268+
inspecting the type arguments supplied at a class's own extends /
269+
implements / use sites. Returns null when no type arguments were
270+
specified for that ancestor at this class's clause site (consumers
271+
enumerate ancestors via the existing getParentClass() / getInterfaces()
272+
/ getTraits() APIs).
265273
. Added ReflectionNamedType::hasGenericArguments() and
266274
ReflectionNamedType::getGenericArguments(). The arguments are returned
267275
as ReflectionType instances in source order (pre-erasure form);

Zend/zend_compile.c

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9872,15 +9872,24 @@ static void zend_compile_use_trait(const zend_ast *ast) /* {{{ */
98729872
zend_ast *trait_ast = traits->child[i];
98739873

98749874
if (ce->ce_flags & ZEND_ACC_INTERFACE) {
9875-
zend_string *name = zend_ast_get_str(trait_ast);
9875+
zend_string *name = trait_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE
9876+
? zend_ast_get_str(trait_ast->child[0])
9877+
: zend_ast_get_str(trait_ast);
98769878
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use traits inside of interfaces. "
98779879
"%s is used in %s", ZSTR_VAL(name), ZSTR_VAL(ce->name));
98789880
}
98799881

9880-
ce->trait_names[ce->num_traits].name =
9882+
uint32_t trait_index = ce->num_traits;
9883+
ce->trait_names[trait_index].name =
98819884
zend_resolve_const_class_name_reference(trait_ast, "trait name");
9882-
ce->trait_names[ce->num_traits].lc_name = zend_string_tolower(ce->trait_names[ce->num_traits].name);
9885+
ce->trait_names[trait_index].lc_name = zend_string_tolower(ce->trait_names[trait_index].name);
98839886
ce->num_traits++;
9887+
9888+
if (trait_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) {
9889+
zend_type pre = zend_compile_pre_erasure_typename(trait_ast);
9890+
zend_generic_type_table_set_trait_use(
9891+
zend_generic_get_or_create_class_table(ce), trait_index, pre);
9892+
}
98849893
}
98859894

98869895
if (!adaptations) {
@@ -9916,6 +9925,12 @@ static void zend_compile_implements(zend_ast *ast) /* {{{ */
99169925
interface_names[i].name =
99179926
zend_resolve_const_class_name_reference(class_ast, "interface name");
99189927
interface_names[i].lc_name = zend_string_tolower(interface_names[i].name);
9928+
9929+
if (class_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) {
9930+
zend_type pre = zend_compile_pre_erasure_typename(class_ast);
9931+
zend_generic_type_table_set_implements(
9932+
zend_generic_get_or_create_class_table(ce), i, pre);
9933+
}
99199934
}
99209935

99219936
ce->num_interfaces = list->children;
@@ -10055,6 +10070,15 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top
1005510070
if (extends_ast) {
1005610071
ce->parent_name =
1005710072
zend_resolve_const_class_name_reference(extends_ast, "class name");
10073+
/* Capture the pre-erasure type when the extends clause specifies type
10074+
* arguments (e.g. `extends Box<int>`). For an interface declaration
10075+
* this AST holds the list of parent interfaces; for class/trait it's
10076+
* the single parent class. */
10077+
if (extends_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) {
10078+
zend_type pre = zend_compile_pre_erasure_typename(extends_ast);
10079+
zend_generic_type_table_set_extends(
10080+
zend_generic_get_or_create_class_table(ce), pre);
10081+
}
1005810082
}
1005910083

1006010084
CG(active_class_entry) = ce;

ext/reflection/php_reflection.c

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8665,6 +8665,97 @@ ZEND_METHOD(ReflectionNamedType, getGenericArguments)
86658665
}
86668666
}
86678667

8668+
static void reflection_build_named_args_list(zval *return_value, const zend_type *boxed,
8669+
zend_class_entry *declaring_class)
8670+
{
8671+
if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) {
8672+
RETURN_NULL();
8673+
}
8674+
8675+
zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed);
8676+
array_init_size(return_value, named->count);
8677+
for (uint32_t i = 0; i < named->count; i++) {
8678+
zval entry;
8679+
reflection_type_factory_ex(named->args[i], &entry, false,
8680+
(zend_type) ZEND_TYPE_INIT_NONE(0),
8681+
declaring_class, NULL);
8682+
zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry);
8683+
}
8684+
}
8685+
8686+
ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentClass)
8687+
{
8688+
reflection_object *intern;
8689+
zend_class_entry *ce;
8690+
8691+
ZEND_PARSE_PARAMETERS_NONE();
8692+
GET_REFLECTION_OBJECT_PTR(ce);
8693+
8694+
if (!ce->generic_types || !ce->generic_types->extends) {
8695+
RETURN_NULL();
8696+
}
8697+
8698+
reflection_build_named_args_list(return_value, ce->generic_types->extends, ce);
8699+
}
8700+
8701+
ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentInterface)
8702+
{
8703+
reflection_object *intern;
8704+
zend_class_entry *ce;
8705+
zend_string *name;
8706+
8707+
ZEND_PARSE_PARAMETERS_START(1, 1)
8708+
Z_PARAM_STR(name)
8709+
ZEND_PARSE_PARAMETERS_END();
8710+
GET_REFLECTION_OBJECT_PTR(ce);
8711+
8712+
if (!ce->generic_types || !ce->generic_types->implements) {
8713+
RETURN_NULL();
8714+
}
8715+
8716+
zval *zv;
8717+
ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) {
8718+
zend_type *boxed = (zend_type *) Z_PTR_P(zv);
8719+
if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) {
8720+
zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed);
8721+
if (zend_string_equals_ci(named->name, name)) {
8722+
reflection_build_named_args_list(return_value, boxed, ce);
8723+
return;
8724+
}
8725+
}
8726+
} ZEND_HASH_FOREACH_END();
8727+
RETURN_NULL();
8728+
}
8729+
8730+
ZEND_METHOD(ReflectionClass, getGenericArgumentsForUsedTrait)
8731+
{
8732+
reflection_object *intern;
8733+
zend_class_entry *ce;
8734+
zend_string *name;
8735+
8736+
ZEND_PARSE_PARAMETERS_START(1, 1)
8737+
Z_PARAM_STR(name)
8738+
ZEND_PARSE_PARAMETERS_END();
8739+
GET_REFLECTION_OBJECT_PTR(ce);
8740+
8741+
if (!ce->generic_types || !ce->generic_types->trait_uses) {
8742+
RETURN_NULL();
8743+
}
8744+
8745+
zval *zv;
8746+
ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) {
8747+
zend_type *boxed = (zend_type *) Z_PTR_P(zv);
8748+
if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) {
8749+
zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed);
8750+
if (zend_string_equals_ci(named->name, name)) {
8751+
reflection_build_named_args_list(return_value, boxed, ce);
8752+
return;
8753+
}
8754+
}
8755+
} ZEND_HASH_FOREACH_END();
8756+
RETURN_NULL();
8757+
}
8758+
86688759
PHP_MINIT_FUNCTION(reflection) /* {{{ */
86698760
{
86708761
memcpy(&reflection_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));

ext/reflection/php_reflection.stub.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,35 @@ public function isGeneric(): bool {}
446446

447447
/** @return list<ReflectionGenericTypeParameter> */
448448
public function getGenericParameters(): array {}
449+
450+
/**
451+
* Returns the type arguments this class supplies at the parent-class extends
452+
* site, in source order. Returns null if there is no parent class, or if
453+
* the extends clause specified no type arguments.
454+
*
455+
* @return list<ReflectionType>|null
456+
*/
457+
public function getGenericArgumentsForParentClass(): ?array {}
458+
459+
/**
460+
* Returns the type arguments this class supplies for the named parent
461+
* interface (a directly-listed `implements` entry, or, for an interface
462+
* declaration, a directly-listed `extends` entry), in source order.
463+
* Returns null if $name is not a directly-listed parent interface, or if
464+
* no type arguments were specified at the use site.
465+
*
466+
* @return list<ReflectionType>|null
467+
*/
468+
public function getGenericArgumentsForParentInterface(string $name): ?array {}
469+
470+
/**
471+
* Returns the type arguments this class supplies at the use site for trait
472+
* $name, in source order. Returns null if $name is not a directly-used
473+
* trait, or if no type arguments were specified at the use site.
474+
*
475+
* @return list<ReflectionType>|null
476+
*/
477+
public function getGenericArgumentsForUsedTrait(string $name): ?array {}
449478
}
450479

451480
class ReflectionObject extends ReflectionClass

ext/reflection/php_reflection_arginfo.h

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/reflection/php_reflection_decl.h

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/reflection/tests/ReflectionClass_toString_001.phpt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Class [ <internal:Reflection> class ReflectionClass implements Stringable, Refle
3030
Property [ public string $name ]
3131
}
3232

33-
- Methods [66] {
33+
- Methods [69] {
3434
Method [ <internal:Reflection> private method __clone ] {
3535

3636
- Parameters [0] {
@@ -528,5 +528,28 @@ Class [ <internal:Reflection> class ReflectionClass implements Stringable, Refle
528528
}
529529
- Return [ array ]
530530
}
531+
532+
Method [ <internal:Reflection> public method getGenericArgumentsForParentClass ] {
533+
534+
- Parameters [0] {
535+
}
536+
- Return [ ?array ]
537+
}
538+
539+
Method [ <internal:Reflection> public method getGenericArgumentsForParentInterface ] {
540+
541+
- Parameters [1] {
542+
Parameter #0 [ <required> string $name ]
543+
}
544+
- Return [ ?array ]
545+
}
546+
547+
Method [ <internal:Reflection> public method getGenericArgumentsForUsedTrait ] {
548+
549+
- Parameters [1] {
550+
Parameter #0 [ <required> string $name ]
551+
}
552+
- Return [ ?array ]
553+
}
531554
}
532555
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Reflection: composite (union/intersection) type args inside extends/implements
3+
--FILE--
4+
<?php
5+
class A {}
6+
class B {}
7+
class Pair<X, Y> {}
8+
9+
class C extends Pair<A|B, A&B> {}
10+
11+
$args = (new ReflectionClass('C'))->getGenericArgumentsForParentClass();
12+
echo "count: ", count($args), "\n";
13+
echo "[0] kind: ", get_class($args[0]), "\n";
14+
echo "[1] kind: ", get_class($args[1]), "\n";
15+
?>
16+
--EXPECT--
17+
count: 2
18+
[0] kind: ReflectionUnionType
19+
[1] kind: ReflectionIntersectionType
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Reflection: nested generic arguments inside extends/implements/use are preserved
3+
--FILE--
4+
<?php
5+
class Box<T> {}
6+
class Pair<K, V> {}
7+
interface I<X> {}
8+
trait T1<U> { public function noop(): void {} }
9+
10+
class WithNested
11+
extends Pair<Box<int>, string>
12+
implements I<Box<float>>
13+
{
14+
use T1<Box<bool>>;
15+
}
16+
17+
$rc = new ReflectionClass('WithNested');
18+
19+
// Nested in extends
20+
$args = $rc->getGenericArgumentsForParentClass();
21+
echo "parent[0]: ", $args[0]->getName();
22+
echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n";
23+
echo "parent[1]: ", $args[1]->getName(), "\n";
24+
25+
// Nested in implements
26+
$args = $rc->getGenericArgumentsForParentInterface('I');
27+
echo "I[0]: ", $args[0]->getName();
28+
echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n";
29+
30+
// Nested in use
31+
$args = $rc->getGenericArgumentsForUsedTrait('T1');
32+
echo "T1[0]: ", $args[0]->getName();
33+
echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n";
34+
?>
35+
--EXPECT--
36+
parent[0]: Box<int>
37+
parent[1]: string
38+
I[0]: Box<float>
39+
T1[0]: Box<bool>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
Reflection: classes with no generic content return null from all three accessors
3+
--FILE--
4+
<?php
5+
class Plain {}
6+
class WithParent extends Plain {}
7+
interface IPlain {}
8+
class WithIface implements IPlain {}
9+
trait TPlain { public function noop(): void {} }
10+
class WithTrait { use TPlain; }
11+
12+
foreach (['Plain', 'WithParent', 'WithIface', 'WithTrait'] as $cls) {
13+
$rc = new ReflectionClass($cls);
14+
$p = $rc->getGenericArgumentsForParentClass();
15+
$i = $rc->getGenericArgumentsForParentInterface('IPlain');
16+
$t = $rc->getGenericArgumentsForUsedTrait('TPlain');
17+
echo $cls, ": parent=", ($p === null ? "null" : "non-null"),
18+
" iface=", ($i === null ? "null" : "non-null"),
19+
" trait=", ($t === null ? "null" : "non-null"), "\n";
20+
}
21+
?>
22+
--EXPECT--
23+
Plain: parent=null iface=null trait=null
24+
WithParent: parent=null iface=null trait=null
25+
WithIface: parent=null iface=null trait=null
26+
WithTrait: parent=null iface=null trait=null

0 commit comments

Comments
 (0)