Hey all, It took me a while, but I'm finally caught up with this thread, and would like to give my 2 cents.
On 25 May 2025, at 23:17, Rowan Tommins [IMSoP] <imsop....@rwec.co.uk> wrote: > > On 25/05/2025 21:28, Larry Garfield wrote: >> Even if we develop some way such that in Foo.php, loading the class >> \Beep\Boop\Narf pulls from /beep/boop/v1/Narf.php and loading it from >> Bar.php pulls the same class from /beep/boop/v2/Narf.php, and does something >> or other to keep the symbols separate... Narf itself is going to load >> \Beep\Boop\Poink at some point. So which one does it get? Or rather, >> there's now two Narfs. How do they know that the v1 version of Narf should >> get the v1 version of Poink and the v2 version should get the v2 version. > > > The prefixing, in my mind, has nothing to do with versions. There is no "v1" > and "v2" directory, there are just two completely separate "vendor" > directories, with the same layout we have right now. > > So it goes like this: > > 1. Some code in wp-plugins/AlicesCalendar/vendor/Beep/Boop/Narf.php mentions > a class called \Beep\Boop\Poink > 2. The Container mechanism has rewritten this to > \__Container\AlicesCalendar\Beep\Boop\Poink, but that isn't defined yet > 3. The isolated autoloader stack (loaded from > wp-plugins/AlicesCalendar/vendor/autoload.php) is asked for the original > name, \Beep\Boop\Poink > 4. It includes the file wp-plugins/AlicesCalendar/vendor/Beep/Boop/Poink.php > which contains the defintion of \Beep\Boop\Poink > 5. The Container mechanism rewrites the class to > \__Container\AlicesCalendar\Beep\Boop\Poink and carries on > > When code in wp-plugins/BobsDocs/vendor/Beep/Boop/Narf.php mentions > \Beep\Boop\Poink, the same thing happens, but with a completely separate > sandbox: the rewritten class name is \__Container\BobsDocs\Beep\Boop\Poink, > and the autoloader was loaded from wp-plugins/BobsDocs/vendor/autoload.php In this thread I see a lot of talking about Composer and autoloaders. But in essence those are just tools we use to include files into our current PHP process, so for the sake of simplicity (and compatibility), let's disregard all of that for a moment. Instead, please bear with me while we do a little gedankenexperiment... First, imagine one gigantic PHP file, `huge.php`, that contains all the PHP code that is included from all the libraries you need during a single PHP process lifecycle. That is in the crudest essence how PHP's include system currently works: files get included, those files declare symbols within the scope of the current process. If you were to copy-paste all the code you need (disregarding the `declare()` statements) in one huge PHP file, you essentially get the same result. So in our thought experiment we'll be doing just that. The only rule is that we copy all the code verbatim (again, disregarding the `declare()` statements), because that's how PHP includes also work. ```php <?php // ... namespace Acme; class Foo {} namespace Acme; class Bar { public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; $this->foo = new $fooClass(); } } namespace Spam; use Acme\Bar; class Ham extends Bar {} namespace Spam; use Acme\Bar; class Bacon extends Bar {} // ... ``` Now, the problem here is that if we copy-paste two different versions of the same class with the same FQN into our `huge.php` file they will try to declare the same symbols which will cause a conflict. Let's say our `Ham` depends on one version of `Acme\Bar` and our `Bacon` depends on another version of `Acme\Bar`: ```php <?php // ... namespace Acme; class Foo {} namespace Acme; class Bar { public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; $this->foo = new $fooClass(); } } namespace Spam; use Acme\Bar; class Ham extends Bar {} namespace Acme; class Foo {} // Fatal error: Cannot declare class Foo, because the name is already in use namespace Acme; class Bar { // Fatal error: Cannot declare class Bar, because the name is already in use public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; $this->foo = new $fooClass(); } } namespace Spam; use Acme\Bar; class Bacon extends Bar {} // ... ``` So how do we solve this in a way that we can copy-paste the code from both versions of `Acme\Foo`, verbatim into `huge.php`? Well, one way is to break the single rule we have created: modify the code. What if we just let the engine quietly rewrite the code? Well, then we quickly run into an issue. Any non-symbol references to classes are hard to detect and rewrite, so this would break: ```php <?php // ... namespace Acme; class Foo {} namespace Acme; class Bar { public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; $this->foo = new $fooClass(); } } namespace Spam; use Acme\Bar; class Ham extends Bar {} namespace Spam\Bacon\Acme; // Quietly rewritten class Foo {} namespace Spam\Bacon\Acme; // Quietly rewritten class Bar { public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; // <== Whoops, missed this one!!! $this->foo = new $fooClass(); } } namespace Spam; use Spam\Bacon\Acme\Bar; // Quietly rewritten class Bacon extends Bar {} // ... ``` So let's just follow our rule for now. Now how do we include Foo and Bar twice? Well, let's try Rowan's approach of "containerizing." Let's take a very naive approach to what that syntax might look like. We simply copy-paste the code for our second version of `Acme\Bar` into the scope of a container. For the moment let's assume that the container works like a "UnionFS" of sorts, where symbols declared inside the container override any symbols that may already exist outside the container: ```php <?php // ... namespace Acme; class Foo {} namespace Acme; class Bar { public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; $this->foo = new $fooClass(); } } namespace Spam; use Acme\Bar; class Ham extends Bar {} container Bacon_Acme { namespace Acme; class Foo {} namespace Acme; class Bar { public readonly Foo $foo; public function __construct() { // For some reason, there is a string reference here. Don't ask. $fooClass = '\Acme\Foo'; $this->foo = new $fooClass(); } } } namespace Spam; use Bacon_Acme\\Acme\Bar; class Bacon extends Bar {} // ... ``` That seems like it could work. As you can see, I've decided to use double backspace (`\\`) to separate container and namespace in this example. You may wonder how this would look in the real world, where not all code is copy-pasted into a single `huge.php`. Of course, part of this depends on the autoloader implementation, but let's start with a first step of abstraction by replacing the copy-pasted code with includes: ```php // ... require_once '../vendor/acme/acme/Foo.php'; require_once '../vendor/acme/acme/Bar.php'; namespace Spam; use Acme\Bar; class Ham extends Bar {} container Bacon_Acme { require_once '../lib/acme/acme/Foo.php'; require_once '../lib/acme/acme/Bar.php'; } namespace Spam; use Bacon_Acme\\Acme\Bar; class Bacon extends Bar {} ``` Now what if we want the autoloader to be able to resolve this? Well, once a class symbol is resolved with a container prefix, it would have to also perform all its includes inside the scope of that container. ```php function autoload($class_name) { // Do autoloading shizzle. } spl_autoload_register(); namespace Spam; use Bacon_Acme\\Acme\Bar; class Bacon extends Bar {} // Meanwhile, behind the scenes, in the autoloader: container Bacon_acme { autoload(Acme\Bar::class); } ``` Now this mail is already quite long enough, so I'm gonna wrap it up here and do some more brainstorming. But I hope I may have inspired some of you. All in all, I think my approach might actually work, although I haven't even considered what the implementation would even look like. Again, the main point I want to make is to just disregard composer and the autoloader for now; those are just really fancy wrappers around import statements. Whatever solution we end up with would have to work independently of Composer and/or the autoloader. Alwin