On Mon, May 26, 2025, at 21:39, Alwin Garside wrote:
> 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] <[email protected]> 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
>
I’m starting to think that maybe modules might be a bad idea; or at least, class/module
visibility.
As an anecdote, I was looking to extract a protobuf encoding library from a larger codebase and
create a separate library for Larry’s Serde library. During the extraction I realized that many of
the classes and functions I was relying on actually used @internal classes/functions. If
“module” visibility were a thing… would my implementation have been possible?
In other words, if visibility comes with modules; there really needs to be some kind of escape
hatch. Some way to say, “I know what I’m doing, so get out of my way.”
In Java, you can do this by creating a namespaced class in the target namespace. I’m not sure
about other languages; but it’s something to think about.
— Rob