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](http://docs.basereality.com/InterfaceSegregationPortsmouth/#/42) 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: ```php // 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: ```php // 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: ```php 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: ```php 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: ```php 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: ```php 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 ```php 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: ```php 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.
