Hi all,
This is (by my count) a 5th attempt at adding function autoloading to PHP.
- RFC: https://wiki.php.net/rfc/function-autoloading-five-oh
- PR: https://github.com/php/php-src/pull/22337
Looking forward to the discussion around this one!
-- pmj
Hey Paul,
Hi all,
This is (by my count) a 5th attempt at adding function autoloading to PHP.
- RFC: https://wiki.php.net/rfc/function-autoloading-five-oh
- PR: https://github.com/php/php-src/pull/22337
Looking forward to the discussion around this one!
-- pmj
What is the motivation to not do a second call to the function
autoloaders with "bar" (global namespace) when "bar()" is being called
in the Foo namespace and the initial call with "Foo\bar" doesn't resolve
to anything?
That would probably have much less rough edges, completely sidestepping
the issue here with Foo\bar() not being found.
Thanks,
Bob
Hi Bob,
(Sorry for the very long delay on this reply.)
What is the motivation to not do a second call to the function autoloaders with "bar" (global namespace) when "bar()" is being called in the Foo namespace and the initial call with "Foo\bar" doesn't resolve to anything?
If I understand your question and the engine itself correctly:
The function autoloader only fires after the name resolution has occurred and no global fallback has been found. That final resolution of the name (the global one) is the only one that can be passed to the autoloader at that point. The original namespaced name just isn't available. That is: the resolver looks for the namespaced function, sees it is not defined, looks for a global name to fall back to, that one isn't defined, and then an error gets raised; the function autoloader kicks in at the moment the error would have been raised.
As far as I can tell, to get access to the original namespaced name, the implementation would have to hook into the name resolution logic, and that carries a more substantial set of troubles. Cf. these notes from years ago:
- S. Malyshev, 2013: https://externals.io/message/68693
- N. Popov, 2016: https://externals.io/message/94533 and https://externals.io/message/94897
- T. Brown, 2019: https://externals.io/message/105757
It looks like putting function autoloading the name resolution logic is huge a performance drag.
That would probably have much less rough edges, completely sidestepping the issue here with Foo\bar() not being found.
There is the possibility of something like declare(strict_namespace=1) to avoid the global fallback entirely, which would give the autoloader the namespaced function name instead of the global one. (This is pretty much what Brown suggests above.) Do you feel that's a viable path forward?
-- pmj
Hi all,
This is (by my count) a 5th attempt at adding function autoloading to PHP.
- RFC: https://wiki.php.net/rfc/function-autoloading-five-oh
- PR: https://github.com/php/php-src/pull/22337
Looking forward to the discussion around this one!
-- pmj
I assume you saw my last attempt at this? :)
The general consensus was that spl autoloader needs to go and nobody wanted to see more added to it. That being said, maybe the list has changed its mind... hopefully.
— Rob
Hi Rob,
I assume you saw my last attempt at this? :)
I did, I took notes!
The general consensus was that spl autoloader needs to go and nobody wanted to see more added to it. That being said, maybe the list has changed its mind... hopefully.
One hopes.
-- pmj
Hi all,
This is (by my count) a 5th attempt at adding function autoloading to PHP.
- RFC: https://wiki.php.net/rfc/function-autoloading-five-oh
- PR: https://github.com/php/php-src/pull/22337
Looking forward to the discussion around this one!
-- pmj
Hi Paul!
To make unqualified calls to
strlen()resolve to Foo\strlen() inside the Foo namespace, you must import the fully qualified function first
Imagine we have an old guzzle/psr7 with stream_for function, and they
use it without a qualifier. I defined a global stream_for so now I
hijacked the internal library logic. Is it correct?
The previous attempt with namespace pinning made more sense to me
--
Anton
Imagine we have an old guzzle/psr7 with stream_for function, and they use it without a qualifier. I defined a global stream_for so now I hijacked the internal library logic. Is it correct?
The previous attempt with namespace pinning made more sense to me
The concern that occurred to me is similar: if I rely on function autoloading, but forget to fully-qualify a name in the current namespace, it would accidentally work as long as some other code triggered loading of that function first. Then some small refactoring - or even a different path through the code at runtime - and it will silently fall back to a function in the global namespace instead.
I think if autoloading is going to require some extra "ceremony" in the code, it would be preferable to have a declare() at the top of the file forcing the interpretation of all unqualified names (either "always assume current namespace" or "always assume global namespace"). That way, code would not change behaviour based on hard-to-predict side effects elsewhere.
Rowan Tommins
[IMSoP]
Hi all,
Imagine we have an old guzzle/psr7 with stream_for function, and they use it without a qualifier. I defined a global stream_for so now I hijacked the internal library logic. Is it correct?
The previous attempt with namespace pinning made more sense to me
The concern that occurred to me is similar: if I rely on function autoloading, but forget to fully-qualify a name in the current namespace, it would accidentally work as long as some other code triggered loading of that function first. Then some small refactoring - or even a different path through the code at runtime - and it will silently fall back to a function in the global namespace instead.
I think if autoloading is going to require some extra "ceremony" in the code, it would be preferable to have a declare() at the top of the file forcing the interpretation of all unqualified names (either "always assume current namespace" or "always assume global namespace"). That way, code would not change behaviour based on hard-to-predict side effects elsewhere.
Theodore Brown mentioned something like that as well in https://externals.io/message/105757.
Please see the following PR against my own branch to see what it would look like if added to the function-autoloading (mark 5) branch.
https://github.com/pmjones/php-src/pull/1
Hypothetically this could be its own separate, precursor RFC and PR, but it's small enough it might go well here.
Thoughts?
-- pmj
Please see the following PR against my own branch to see what it would look like if added to the function-autoloading (mark 5) branch.
https://github.com/pmjones/php-src/pull/1
Hypothetically this could be its own separate, precursor RFC and PR, but it's small enough it might go well here.
Interesting that it's such a small change. My only comment would be that if we do add this, let's please not use the word "strict"; it tends to lead to incorrect assumptions like "strict is always better".
Some rather verbose names were suggested during the discussion of https://wiki.php.net/rfc/use_global_elements
My preference would be a 3-way switch, e.g.
declare(function_const_ns='global');
declare(function_const_ns='current');
declare(function_const_ns='fallback');
I note that previous RFC was fairly comprehensively rejected, though, so you'd have to dig into why to figure out if it's worth resurrecting.
Rowan Tommins
[IMSoP]
Hi
Am 2026-06-19 13:31, schrieb Rowan Tommins [IMSoP]:
Interesting that it's such a small change. My only comment would be
that if we do add this, let's please not use the word "strict"; it
tends to lead to incorrect assumptions like "strict is always better".
For reference, I did something similar a year ago in
https://github.com/TimWolla/php-src/commit/aa6fc0c6ef3661e2fea4043ff1e07897952da2a5,
with an additional variant of erroring when no explicit decision is
made, which could also be a precusor to deprecate the global fallback
entirely.
Best regards
Tim Düsterhus
Hi Tim,
Hi
Am 2026-06-17 16:47, schrieb Rowan Tommins [IMSoP]:
The concern that occurred to me is similar: if I rely on function autoloading, but forget to fully-qualify a name in the current namespace, it would accidentally work as long as some other code triggered loading of that function first. Then some small refactoring - or even a different path through the code at runtime - and it will silently fall back to a function in the global namespace instead.
Agreed on this being a non-starter for the proposal for me.
I don't especially like it myself. FWIW, I don't actually see a lot of namespaced calls that this would fumble on (the vast majority of function autoloading I think would apply to global user functions based on my legacy work) but it's still kind of clunky.
As we know, it's caused by the namespace resolution rules, and I'd rather not take those on directly; the strict_namespace declaration might help to soothe it. Speaking of which:
Hi
Am 2026-06-19 13:31, schrieb Rowan Tommins [IMSoP]:
Interesting that it's such a small change. My only comment would be that if we do add this, let's please not use the word "strict"; it tends to lead to incorrect assumptions like "strict is always better".
For reference, I did something similar a year ago in https://github.com/TimWolla/php-src/commit/aa6fc0c6ef3661e2fea4043ff1e07897952da2a5, with an additional variant of erroring when no explicit decision is made, which could also be a precusor to deprecate the global fallback entirely.
Do you feel force_global_function_fallback=false and strict_namespace=1 are substantially the same thing, or are they different in some way?
-- pmj
Hi
Am 2026-06-17 16:47, schrieb Rowan Tommins [IMSoP]:
The concern that occurred to me is similar: if I rely on function
autoloading, but forget to fully-qualify a name in the current
namespace, it would accidentally work as long as some other code
triggered loading of that function first. Then some small refactoring -
or even a different path through the code at runtime - and it will
silently fall back to a function in the global namespace instead.
Agreed on this being a non-starter for the proposal for me.
Best regards
Tim Düsterhus
Hi Paul,
Looking forward to the discussion around this one!
I've very much in favour of autoloading. Please can you address Bob's
concern:
Bob Weinand wrote:
What is the motivation to not do a second call to the function
autoloaders with "bar" (global namespace) when "bar()" is being called
in the Foo namespace and the initial call with "Foo\bar" doesn't resolve
to anything?That would probably have much less rough edges, completely sidestepping
the issue here with Foo\bar() not being found.
I've also previously been involved in an RFC attempt for function
autoloading. I'm pretty certain that I came to the conclusion that for this
edge case; that the function autoloader needed to be called twice to have
the least surprising behaviour.
btw I don't think the RFC makes a strong case for why it is beneficial -
which keeps the RFC text short, but might leave people wondering why it is
needed. Some people don't see that function loading is useful (in exactly
the same way people didn't think class autoloading was useful) and so
aren't prepared to accept any trade-off. There is an overlapping problem of
people who are not maintainers pooh-poohing ideas just because they don't
personally want to use them.
It probably shouldn't be part of this RFC, but being able to bind closure
to functions, ala run runkit7-function-add^1 would taken function
autoloading from a medium useful thing, to a very powerful piece^2 of
technology.
Though as PIE https://github.com/php/pie exists (even though the PHP
extension ecosystem is "not a great experience") the argument for putting
even 'trivial' function into core might not be strong.
cheers
Dan
Ack
^1 the original runkit7_function_add is somewhat astonishing, with two
completely different function signatures in the same function. I've
stripped out to be a single function, without the operator overloading
here: https://github.com/Danack/fnbind/blob/main/fnbind.stub.php as an
extension.
^2 - I previously wrote some words on why function autoloading could be
powerful. Most of it still makes sense. If I had the energy, I'd add some
more words about 'making it easier to introduce complexity in a way that is
sanely hidden from users', which is good both for newbies learning to code,
and teams with a large codebases.
Okay, so to try to explain what I want to use it for and why....
- Auto-loading sometimes just isn't worth it
- Configurable per namespace functions are cool.
- Stuff I wouldn't personally condone using.
Auto-loading sometimes just isn't worth it
So, this is something I've been thinking about since I gave some talks
about how awesome dependency is. In particular, one bit of the talk is
about the downsides of dependency injection.
This slide
and the next one, show how easy 'bad' code is to use compared to
'dependency injected' code.
Although I told people in the talk that you get used to, I actually got
really annoyed at this overhead when I was actually writing tests for
'properly' written code.
Having to create and inject a logger class in every single test is just not
good value for money.
It kind of pains me to say it, but the Laravel way of using 'facades' aka
global functions hidden behind a static class is probably a better way of
doing things for certain types of object, particularly those where you are
only going to have one (or maybe a couple) of types of object in a given
environment.
For example, in a testing environment I would only use an in-memory logger,
and so I'd have some code like this:
// Boring logger
interface Logger
{
public function log(string $level, string $message);
}
// Logger used for tests.
class InMemoryDevLogger implements Logger
{
private $log_entries = [];
public function log(string $level, string $message) {
$this->log_entries[] = [$level, $message];
frwite(STDERR, $level . " : " . $message);
}
public function findMessage(string $pattern): array|null {
foreach ($this->log_entries as $log_entry) {
[$level, $message] = $log_entry;
if (preg_match($pattern, $message) === 1) {
return [$level, $message];
}
}
return null;
}
}
And then if you had some code that needed testing:
// Currently, doing everything with dependency injection.
function do_the_needful(
UserParams $user,
UserRepo $userRepo,
Logger $logger
) {
$user = $userRepo->findUser($user->id);
if ($user === null) {
$logger->log("INFO", "User not found");
}
$logger->log("INFO", "User found, about to foo User");
$result = fooUser($user);
if ($result !== true) {
$logger->log("INFO", "Failed to foo user.");
return new ErrorResponse();
}
$logger->log("INFO", "User has been foo'd.");
return new SuccessResponse();
}
You would write the test as:
class SomeTest {
function test_do_the_needful_UserNotFound()
{
$userParams = createFakeUserParams();
$emptyRepo = new EmptyUserRepo();
$logger = new InMemoryLogger();
$result = do_the_needful(
$userParams,
$emptyRepo,
$logger
);
$this->assertInstanceOf($result, ErrorResponse::class);
$messageFound = $logger->findMessage(/* Some appropriate pattern
*/);
$this->assertTrue($messageFound);
}
}
This isn't the worst thing in the world....but it's just tedious.
Why not just use Laravel style 'facades' ?
One of the really nice thing about using DI is that all of the config for
your application can be at the top-level of your app in a single place.
That much nicer than using a service locator where the creation of objects
can be all over the place.
It's also really nice to have a powerful enough config system to avoid
needing separate 'dev' config, and 'prod' config.
So, I'd really like to be able to setup a function autoloader like this:
function bindLoggerFunctions()
{
if (ENVIRONMENT == "DEV") {
$logger = new InMemoryLogger();
function_add('log', $logger->log(...));
function_add('log_search', $logger->findMessage(...));
return;
}
if (ENVIRONMENT == "PROD") {
$logger = createMonologLogger();
function_add('log', $logger->log(...));
function_add('log_search', log_search_not_allowed(...));
}
throw \Exception("Unknown env: " . ENVIRONMENT);
}
function loader($name) {
if ($name === 'log' || $name === 'log_search') {
bindLoggerFunctions();
}
}
Which would allow me to reduce my code to:
function do_the_needful(
UserParams $user,
UserRepo $userRepo
) {
$user = $userRepo->findUser($user->id);
if ($user === null) {
log("INFO", "User not found");
}
log("INFO", "User found, about to foo User");
$result = fooUser($user);
if ($result !== true) {
log("INFO", "Failed to foo user.");
return new ErrorResponse();
}
log("INFO", "User has been foo'd.");
return new SuccessResponse();
}
And would reduce the test code to by a bit:
class SomeTest {
function test_do_the_needful_UserNotFound()
{
$userParams = createFakeUserParams();
$emptyRepo = new EmptyUserRepo();
$result = do_the_needful(
$userParams,
$emptyRepo
);
$this->assertInstanceOf($result, ErrorResponse::class);
$messageFound = log_search(/* Some appropriate pattern */);
$this->assertTrue($messageFound);
}
}
It's not the biggest thing in the world, and it's really easy for
code-purists to say "This isn't a big enough saving to be worth the
hackery" but anything that reduces the costs of writing tests is worth at
least some amount of tradeoff.
I think this pattern would only be worth using for any type of object that
never had any different implementations used in a test environment. So for
example, a UserRepo is going to have different implementations of things
like:
- EmptyUserRepo - for testing users not found
- SeededUserRepo - that contains prepared test accounts.
- ErrorUserRepo - all operations throw exceptions.
So that wouldn't be a good match for function binding....but for loggers,
there just doesn't seem to be any value in 'doing it properly'.
Configurable per namespace functions are cool
Okay, so the second thing that would be possible (and is slightly
contradictory to the first one), is that
namespace {
funtion getLogClassNameForNamespace($namespace) {
if ($namespace === 'Foo') {
// For some reason, we care about Info log level stuff
// in the Foo namespace.
return InfoLogger::class;
}
// For everything else, we only care about warning.
return WarningLogger::class;
}
function loader($name) {
global $injector;
$namespace_parts = explode('\\', $name);
$function_name = array_pop($namespace_parts);
$namespace = implode($namespace_parts);
if ($function_name === 'log') {
$loggerClassname = getLogClassNameForNamespace($namespace);
$logger = $logger->make($loggerClassname);
function_bind($name, $logger->log(...));
}
}
autoload_register_function('loader');
}
namespace Foo {
log('Info', "Hello there sailor!");
}
namespace Bar {
log('Info', "Hello there sailor!");
}
- Stuff I wouldn't personally condone using
So I don't think I would ever code something like this:
function add($x, $y) {
return $x + $y;
}
function loader($name) {
if (str_starts_with($name, "add_") === true) {
$number = substr($name, strlen("add_"));
$number = (int)$number;
$fn = function ($x) use ($number) {
return add($x, $y);
}
function_create("add_", $fn);
}
}
autoload_register_function('loader');
$result = add_2(2);
But I have a suspicion people would be able to create some interesting
stuff.
Hi Dan,
(Thanks for your patience while waiting on my reply.)
I've very much in favour of autoloading. Please can you address Bob's concern:
Replied, and (I hope) addressed -- I'm sure he will say if not.
I've also previously been involved in an RFC attempt for function autoloading. I'm pretty certain that I came to the conclusion that for this edge case; that the function autoloader needed to be called twice to have the least surprising behaviour.
Does the suggestion of declare(strict_namespace=1) (suggested in various forms by others as well), to disable the global fallback on unqualified function and constant names, sound like a reasonable alternative?
btw I don't think the RFC makes a strong case for why it is beneficial - which keeps the RFC text short, but might leave people wondering why it is needed. Some people don't see that function loading is useful (in exactly the same way people didn't think class autoloading was useful) and so aren't prepared to accept any trade-off. There is an overlapping problem of people who are not maintainers pooh-poohing ideas just because they don't personally want to use them.
Fair enough -- I did want to keep it short, but I can add some "motivation" narrative. I see your notes appended, I will incorporate them as I am able.
As long as we're here:
One of the early steps in the MLAPHP process is to convert functions to static class methods. The idea is not to make the code "object- or class-oriented" but to make it "autoloadable." Doing so means you can get rid of include statements, thereby removing any side effects that might come includes. This makes the logical flow more comprehensible.
This conversion process is tedious and time consuming, even with artificial assistance. It would be much better if the functions could be autoloaded directly, and the include statements removed without having to do a conversion. The functions could be moved to an autoloadable location, leaving only side-effects (if any) in the included files. The whole refactoring process becomes much more tractable.
So in a way, a big motivation is "get rid of as many include statements as possible".
-- pmj