Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions UPGRADING.FUNCTION-AUTOLOADING
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
UPGRADE NOTES: FUNCTION AUTOLOADING

1. New Features
2. Changed Functions
3. New Functions
4. Internal API Changes

========================================
1. New Features
========================================

- Core:
. Added function autoloading support. Register callbacks via
spl_autoload_register_function_loader() that are invoked when an
undefined function is referenced. It is triggered wherever an
undefined function name is resolved: direct calls, dynamic calls
through a string variable ("$func()"), function_exists(), callable
resolution (is_callable(), call_user_func()/call_user_func_array(),
array and sort callbacks such as array_map()/array_filter()/usort(),
callable typed parameters, and Closure::fromCallable()), and the
Reflection API (new ReflectionFunction($name)).
As with class autoloading, it is NOT triggered by contexts that
deliberately avoid autoloading: function_exists($name, false),
is_callable($name, true) (syntax-only checks), and
get_defined_functions(). spl_autoload_call_function_loader() can be
used to trigger function autoloading manually in those situations.

========================================
2. Changed Functions
========================================

- Core:
. function_exists() now accepts an optional bool $autoload parameter.
When true (the default, matching class_exists()), function
autoloading is triggered before the function reports as missing.
BC note: existing function_exists() guards around conditional
function definitions (polyfills) will now consult registered function
loaders before falling through to define the fallback. Pass
function_exists($name, false) to check without autoloading.

========================================
3. New Functions
========================================

- SPL:
. spl_autoload_register_function_loader() registers a function autoloader.
. spl_autoload_unregister_function_loader() unregisters a function autoloader.
. spl_autoload_function_loaders() returns registered function autoloaders.
. spl_autoload_call_function_loader() manually triggers function autoloading.

========================================
4. Internal API Changes
========================================

- Zend:
. Added zend_autoload_function_fcc_map_to_callable_zval_map() as the
function-loader counterpart to the existing
zend_autoload_fcc_map_to_callable_zval_map(). The existing function
keeps its name and behaviour (it backs spl_autoload_functions()), so
there is no break for extensions that call it.
70 changes: 70 additions & 0 deletions Zend/tests/autoload/function_autoload_callable_kinds.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
--TEST--
Function autoloading: method and trampoline loaders
--FILE--
<?php
class StaticLoader {
public static function load(string $name): void {
if ($name === 'demo_func') {
eval('function demo_func() { return "from_static"; }');
}
}
}

class InstanceLoader {
public function load(string $name): void {
if ($name === 'demo_func2') {
eval('function demo_func2() { return "from_instance"; }');
}
}
}

class TrampolineTest {
public function __call(string $name, array $arguments) {
echo "Trampoline for $name: $arguments[0]\n";
}
}

echo "== static method ==\n";
spl_autoload_register_function_loader([StaticLoader::class, 'load']);
var_dump(demo_func());

echo "== instance method ==\n";
$obj = new InstanceLoader();
spl_autoload_register_function_loader([$obj, 'load']);
var_dump(demo_func2());
var_dump(count(spl_autoload_function_loaders()));
spl_autoload_unregister_function_loader([StaticLoader::class, 'load']);
spl_autoload_unregister_function_loader([$obj, 'load']);

echo "== trampoline ==\n";
$o = new TrampolineTest();
$callback1 = [$o, 'trampoline1'];
$callback2 = [$o, 'trampoline2'];
spl_autoload_register_function_loader($callback1);
spl_autoload_register_function_loader($callback2);
spl_autoload_register_function_loader($callback1); // duplicate, ignored
var_dump(count(spl_autoload_function_loaders()));
var_dump(function_exists('demo_func3', true)); // consults both trampolines, stays false
var_dump(spl_autoload_unregister_function_loader($callback1));
var_dump(spl_autoload_unregister_function_loader($callback1));
var_dump(spl_autoload_unregister_function_loader($callback2));
var_dump(spl_autoload_function_loaders());
var_dump(function_exists('demo_func3', true));
?>
--EXPECT--
== static method ==
string(11) "from_static"
== instance method ==
string(13) "from_instance"
int(2)
== trampoline ==
int(2)
Trampoline for trampoline1: demo_func3
Trampoline for trampoline2: demo_func3
bool(false)
bool(true)
bool(false)
bool(true)
array(0) {
}
bool(false)
49 changes: 49 additions & 0 deletions Zend/tests/autoload/function_autoload_chain.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--TEST--
Function autoloading: multiple loaders and prepend order
--FILE--
<?php
echo "== order ==\n";
$l1 = function (string $name) {
echo "loader1: $name\n";
};
$l2 = function (string $name) {
echo "loader2: $name\n";
if ($name === 'demo_func') {
eval('function demo_func() { return "from_loader2"; }');
}
};
$l3 = function (string $name) {
echo "loader3: $name\n"; // never reached: loader2 already defined it
};
spl_autoload_register_function_loader($l1);
spl_autoload_register_function_loader($l2);
spl_autoload_register_function_loader($l3);
var_dump(demo_func());
spl_autoload_unregister_function_loader($l1);
spl_autoload_unregister_function_loader($l2);
spl_autoload_unregister_function_loader($l3);

