Commit badf84e
committed
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
0 commit comments