Class autoloading has existed in PHP for 20+ years. However, there is no equivalent for function autoloading.
This RFC proposes function autoloading, modeled directly on class autoloading, with no cost to calls that resolve normally. As with class autoloading, the consumer may register a callback that defines a function on demand (usually via include of a definition file) and the engine invokes it whenever a function name fails to resolve.
The following example autoloads functions on a one-function-per-file basis ...
// Register a loader that maps a function name to a file. spl_autoload_register_function_loader(function (string $function) : void { $path = __DIR__ . '/functions/' . str_replace('\\', '/', $function) . '.php'; if (is_file($path)) { require $path; } }); // functions/Foo/greet.php defines Foo\greet(); it is loaded on first use. echo \Foo\greet('world');
... but note that multiple functions might be defined in a single file as well. For an example of such logic see the Moto autoloading algorithm, which applies to classes as well as functions.
Function autoloading surfaces a name-resolution gotcha for unqualified calls inside a namespace (see Namespace resolution problems). A companion RFC, Strict Namespace Resolution, proposes a declare(strict_namespace=1) directive that addresses it by turning off the global fallback for unqualified function and constant names in a file, so a namespaced call reaches the loader instead of resolving to a global of the same name. That directive is optional; this RFC stands on its own.
Function autoloading is implemented essentially as a copy of class autoloading. Where the engine would have thrown Error: Call to undefined function, the implementation now consults any registered function loaders, one of which may define the missing function.
The implementation introduces four new global SPL functions to manage the function loaders, and an optional $autoload parameter is added to function_exists():
// new functions: function spl_autoload_register_function_loader( callable $callback, bool $prepend = false ) : bool {} function spl_autoload_unregister_function_loader( callable $callback ) : bool {} function spl_autoload_function_loaders() : array {} function spl_autoload_call_function_loader( string $function_name ) : void {} // existing function, new optional second parameter: function function_exists( string $function, bool $autoload = true ) : bool {}
A function loader is any callable taking one string argument: the function name as resolved at the call site, fully qualified, leading backslash removed, original case preserved.
That resolved name may be namespaced (bar() inside namespace Foo becomes Foo\bar).
It may also be global (a top-level bar(), an explicit \bar(), or a string passed to an API such as call_user_func() or function_exists()).
The loader callback is expected to define the function if it can (typically by requiring a file), or do nothing if it cannot. Its return value is ignored.
Loaders run only when a name fails to map to a defined function, on the same failed-lookup path that today throws Error: Call to undefined function.
A call to an already-defined function never invokes the loaders; such calls are “free” in performance terms.
Loading may occur at any site that resolves a function name by value:
bar()) and dynamic calls through a variable ($fn());function_exists(), unless autoloading is disabled (see below);new ReflectionFunction($name));is_callable();call_user_func() and call_user_func_array();array_map(), array_filter(), and usort();callable-typed parameters; and,Closure::fromCallable().JIT-compiled code honors it as well.
A few contexts do not autoload, matching their equivalents on the class side:
function_exists($name, false), an explicit table check;is_callable($name, true), the syntax-only check;get_defined_functions(), an enumeration of what is already defined.
spl_autoload_call_function_loader(string $function_name) runs the chain manually for these cases. It triggers every registered loader for the name and never throws if none defines it.
For an unqualified call inside a namespace, PHP looks first in the current namespace, then falls back to the global function table.
Function autoloading adds a followup step: if both miss, the loader chain is consulted once, using the fully-qualified name as resolved at the call site. The engine then rechecks only that name.
This has three consequences:
function_exists('Foo\bar', false) stays false and Foo\bar remains autoloadable on a later fully-qualified call.Foo\bar but defines the global bar instead, the current call still fails with Error: Call to undefined function Foo\bar() (an answer to a different question is not rebound); the next bar() call finds the global through the fallback, without re-consulting the loader.I don't want to understate the case here: that last point might well be considered unexpected behavior based on how class autoloading works.
Let's say we have the namespace Foo, and in some file a namespaced stdClass class, and autoloading is set to load that file based on the class name. It has not been loaded yet, but the global stdClass is already loaded because it is built in to PHP.
namespace Foo; class stdClass {}
In another file, also in the Foo namespace, we instantiate an unqualified stdClass, and the autoloader gives us the Foo\stdClass class:
namespace Foo; $stdClass = new stdClass(); // instanceof Foo\stdClass
Class name resolution never falls back to the global namespace when an unqualified name does not resolve to an already-loaded class in the same namespace.
But it will not work that way with function autoloading.
Let's say we have the namespace Foo, and in some file a namespaced strlen() function, and autoloading is set to load that file based on the function name. It has not been loaded yet, but the global strlen() is already loaded because it is built in to PHP.
namespace Foo; function strlen(string $str) : int { // Foo\strlen() always returns -1 return -1; }
In another file, also in the Foo namespace, we call an unqualified strlen(), but the autoloader will not trigger; we get the global strlen() function instead:
namespace Foo; $len = strlen('bar'); // 3
This is because function name resolution always falls back to the global namespace when an unqualified name does not resolve to an already-loaded function in the same namespace.
That is, an unqualified strlen() call from within the Foo namespace will always fall back to the global strlen() when Foo\strlen() is not already loaded.
This is a narrow failure mode but very real and unexpected.
There are two ways to make an unqualified strlen() resolve to Foo\strlen() inside the Foo namespace.
The first is to import the fully qualified function:
Now the resolution logic will look specifically for Foo\strlen(), miss, and trigger the autoloader. Having to import a function from the same namespace is a chore, though, and it only covers the one name you import.
The second is to turn off the global fallback for the whole file with declare(strict_namespace=1). Then every unqualified function (and constant) name resolves to the current namespace, so strlen() is Foo\strlen and autoloads on its own:
declare(strict_namespace=1); namespace Foo; $len = strlen('bar'); // Foo\strlen: autoloads, then -1
That directive is proposed separately, in the companion Strict Namespace Resolution RFC, and is not part of this RFC. In brief: it is a compile-time, per-file opt-in that disables the global fallback for unqualified function and constant names, so they resolve to the current namespace only, the same path an unqualified class name already takes. Fully qualified names (\strlen()) and use function/use const imports still reach the global table. See that RFC for the exact semantics and constraints.
Having said all that, the shadow-global case is not common. I researched the top 1000 packages on Packagist (981 with analyzable source, roughly 161,000 files), tracking three things:
While the unqualified call count is large (312,575 such calls across 75% of packages), the shadow function definition count is small: only 766 such definitions in 17 packages. Of those, 96% belong to two libraries, thecodingmachine/safe and azjezz/psl. And even then, those packages are written to be imported with use function and produce no hazards.
In the end, it looks like there are only 2 packages and 8 call sites that would behave unexpectedly if they converted to function autoloading and dropped their include calls. Either a use function import or declare(strict_namespace=1) would fix each one.
Function loaders live in their own registry, separate from class loaders: registering one has no effect on class autoloading, or vice versa.
spl_autoload_register_function_loader() appends a loader, or with $prepend = true prepends it. Loaders are consulted in order, and the first to define the function ends the pass. Registering the same callable twice is a no-op.
spl_autoload_function_loaders() returns the registered callables in order.
spl_autoload_unregister_function_loader() removes one, returning true if it was registered, false otherwise.
Passing spl_autoload_call_function_loader itself to the register function throws a ValueError.
Loaders may register or unregister loaders mid-pass; iteration is safe.
function_exists() gains an optional second parameter $autoload, defaulting to true like class_exists(). When true, a missing function triggers the chain before the result is reported; when false, the table is checked without autoloading.
The true default is a behavioral change (see Backward Incompatible Changes).
Loader exceptions propagate. At direct and dynamic call sites and from is_callable() they surface directly. The call APIs (call_user_func(), Closure::fromCallable()) wrap them in a TypeError with the loader's exception as the previous exception. They are never suppressed.
Failed autoloads are not cached, positively or negatively. A loader that declines silently (throws nothing, defines nothing), or one that throws, does not poison the name: the next reference consults the chain again, so a name unloadable now may load later.
Re-entrant autoloading of the name being resolved is blocked: if a loader resolving bar calls bar(), that nested call fails with Call to undefined function rather than recursing.
Basic one-function-per-file loader:
spl_autoload_register_function_loader(function (string $function) : void { // map "Foo\Math\add" to functions/Foo/Math/add.php $path = __DIR__ . '/functions/' . str_replace('\\', '/', $function) . '.php'; if (is_file($path)) { require $path; } }); // first reference loads functions/Foo/Math/add.php on demand echo \Foo\Math\add(2, 3); // 5
See the Moto autoloader for a loader that handles more-than-one function per file.
Namespace edge case: a global fallback wins without consulting the loader, but a fully-qualified call autoloads:
namespace Foo; // define a global helper() \eval('function helper() { return "global"; }'); \spl_autoload_register_function_loader(function (string $name) : void { echo "loader($name)\n"; if ($name === 'Foo\helper') { eval('namespace Foo; function helper() { return "namespaced"; }'); } }); // a global helper() exists, so an unqualified call resolves to it through the // global fallback and the loader is NOT consulted \var_dump(helper()); // string(6) "global" // a fully-qualified call has no global fallback, so it autoloads Foo\helper \var_dump(namespace\helper()); // loader(Foo\helper), then string(10) "namespaced"
Polyfill guard, now that function_exists() autoloads by default:
spl_autoload_register_function_loader(function (string $name) : void { if ($name === 'acme_slug') { eval('function acme_slug(string $s) : string { return strtolower(strtr($s, " ", "-")); }'); } }); // default: function_exists() autoloads, so the loader defines acme_slug() // and the polyfill body below is skipped if (! function_exists('acme_slug')) { // ... define a fallback ... } \var_dump(function_exists('acme_slug', false)); // bool(true): the loader defined it // opt out: check the table only, do not autoload. \var_dump(function_exists('made_up_func', false)); // bool(false): loader not consulted
Two changes are observable to existing code.
1. function_exists() autoloads by default. With $autoload defaulting to true (like class_exists()), a check on a missing function now consults registered loaders before returning. Consider the common polyfill idiom:
if (! function_exists('bar')) { function bar() { /* fallback */ } }
If a loader for bar is registered, this calls it before defining the fallback. For a pure table check, pass function_exists($name, false). With no loaders registered (the default), behavior is unchanged.
2. Four new global function names. spl_autoload_register_function_loader, spl_autoload_unregister_function_loader, spl_autoload_function_loaders, and spl_autoload_call_function_loader enter the global namespace. Userland functions with these exact names would conflict, but the names are specific enough to make that rare.
There are no other changes; existing class autoloaders cannot tell this feature exists.
Next PHP 8.x (targeting PHP 8.6).
Static analyzers, IDEs, and language servers will want to account for an undefined function being autoloaded, as they already do for classes. The feature makes per-function autoloading viable for function libraries, without forcing functions into static methods or eagerly-required files.
The implementation lives in core (Zend/zend_autoload.c), along with the class autoloading hook: a zend_function_autoload hook next to zend_autoload, and zend_lookup_function() paralleling the class lookup.
One new internal API, zend_autoload_function_fcc_map_to_callable_zval_map(), is the function-loader counterpart to the existing zend_autoload_fcc_map_to_callable_zval_map(), which keeps its name and behavior (it still backs spl_autoload_functions()). Dependent extensions are unaffected, and opcache/JIT is updated to honor it.
No SAPI-specific impact.
None known.
Should the userland API move from the spl_autoload_* prefix to a core autoload_* naming? (This RFC keeps the spl_autoload_ prefix and takes no position on a future rename.)
Out of scope, and proposable separately:
include/require;spl_autoload-style helper);declare(strict_namespace=1) toward this.Primary Vote requiring a 2/3 majority to accept the RFC:
Proof of concept: php/php-src pull request #22337.
Tests accompany the implementation:
Zend/tests/autoload/function_autoload_*.phpt: registration, invocation across all trigger sites, loader chains and ordering, exception propagation and the absence of caching, namespace resolution (including the four-scenarios and no-pinning cases), name normalization, callable kinds, recursion and mutation during a pass, manual triggering, registry independence from class loaders, and use function imports;ext/opcache/tests/jit/function_autoload_00{1,2,3}.phpt: JIT-compiled code honors autoloading;ext/spl/tests/autoloading/spl_autoload_register_function_loader_rejects_call_function_loader.phpt: the ValueError guard.After the RFC is implemented, this section should contain:
Two approaches that recurred across earlier proposals are explicitly rejected here.
$type argument (or an AUTOLOAD_* bitmask), with the callback switching on type. This RFC uses a separate registry and a single-argument callback, so no callback switches on a type, and adding function support cannot change the arguments existing class autoloaders receive.function_exists() as a BC break. This RFC runs the loader only after the global fallback fails, so there is nothing to pin: defined functions are never probed, and function_exists() reports only functions that exist.A condensed comparison of the principal prior proposals:
| Proposal | Year | Status | Registration | Callback receives | BC surface |
|---|---|---|---|---|---|
| rfc/autofunc (Kovacs) | 2011 | Inactive | spl_autoload_register($cb, ..., $types) | ($name, $type) | new args to existing API and callbacks |
| function_autoloading2 (Ferrara/Wiedler) | 2013-15 | Draft | php\autoload_register($cb, $type) | ($name, $type) | SPL proxied; SPL globals removed |
| core-autoloading (Banyard/Ackroyd) | 2023 | Under Discussion | autoload_register_function() (new family) | ($name) | API renamed/aliased; pinning visible in function_exists() |
| function_autoloading4 (Landers) | 2024 | Discarded | spl_autoload_register($cb, ..., $types) | ($name, $type) | broke existing callbacks (Symfony) |
| This RFC | 2026 | Draft | spl_autoload_register_function_loader() (new, separate) | ($name), FQN | no existing signatures changed; function_exists() default now true |
declare(strict_namespace=1) into the separate Strict Namespace Resolution RFC; this RFC now references it as a companion rather than proposing it (0.3).declare(strict_namespace=1) as a per-file opt-out of the global fallback, with a prevalence note on the shadow-global case (0.2).