echo "== prepend ==\n";
$append = function (string $name) {
echo "appended: $name\n"; // never reached: prepended runs first and defines
};
$prepend = function (string $name) {
echo "prepended: $name\n";
if ($name === 'demo_func2') {
eval('function demo_func2() { return "ok"; }');
}
};
spl_autoload_register_function_loader($append);
spl_autoload_register_function_loader($prepend, prepend: true);
var_dump(demo_func2());
spl_autoload_unregister_function_loader($append);
spl_autoload_unregister_function_loader($prepend);
?>
--EXPECT--
== order ==
loader1: demo_func
loader2: demo_func
string(12) "from_loader2"
== prepend ==
prepended: demo_func2
string(2) "ok"
104 changes: 104 additions & 0 deletions Zend/tests/autoload/function_autoload_exceptions.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
--TEST--
Function autoloading: loader exceptions propagate, failures are not cached
--FILE--
<?php
echo "== direct propagates ==\n";
$thrower = function (string $name) {
throw new RuntimeException("Autoload failed for: $name");
};
spl_autoload_register_function_loader($thrower);
try {
missing_func();
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
}
// failed autoloads are not cached, so the loader runs again
try {
missing_func();
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
}

echo "== dynamic propagates ==\n";
$f = 'missing_func';
try {
$f();
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
}
spl_autoload_unregister_function_loader($thrower);

echo "== callable resolution wrapping ==\n";
// is_callable() rethrows the loader's exception directly; the call/closure APIs
// wrap it in a TypeError but keep it as the previous exception. Either way the
// loader's exception must not be swallowed.
$thrower = function (string $name) {
throw new RuntimeException("loader failed");
};
spl_autoload_register_function_loader($thrower);
function check(callable $cb): void {
try {
$cb();
echo "no exception\n";
} catch (\Throwable $e) {
$origin = $e instanceof RuntimeException ? $e : $e->getPrevious();
echo get_class($e), " -> ", get_class($origin), ": ", $origin->getMessage(), "\n";
}
}
check(fn() => is_callable('boom'));
check(fn() => call_user_func('boom'));
check(fn() => Closure::fromCallable('boom'));
spl_autoload_unregister_function_loader($thrower);

echo "== silent decline retries ==\n";
// A name that fails to load is retried; there is no negative cache.
// A loader that silently declines (no throw, no definition) does not poison the
// name, so a name unloadable now may become loadable later. (The throwing-loader
// retry is covered by the "direct propagates" section above.)
$attempts = 0;
$enabled = false;
$loader = function (string $name) use (&$attempts, &$enabled) {
if ($name !== 'late_func') {
return;
}
$attempts++;
echo "attempt $attempts\n";
if ($enabled) {
eval('function late_func() { return "loaded on retry"; }');
}
// otherwise decline silently: no exception, no definition
};
spl_autoload_register_function_loader($loader);
try {
late_func();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
late_func();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
$enabled = true;
var_dump(late_func());
echo "attempts: $attempts\n";
spl_autoload_unregister_function_loader($loader);
?>
--EXPECT--
== direct propagates ==
Autoload failed for: missing_func
Autoload failed for: missing_func
== dynamic propagates ==
Autoload failed for: missing_func
== callable resolution wrapping ==
RuntimeException -> RuntimeException: loader failed
TypeError -> RuntimeException: loader failed
TypeError -> RuntimeException: loader failed
== silent decline retries ==
attempt 1
Call to undefined function late_func()
attempt 2
Call to undefined function late_func()
attempt 3
string(15) "loaded on retry"
attempts: 3
54 changes: 54 additions & 0 deletions Zend/tests/autoload/function_autoload_four_scenarios.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
Function autoloading: the four namespace resolution scenarios for an unqualified call
--FILE--
<?php
namespace App;

$count = 0;
\spl_autoload_register_function_loader(function (string $name) use (&$count) {
$count++;
echo "loader($name)\n";
if ($name === 'App\s3') {
eval('namespace App; function s3() { return "ns s3"; }');
} elseif ($name === 'App\s4') {
// Answer a request for App\s4 by defining the GLOBAL function instead.
eval('function s4() { return "global s4"; }');
}
});

// 1. App\s1 is already defined: it is called, loader not consulted.
function s1() { return "ns s1"; }
\var_dump(s1());

// 2. Only global s2 is defined: global fallback is used, loader not consulted.
eval('function s2() { return "global s2"; }');
\var_dump(s2());

// 3. Neither defined: loader is called once with App\s3 and defines it.
\var_dump(s3());
\var_dump(s3()); // already defined: no second consult

// 4. Neither defined, loader defines global s4 instead of App\s4: the current
// call still throws (an answer to a different question is not rebound),
// then the next call finds the global through the normal fallback.
try {
\var_dump(s4());
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
\var_dump(s4()); // global found via fallback: no re-consult

echo "loader calls: $count\n";
\var_dump(\function_exists('App\s4', false)); // no pinning: never became App\s4
?>
--EXPECT--
string(5) "ns s1"
string(9) "global s2"
loader(App\s3)
string(5) "ns s3"
string(5) "ns s3"
loader(App\s4)
Call to undefined function App\s4()
string(9) "global s4"
loader calls: 2
bool(false)
Loading
Loading