Skip to content

Commit da508ed

Browse files
committed
implement function autoloading
1 parent 71d27d8 commit da508ed

37 files changed

Lines changed: 1624 additions & 40 deletions

UPGRADING.FUNCTION-AUTOLOADING

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
UPGRADE NOTES: FUNCTION AUTOLOADING
2+
3+
1. New Features
4+
2. Changed Functions
5+
3. New Functions
6+
4. Internal API Changes
7+
8+
========================================
9+
1. New Features
10+
========================================
11+
12+
- Core:
13+
. Added function autoloading support. Register callbacks via
14+
spl_autoload_register_function_loader() that are invoked when an
15+
undefined function is referenced. It is triggered wherever an
16+
undefined function name is resolved: direct calls, dynamic calls
17+
through a string variable ("$func()"), function_exists(), callable
18+
resolution (is_callable(), call_user_func()/call_user_func_array(),
19+
array and sort callbacks such as array_map()/array_filter()/usort(),
20+
callable typed parameters, and Closure::fromCallable()), and the
21+
Reflection API (new ReflectionFunction($name)).
22+
As with class autoloading, it is NOT triggered by contexts that
23+
deliberately avoid autoloading: function_exists($name, false),
24+
is_callable($name, true) (syntax-only checks), and
25+
get_defined_functions(). spl_autoload_call_function_loader() can be
26+
used to trigger function autoloading manually in those situations.
27+
28+
========================================
29+
2. Changed Functions
30+
========================================
31+
32+
- Core:
33+
. function_exists() now accepts an optional bool $autoload parameter.
34+
When true (the default, matching class_exists()), function
35+
autoloading is triggered before the function reports as missing.
36+
BC note: existing function_exists() guards around conditional
37+
function definitions (polyfills) will now consult registered function
38+
loaders before falling through to define the fallback. Pass
39+
function_exists($name, false) to check without autoloading.
40+
41+
========================================
42+
3. New Functions
43+
========================================
44+
45+
- SPL:
46+
. spl_autoload_register_function_loader() registers a function autoloader.
47+
. spl_autoload_unregister_function_loader() unregisters a function autoloader.
48+
. spl_autoload_function_loaders() returns registered function autoloaders.
49+
. spl_autoload_call_function_loader() manually triggers function autoloading.
50+
51+
========================================
52+
4. Internal API Changes
53+
========================================
54+
55+
- Zend:
56+
. Added zend_autoload_function_fcc_map_to_callable_zval_map() as the
57+
function-loader counterpart to the existing
58+
zend_autoload_fcc_map_to_callable_zval_map(). The existing function
59+
keeps its name and behaviour (it backs spl_autoload_functions()), so
60+
there is no break for extensions that call it.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
--TEST--
2+
Function autoloading: method and trampoline loaders
3+
--FILE--
4+
<?php
5+
class StaticLoader {
6+
public static function load(string $name): void {
7+
if ($name === 'demo_func') {
8+
eval('function demo_func() { return "from_static"; }');
9+
}
10+
}
11+
}
12+
13+
class InstanceLoader {
14+
public function load(string $name): void {
15+
if ($name === 'demo_func2') {
16+
eval('function demo_func2() { return "from_instance"; }');
17+
}
18+
}
19+
}
20+
21+
class TrampolineTest {
22+
public function __call(string $name, array $arguments) {
23+
echo "Trampoline for $name: $arguments[0]\n";
24+
}
25+
}
26+
27+
echo "== static method ==\n";
28+
spl_autoload_register_function_loader([StaticLoader::class, 'load']);
29+
var_dump(demo_func());
30+
31+
echo "== instance method ==\n";
32+
$obj = new InstanceLoader();
33+
spl_autoload_register_function_loader([$obj, 'load']);
34+
var_dump(demo_func2());
35+
var_dump(count(spl_autoload_function_loaders()));
36+
spl_autoload_unregister_function_loader([StaticLoader::class, 'load']);
37+
spl_autoload_unregister_function_loader([$obj, 'load']);
38+
39+
echo "== trampoline ==\n";
40+
$o = new TrampolineTest();
41+
$callback1 = [$o, 'trampoline1'];
42+
$callback2 = [$o, 'trampoline2'];
43+
spl_autoload_register_function_loader($callback1);
44+
spl_autoload_register_function_loader($callback2);
45+
spl_autoload_register_function_loader($callback1); // duplicate, ignored
46+
var_dump(count(spl_autoload_function_loaders()));
47+
var_dump(function_exists('demo_func3', true)); // consults both trampolines, stays false
48+
var_dump(spl_autoload_unregister_function_loader($callback1));
49+
var_dump(spl_autoload_unregister_function_loader($callback1));
50+
var_dump(spl_autoload_unregister_function_loader($callback2));
51+
var_dump(spl_autoload_function_loaders());
52+
var_dump(function_exists('demo_func3', true));
53+
?>
54+
--EXPECT--
55+
== static method ==
56+
string(11) "from_static"
57+
== instance method ==
58+
string(13) "from_instance"
59+
int(2)
60+
== trampoline ==
61+
int(2)
62+
Trampoline for trampoline1: demo_func3
63+
Trampoline for trampoline2: demo_func3
64+
bool(false)
65+
bool(true)
66+
bool(false)
67+
bool(true)
68+
array(0) {
69+
}
70+
bool(false)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Function autoloading: multiple loaders and prepend order
3+
--FILE--
4+
<?php
5+
echo "== order ==\n";
6+
$l1 = function (string $name) {
7+
echo "loader1: $name\n";
8+
};
9+
$l2 = function (string $name) {
10+
echo "loader2: $name\n";
11+
if ($name === 'demo_func') {
12+
eval('function demo_func() { return "from_loader2"; }');
13+
}
14+
};
15+
$l3 = function (string $name) {
16+
echo "loader3: $name\n"; // never reached: loader2 already defined it
17+
};
18+
spl_autoload_register_function_loader($l1);
19+
spl_autoload_register_function_loader($l2);
20+
spl_autoload_register_function_loader($l3);
21+
var_dump(demo_func());
22+
spl_autoload_unregister_function_loader($l1);
23+
spl_autoload_unregister_function_loader($l2);
24+
spl_autoload_unregister_function_loader($l3);
25+
26+
echo "== prepend ==\n";
27+
$append = function (string $name) {
28+
echo "appended: $name\n"; // never reached: prepended runs first and defines
29+
};
30+
$prepend = function (string $name) {
31+
echo "prepended: $name\n";
32+
if ($name === 'demo_func2') {
33+
eval('function demo_func2() { return "ok"; }');
34+
}
35+
};
36+
spl_autoload_register_function_loader($append);
37+
spl_autoload_register_function_loader($prepend, prepend: true);
38+
var_dump(demo_func2());
39+
spl_autoload_unregister_function_loader($append);
40+
spl_autoload_unregister_function_loader($prepend);
41+
?>
42+
--EXPECT--
43+
== order ==
44+
loader1: demo_func
45+
loader2: demo_func
46+
string(12) "from_loader2"
47+
== prepend ==
48+
prepended: demo_func2
49+
string(2) "ok"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
--TEST--
2+
Function autoloading: loader exceptions propagate, failures are not cached
3+
--FILE--
4+
<?php
5+
echo "== direct propagates ==\n";
6+
$thrower = function (string $name) {
7+
throw new RuntimeException("Autoload failed for: $name");
8+
};
9+
spl_autoload_register_function_loader($thrower);
10+
try {
11+
missing_func();
12+
} catch (RuntimeException $e) {
13+
echo $e->getMessage(), "\n";
14+
}
15+
// failed autoloads are not cached, so the loader runs again
16+
try {
17+
missing_func();
18+
} catch (RuntimeException $e) {
19+
echo $e->getMessage(), "\n";
20+
}
21+
22+
echo "== dynamic propagates ==\n";
23+
$f = 'missing_func';
24+
try {
25+
$f();
26+
} catch (RuntimeException $e) {
27+
echo $e->getMessage(), "\n";
28+
}
29+
spl_autoload_unregister_function_loader($thrower);
30+
31+
echo "== callable resolution wrapping ==\n";
32+
// is_callable() rethrows the loader's exception directly; the call/closure APIs
33+
// wrap it in a TypeError but keep it as the previous exception. Either way the
34+
// loader's exception must not be swallowed.
35+
$thrower = function (string $name) {
36+
throw new RuntimeException("loader failed");
37+
};
38+
spl_autoload_register_function_loader($thrower);
39+
function check(callable $cb): void {
40+
try {
41+
$cb();
42+
echo "no exception\n";
43+
} catch (\Throwable $e) {
44+
$origin = $e instanceof RuntimeException ? $e : $e->getPrevious();
45+
echo get_class($e), " -> ", get_class($origin), ": ", $origin->getMessage(), "\n";
46+
}
47+
}
48+
check(fn() => is_callable('boom'));
49+
check(fn() => call_user_func('boom'));
50+
check(fn() => Closure::fromCallable('boom'));
51+
spl_autoload_unregister_function_loader($thrower);
52+
53+
echo "== silent decline retries ==\n";
54+
// A name that fails to load is retried; there is no negative cache.
55+
// A loader that silently declines (no throw, no definition) does not poison the
56+
// name, so a name unloadable now may become loadable later. (The throwing-loader
57+
// retry is covered by the "direct propagates" section above.)
58+
$attempts = 0;
59+
$enabled = false;
60+
$loader = function (string $name) use (&$attempts, &$enabled) {
61+
if ($name !== 'late_func') {
62+
return;
63+
}
64+
$attempts++;
65+
echo "attempt $attempts\n";
66+
if ($enabled) {
67+
eval('function late_func() { return "loaded on retry"; }');
68+
}
69+
// otherwise decline silently: no exception, no definition
70+
};
71+
spl_autoload_register_function_loader($loader);
72+
try {
73+
late_func();
74+
} catch (Error $e) {
75+
echo $e->getMessage(), "\n";
76+
}
77+
try {
78+
late_func();
79+
} catch (Error $e) {
80+
echo $e->getMessage(), "\n";
81+
}
82+
$enabled = true;
83+
var_dump(late_func());
84+
echo "attempts: $attempts\n";
85+
spl_autoload_unregister_function_loader($loader);
86+
?>
87+
--EXPECT--
88+
== direct propagates ==
89+
Autoload failed for: missing_func
90+
Autoload failed for: missing_func
91+
== dynamic propagates ==
92+
Autoload failed for: missing_func
93+
== callable resolution wrapping ==
94+
RuntimeException -> RuntimeException: loader failed
95+
TypeError -> RuntimeException: loader failed
96+
TypeError -> RuntimeException: loader failed
97+
== silent decline retries ==
98+
attempt 1
99+
Call to undefined function late_func()
100+
attempt 2
101+
Call to undefined function late_func()
102+
attempt 3
103+
string(15) "loaded on retry"
104+
attempts: 3
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
Function autoloading: the four namespace resolution scenarios for an unqualified call
3+
--FILE--
4+
<?php
5+
namespace App;
6+
7+
$count = 0;
8+
\spl_autoload_register_function_loader(function (string $name) use (&$count) {
9+
$count++;
10+
echo "loader($name)\n";
11+
if ($name === 'App\s3') {
12+
eval('namespace App; function s3() { return "ns s3"; }');
13+
} elseif ($name === 'App\s4') {
14+
// Answer a request for App\s4 by defining the GLOBAL function instead.
15+
eval('function s4() { return "global s4"; }');
16+
}
17+
});
18+
19+
// 1. App\s1 is already defined: it is called, loader not consulted.
20+
function s1() { return "ns s1"; }
21+
\var_dump(s1());
22+
23+
// 2. Only global s2 is defined: global fallback is used, loader not consulted.
24+
eval('function s2() { return "global s2"; }');
25+
\var_dump(s2());
26+
27+
// 3. Neither defined: loader is called once with App\s3 and defines it.
28+
\var_dump(s3());
29+
\var_dump(s3()); // already defined: no second consult
30+
31+
// 4. Neither defined, loader defines global s4 instead of App\s4: the current
32+
// call still throws (an answer to a different question is not rebound),
33+
// then the next call finds the global through the normal fallback.
34+
try {
35+
\var_dump(s4());
36+
} catch (\Error $e) {
37+
echo $e->getMessage(), "\n";
38+
}
39+
\var_dump(s4()); // global found via fallback: no re-consult
40+
41+
echo "loader calls: $count\n";
42+
\var_dump(\function_exists('App\s4', false)); // no pinning: never became App\s4
43+
?>
44+
--EXPECT--
45+
string(5) "ns s1"
46+
string(9) "global s2"
47+
loader(App\s3)
48+
string(5) "ns s3"
49+
string(5) "ns s3"
50+
loader(App\s4)
51+
Call to undefined function App\s4()
52+
string(9) "global s4"
53+
loader calls: 2
54+
bool(false)

0 commit comments

Comments
 (0)