Repository navigation
fix(introspection): hide compiler-internal members, skip virtual properties in var_dump, add ReflectionType and SessionHandlerInterface - #1923
Open
JGamboa wants to merge 9 commits into
Conversation
Contributor
[Medium impact] The PR appears safe to merge; the remaining trait-name conflict is fixed. SummaryThe PR hides compiler-only members from reflection, skips virtual properties in
Reviews (4) · Last reviewed commit: "fix(checker): reject a trait named Sessi..." · Reviewed by Greptile |
…sors
PHP's Reflection classes declare no private properties, and property-hook
accessors are not PHP methods, but elephc's Reflection listed both:
- `getProperties()`, `hasProperty()` and `getDefaultProperties()` on a
builtin Reflection class (or a user subclass of one) listed the ~36
private `__*` storage slots elephc declares on it. Since every
ReflectionMethod embeds a declaring-class ReflectionClass,
`(new ReflectionClass('Mine'))->getProperties()` over
`class Mine extends ReflectionClass {}` exhausted the default heap.
- `getMethods()` and `hasMethod()` listed the generated
`__propget_*` / `__propset_*` accessors of hooked properties.
- the statically expanded `getProperties()` (receiver known at lowering
time) applied no visibility rule at all, so it also listed a parent's
private properties, which PHP never reports.
The property rule now lives in `ClassInfo::reflection_lists_property` and is
shared by the codegen metadata and the static EIR expansion; method lists
skip hook accessors, resolved on the declaring class. Reflection inside
eval() fragments does not apply these filters yet (documented).
…red_classes()
The WeakMap prelude declares `__ElephcWeakMapIterator` as a program
statement, so the source-order half of the declared-class list exposed it:
$map = new WeakMap();
var_dump(in_array('__ElephcWeakMapIterator', get_declared_classes()));
// bool(true) before, bool(false) in PHP
Code that instantiates every declared class (php-src
Zend/tests/instantiate_all_classes.phpt) then reached the internal iterator
without its map and stopped on "Typed property
__ElephcWeakMapIterator::$__elephc_map must not be accessed before
initialization". The source-order names now go
through the same internal-name filter as the registered ones.
A virtual hooked property (hooks that never touch a backing slot) has no
storage, and PHP's var_dump() does not render it. elephc emitted a row for
it that read the layout's fallback offset:
class A {
private string $n = 'x';
public string $name { get => $this->n; }
}
var_dump(new A());
// elephc printed ["name"]=> uninitialized(string) after ["n":"A":private]
The `_class_vd_desc_*` descriptor rows now skip virtual properties, both in
the declared property table and in the __debugInfo projection, so the rows
agree with the object header count. print_r() already omitted them.
var_export() is unchanged: PHP 8.5 renders virtual properties there with
their get-hook values (`'name' => 'x'`), and elephc omits them before and
after this change. That is a separate gap.
PHP's ReflectionNamedType, ReflectionUnionType and ReflectionIntersectionType extend the abstract ReflectionType, and code typed against it (`?ReflectionType $t`, `$t instanceof ReflectionType`) is common in libraries that inspect signatures. elephc had no ReflectionType, so such code failed to compile with "Unknown type: ReflectionType". ReflectionType is now injected with the other Reflection classes, the three concrete type classes extend it, and the class catalog lists it. The shell declares no members yet, so `allowsNull()` / `__toString()` are still called through the concrete classes (documented); a call on a ReflectionType-typed receiver stays a compile-time "Undefined method" rather than a dispatch the Reflection reachability scan cannot see.
Only the `--web` prelude declared SessionHandlerInterface, so a handler
class, or any code typed against the interface, failed to compile in a
normal build ("Unknown interface: SessionHandlerInterface"), although PHP's
CLI has it.
The checker now injects the interface (open/close/read/write/destroy/gc with
PHP's signatures) unless an internal prelude already declared it, so the
`--web` declaration keeps winning. A user declaration of the name is
rejected as "Cannot redeclare built-in interface: SessionHandlerInterface",
like the other builtin interfaces. The class catalog now routes it as
checker-injected.
…declared lists The source-order filter used the `__elephc` prefix, which also hid user classes and interfaces such as `__ElephcUser`. PHP does not reserve that prefix, so filter user-position names by the builtin class contract's `internal` flag instead.
…de --web Extend the reflection example with a helper typed against the abstract ReflectionType parent, and add a session-handler example that implements SessionHandlerInterface in a plain CLI program.
JGamboa
force-pushed
the
fix/introspection-hidden-internals
branch
from
October 21, 2026 06:04
fa59274 to
45e9c83
Compare
The builtin SessionHandlerInterface injection only looked for a colliding class or interface name, so outside --web a user trait of that name was accepted next to the injected interface. PHP rejects it. Include the declared trait names, as the Reflection injection already does, and point the error at the trait declaration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introspection fixes split out of #1886 (the "introspection" PR of the suggested split). Each one makes elephc report what PHP reports:
__*storage slots elephc declares on the builtin Reflection classes ingetProperties()/hasProperty()/getDefaultProperties(), and listed the generated__propget_*/__propset_*accessors of hooked properties ingetMethods()/hasMethod(). Reflecting a user subclass ofReflectionClassand callinggetProperties()exhausted the default heap. The statically expandedgetProperties()(receiver known at lowering time) also listed a parent's private properties. One rule,ClassInfo::reflection_lists_property, now serves the codegen metadata and the static EIR expansion.get_declared_classes().__ElephcWeakMapIterator, declared by the WeakMap prelude as a program statement, was listed. Code that instantiates every declared class (php-srcZend/tests/instantiate_all_classes.phpt) then hit a fatal error PHP never raises.var_dump(). A property whose hooks never touch a backing slot was printed as["name"]=> uninitialized(string), read from an unrelated layout offset. PHP does not print it, and the object header count already excluded it.print_r()already omitted these properties.ReflectionType. It is now the abstract parent ofReflectionNamedType,ReflectionUnionTypeandReflectionIntersectionType, so?ReflectionTypehints,instanceof ReflectionTypeand(string) $typecompile.SessionHandlerInterfaceoutside--web. Until now only the--webprelude declared it, so a handler class failed with "Unknown interface". The checker now injects the interface unless an internal prelude already declared it (the--webdeclaration still wins). A user redeclaration is rejected like the other builtin interfaces.Closes #1900
Part of #855 (
ReflectionTypeonly;Reflector,ReflectionGeneratorandReflectionFunctionAbstractare still missing)Repro (no issue yet)
Reflection backing slots and hook accessors:
Virtual hooked property:
Not in this PR
nameproperty, which needs the__propget_nameaccessor this PR hides. It goes into the first-class-callables PR, aligned with fix(reflection): expose PHP public properties across object views #1297.UnitEnum/BackedEnumand the non-enum check from the same topic of feat: PHP compatibility batch (parser forms, checker convergence, references, dynamic includes, builtins) #1886. They are already on main via feat(core): complete PHP 8.5 classes and constants #1870.var_export()of virtual hooked properties. PHP 8.5 renders them with their get-hook values ('name' => 'x'), but elephc omits them, before and after this PR. That gap is separate.eval()fragments andmethod_exists()don't apply the new member filters yet (documented indocs/php/classes.md).ReflectionTypedeclares no members of its own yet:allowsNull()is called through the concrete type classes (documented).Tests
This branch is based on
main@ df57a98. Each test below fails on thatmain(checked with a main build) and passes on this branch:codegen::oop::reflection_builtin_subclass::{reflection_class_backing_slots_are_not_reflected, property_hook_accessors_are_not_reflected_as_methods, static_get_properties_hides_reflection_backing_slots, static_get_properties_skips_inherited_private_properties}codegen::core_classes::test_declared_classes_hide_internal_core_prelude_classesvar_dump_object_tests::virtual_hooked_property_is_not_renderedcodegen::oop::reflection_functions::test_reflection_type_is_the_common_parent(main: "Unknown type: ReflectionType")codegen::core_classes::test_session_handler_interface_is_implementable_without_web(main: "Unknown interface: SessionHandlerInterface")error_tests::test_error_session_handler_interface_cannot_be_redeclared(main accepts the user interface)The control test
reflecting_a_reflection_class_subclass_fits_the_default_heappasses on main too. It is kept because it guards the heap budget once the Reflection classes gain public properties.The builtin docs and symbol registries were regenerated with
scripts/docs(two catalog entries changed).Refs #1886
AI assistance
This PR was prepared with substantial help from Claude Code (Anthropic). The changes were split out of #1886, re-based on current main and reviewed by me. Every regression test was run against a main build and against this branch.
🤖 Generated with Claude Code