Re: Module or Class Visibility, Season 2

From: Date: Mon, 26 May 2025 19:39:15 +0000
Subject: Re: Module or Class Visibility, Season 2
References: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
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


Thread (50 messages)

« previous php.internals (#127466) next »