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

Reply via email to