Skip to content

Commit badf84e

Browse files
Reference cross-class and global attribute first-class callables by declaration site on PHP 8.5
Building on the allow_named_closures gate: first-class callables over a method of their own declaring class already serialize as a const-expr declaration-site reference. Cross-class references (Validators::check(...)) and global functions (strlen(...)) could not, because the closure carries no link back to the class whose attribute declares it -- its scope is the target, or none. PHP 8.6 records that declaring class as engine provenance (ReflectionFunction::getConstExprClass); 8.5 does not. This recovers it on 8.5 by instrumenting the paths frameworks use to read attribute metadata: ReflectionAttribute::getArguments() (the closures are the returned argument values) and ::newInstance() (they are properties of the returned attribute instance). For each cross-class or global first-class callable produced, the declaring class -- read from the ReflectionAttribute -- is recorded in a per-worker, name-keyed index. At to_array time, when the scope-based locate misses, the index yields the declaring class and the closure resolves to the same declaration-site reference (mask 1); decode is unchanged. No allow_named_closures opt-in is needed, because these remain declaration-site references, resolvable only to what the named class itself declares, and allowed_classes still gates Closure. The index is keyed and valued by names (not pointers, which churn per request without opcache) and persists across requests, so once a declaration has been seen it resolves in later requests too -- effective across a worker's lifetime under opcache.preload. The locate walk matches a target by identity: user functions by op_array.opcodes (methods, user globals, anonymous closures), internal functions by name and scope. The capture walk is cycle-guarded, since newInstance() returns an object built by an arbitrary attribute constructor. There is no INI knob: the hooks are installed at MINIT on a build without native provenance, and skipped when ReflectionFunction::getConstExprClass exists, since the engine-id path then resolves these directly. Caveats. Recovering the declaring class needs reflection's private object layout, mirrored here and tracking the engine structs. Encoding is load/reflection-order dependent: a runtime callable equivalent to a declared one serializes as a site reference once that declaration has been seen, otherwise by name -- always safe (bounded to what classes declare) and decode is deterministic. The lookup does not autoload, so the declaring class must be loaded when serializing (resident under preload). There is no polyfill counterpart: userland cannot hook reflection. The polyfill decodes these payloads, except internal-global-function ones, whose line-0 reference trips its ReflectionFunction::getStartLine()-based staleness check and fails closed.
1 parent a6d3f67 commit badf84e

3 files changed

Lines changed: 468 additions & 11 deletions

File tree

0 commit comments

Comments
 (0)