The visibility RFC draws a line it inherits from every other use statement: importing is not loading. use extension Vendor\Name; grants visibility, but the declaring file must still be executed by something — a require_once, or the declaring package's Composer autoload.files entry. This RFC removes that residue: an imported-but-unloaded named extension is resolved through the ordinary class autoloader at first use, exactly as an unloaded class is. The use extension line becomes the only line a consumer writes:
// vendor/acme/dom-kit/src/DomTraversal.php (PSR-4 / classmap addressable) namespace Acme\DomKit; extension DomTraversal on \DOMElement $el { public function firstByClass(string $class): ?\DOMElement { /* ... */ } }
// app/render.php — nothing else loads DomTraversal.php use extension Acme\DomKit\DomTraversal; $el->firstByClass('hero'); // first use: autoloaded, resolved, called
The ecosystem half of this pipeline already exists: composer/class-map-generator#43 (draft) teaches Composer's class-map generator to map named extension declarations to their files. PSR-4 itself needs no amendment — its “classes, interfaces, traits, and other similar structures” wording covers extension declarations, as it covered enums. This RFC supplies the engine half.
The visibility RFC left open “what artifact the autoloader maps the name to.” This RFC answers: the named extension's synthetic class entry, registered in the class table under the extension's own name at bind time (when the declaring statement executes). Consequences, stated plainly:
class_exists('Vendor\Name') is true once the declaration has executed — and class_exists('Vendor\Name', true) triggers autoloading, exactly as for a class. This is deliberate: “is this extension available?” now has the same answer mechanism as every other symbol.new Vendor\Name throws (“Cannot instantiate abstract class Vendor\Name” — see Open Issues for the message), extends fails against final, and the CE carries the extension's real qualified name, which is what reflection and diagnostics display.
Resolution stays lazy — the same moment a class is autoloaded is the moment an extension is: first use. When extension-method resolution misses (object or scalar receiver), the engine takes each name in the calling code's import set that has no class-table entry and hands it to the class autoloader (zend_lookup_class_ex, i.e. the spl_autoload chain), then retries the lookup once. Semantics:
use extension statement (or in the declaring position, for self-imports). Import sets store the original spelling alongside the lowercased gating key, because PSR-4 file resolution is case-sensitive.use of a class that is never mentioned — so, unlike the class case (where failure throws and naturally stops repetition), nothing else would bound re-attempts. A per-request negative cache does. No attempt is consumed while no autoloader is registered at all, so bootstrap order cannot permanently poison a name.method_exists, is_callable on objects) can consequently trigger the autoload from an importing frame — the same way class_exists($n, true) triggers class loading. Since these already observe extension methods (base RFC open issue), this adds no new observability, only earlier loading.Note what the trigger does not do: it never maps the missing method to a specific extension. That mapping (method name + receiver type → extension name) does not exist anywhere before the declaring file executes — a PHP source file's contents are not queryable without running it, and the class map knows names→files, nothing more. So the engine uses the only sound key available: the calling file's import set. All unloaded imports are loaded on a miss, then the lookup retries once.
This is precisely where extension autoloading escapes the fate of function autoloading. An unqualified function call has an unbounded candidate space (current namespace, global fallback, any loader's guess); an extension method call, by the visibility RFC's own rules, can only be satisfied by an extension the file explicitly imported by fully-qualified name. The candidate list is finite, explicit, and sitting in the op_array — typically a handful of names.
The trade-off is stated honestly: the engine over-loads. A miss that turns out to be a genuine typo loads all of the file's unloaded imports before the undefined-method error is raised. Three bounds keep this cheap: it happens on the error/first-touch path only; each name is attempted at most once per request; and extension declaring files are declaration-only by construction. The end state after the first miss is deterministic — the file's imports are simply all loaded, the same world its autoload.files entries would have created at bootstrap, arrived at lazily (never earlier, often never). One coupling worth recording: the visibility RFC's group-use and re-export open issues exist to grow import sets cheaply; accepting either later fattens this load batch, so those decisions and this trigger are linked.
With this RFC accepted and Composer support merged, the loading story of the visibility RFC's FAQ (“do I need use extension and a require_once?”) collapses to its final form: no. The package maps its extension files via PSR-4/classmap as it maps classes; the consumer writes use extension Vendor\Name; and calls methods. autoload.files entries and manual require_once become what they are for classes today: unnecessary.
None for code without extensions. Within the extension-methods family (not yet released, so these are proposal-internal changes):
class_exists() answers true for loaded named extensions.Same release as the base Extension Methods RFC; meaningful only if the visibility RFC is accepted (anonymous extensions have no names to load).
This is the RFC Composer has been waiting on: composer/class-map-generator#43 maps extension Name on Target $recv { } declarations into the class map and is held as draft only until this family's grammar is final. Static analyzers gain a stronger property: an unimported extension call is now provably unresolvable (before, a global require anywhere could have loaded an anonymous extension), and IDE “missing import” quick-fixes can offer use extension exactly as they offer use.
One new executor-global (the per-request attempted-names table, lazily allocated) — an ABI change of the routine minor-version kind. The class-table entries for named extensions are alias-typed (IS_ALIAS_PTR), the same mechanism class_alias() uses, so C code iterating the class table must already tolerate them.
None. The negative cache is request-scoped and resets with the executor.
Import sets simply carry one more interned string per import (the original-case name), persisted through the exact machinery the visibility RFC already uses — the persistence code iterates elements and needed no changes. The synthetic CE's name and uninstantiable flag are set at compile time, preserving the family invariant that the persisted script image is never mutated at runtime; the class-table binding at execution writes only to the per-request class table, never to shared memory. Verified green: full Zend suite under opcache.protect_memory=1 and under tracing and function JIT.
new Vendor\Name currently reports “Cannot instantiate abstract class” (the CE reuses the abstract-class instantiation guard). A bespoke “Cannot instantiate extension %s” needs either a spare CE flag or an extension-CE discriminator; cosmetic, but worth fixing before final vote.ReflectionClass works today (getName() returns the extension name, isInstantiable() is false), but the synthetic CE also reports isAnonymous() === true — an artifact of its construction. The family's deferred reflection surface (ReflectionExtension-style first-class API) should subsume this.
Class autoloading itself is untouched — extensions ride the existing chain; no new autoloader hook, no new callback signature, no changes to spl_autoload_register. Anonymous extensions, gating semantics, and everything listed in the prior RFCs' unaffected sections.
name → target map from that single source of truth, and Composer's bootstrap pushes it into the engine through a small registration API (the spl_autoload_register idiom — there is deliberately no query protocol in the autoload chain, so hints must be pushed, not fetched). At miss time the engine loads ancestry-matching (and unhinted) imports first, and the remainder only if the miss persists: a stale or missing hint costs a fallback pass, never correctness. Three design constraints are recorded now, in advance: (1) omit on uncertainty — a textual generator cannot fully reimplement PHP's name resolution for aliased targets, so it must emit a hint only when the target resolves trivially, and an absent hint means always-candidate; (2) no per-request copy — registration must hold a reference to the opcache-shared array (or a lazy provider), not rebuild an engine table each request, per this family's own rejection of per-request setup work; (3) prerequisite: import-set-aware conflict resolution (visibility RFC Open Issue 1) must land first — under first-wins registration, hint-driven load order would let generated metadata select conflict winners, and only ambiguity-is-an-Error makes load order semantically inert.use extension quick-fix diagnostic (“did you forget to import?”) remains as proposed in the visibility RFC.Primary vote requiring a 2/3 majority (per the php/policies voting guidelines); void if the visibility RFC is declined:
Autoload named extensions through the class autoloader as outlined in the RFC? Yes / No
Implemented on the extension-methods-autoload branch, stacked on the visibility RFC's extension-methods branch — the diff between the two branches is exactly this proposal. Zend/tests/extension_methods/ adds: first-use autoload with a one-invocation loader (autoload_basic.phpt), the scalar dispatch path (autoload_scalar.phpt), class_exists/instantiation/reflection of the class-table symbol (autoload_class_symbol.phpt), name collision fatality (autoload_collision.phpt), negative caching of unloadable imports (autoload_negative_cache.phpt), autoloader exception propagation on both object and scalar paths (autoload_throw.phpt), and gating unaffected by autoloading (autoload_gating.phpt). Full matrix green: plain, opcache.protect_memory=1, tracing and function JIT.
After acceptance: link to the merged commit(s), PHP version, and documentation.
spl_autoload_register_extension or a new callback signature) — a second autoload chain doubles the bootstrap surface of every framework for zero expressive gain; the class chain already delivers name→file resolution, and Composer needs one map, not two.use extension statement — would make one use form a runtime construct (the visibility RFC's rejected activation opcode, one layer up), punish speculative imports, and break the position-independence consumers expect from imports.class_exists analog, and its own autoload recursion guards; the class table already has all three, and sharing it is what makes the collision rule enforceable rather than advisory.use extension Vendor\Name for Target;) — proposed to narrow the load-all-imports batch, but the claim is unverifiable at the moment it steers loading: the declaration is, by definition, not loaded yet, so the claim can only be checked after the load it was meant to optimize. A claim stale in the narrow direction — the library widened on DOMElement to on DOMNode, or the consumer named the class where the extension targets its interface — makes the engine skip a load it needed, producing a wrong “Call to undefined method” for a correctly imported, correctly published extension: silent misbehavior, the worst failure class. Preventing it requires a load-time Error on mismatch, which turns every import into a contract on the library's target choice and breaks consumers whenever a library performs the good refactor of widening its target. And since a claim can be wrong, correctness still requires the load-the-rest fallback — the syntax only reorders work the engine must be able to do anyway. The target-hint design in Future Scope achieves the same filtering from the declaration itself, with no consumer-side contract.use extension ... for Target) and method-indexed metadata as a resolution key; specified the target-hint loading optimization in Future Scope with its three design constraints, including the conflict-resolution prerequisite.