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 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.
To make unqualified calls to strlen() resolve to Foo\strlen() inside the Foo namespace, you must import the fully qualified function first:
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 but I don't see a way around it without modifying the name resolution rules.
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);Primary Vote requiring a 2/3 majority to accept the RFC:
Implementation: https://github.com/php/php-src/pull/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 |