On Thu, May 22, 2025 at 4:29 PM Rowan Tommins [IMSoP] <imsop....@rwec.co.uk>
wrote:

> On 22/05/2025 12:09, Michael Morris wrote:
>
>
>> I've tried several times to explain why I think Linux containers are a
>> good analogy; I'm not sure if you didn't understand, or just didn't agree,
>> so I don't know what else I can say.
>>
>
> I have no disagreement with that, but it's an implementation detail. I'm
> not there yet - I'm just trying to describe what I think is needed from
> outside the engine.
>
>
> I think this is where we're not seeing eye to eye, and why we're getting
> frustrated with each other, because I see it as far more fundamental than
> details you have already gone into, like how autoloading will work.
>
> Perhaps a more realistic example will help, and also avoid the confusion
> over "A, B, and D" from earler.
>
> Imagine a WordPress plugin, AlicesCalendar, which uses the Composer
> packages monolog/monolog and google/apiclient. The google/apiclient package
> also requires monolog/monolog.
>
> Another WordPress plugin, BobsDocs, also uses both monolog/monolog and
> google/apiclient, but using different versions.
>
>
> Inside those different places, there are lines of code like this:
>
> $logger = new \Monolog\Logger('alices-calendar'); // in AlicesCalendar
> $logger = new \Monolog\Logger('bobs-docs'); // in BobsDocs
> $logger = new \Monolog\Logger('google-api-php-client'); // in
> google/apiclient
>
> We need to rewrite those lines so that they all refer to the correct
> version of Monolog\Logger.
>
>
> If every package/module/whatever rewrites the classes inside every other
> package/module/whatever, we might start with this:
>
> $logger = new \AlicesCalendar\Monolog\Logger('alices-calendar'); // in
> AlicesCalendar
> $logger = new \BobsDocs\Monolog\Logger('bobs-docs'); // in BobsDocs
> $logger = new \GoogleApiClient\Monolog\Logger('google-api-php-client'); //
> in google/apiclient
>
>
> That only works if we somehow know that AlicesCalendar and BobsDocs use
> the same google/apiclient; if not, we need four copies:
>
> $logger = new \AlicesCalendar\Monolog\Logger('alices-calendar'); // in
> AlicesCalendar
> $logger = new
> \AlicesCalendar\GoogleApiClient\Monolog\Logger('google-api-php-client'); //
> in google/apiclient when called from AlicesCalendar
>
> $logger = new \BobsDocs\Monolog\Logger('bobs-docs'); // in BobsDocs
> $logger = new
> \BobsDocs\GoogleApiClient\Monolog\Logger('google-api-php-client'); // in
> google/apiclient when called from BobsDocs
>
> All of these are separate classes, which can't be used interchangeably,
> and the names get longer and longer to isolate dependencies inside
> dependencies.
>
>
> But we don't actually need the Monolog\Logger used by AlicesCalendar to be
> a different version from the one used by google/api-client. In fact, it
> would be useful if they were the same, so we could pass around the objects
> interchangeably *inside* the plugin code.
>
> So what we want is some way of saying that AlicesCalendar and BobsDocs are
> special; they want to isolate code in a way that normal
> modules/packages/whatever don't. Then we can have 2 copies of
> Monolog\Logger, not 3 or 4:
>
> $logger = new \AlicesCalendar\Monolog\Logger('alices-calendar'); // in
> AlicesCalendar
> $logger = new \AlicesCalendar\Monolog\Logger('google-api-php-client'); //
> in google/apiclient when called from AlicesCalendar
>
> $logger = new \BobsDocs\Monolog\Logger('bobs-docs'); // in BobsDocs
> $logger = new \BobsDocs\Monolog\Logger('google-api-php-client'); // in
> google/apiclient when called from BobsDocs
>
> In this case, PHP doesn't need to know monolog/monolog even exists. It
> just puts either "AlicesCalendar" or "BobsDocs" on any class name it sees.
>
>
> Before we can even think about *how* we'd implement the rewriting (or
> shadowing, or whatever) we need some requirements of *what* we want to
> rewrite. By suggesting an image of "containers" or "sandboxes" rather than
> "packages" or "modules", I was trying to define the requirement that
> "AlicesCalendar and BobsDocs are special, in a way that monolog/monolog and
> google/apiclient are not".
>
This is worlds better, and I think I can work with this.

First, let's revisit how autoloading works, if for no other reason than to
test if *I* understand what's going on correctly.  When PHP encounters a
symbol it doesn't recognize, it triggers the autoload process.  Autoloaders
are closures registered with the engine using spl_autoload_register, and
PHP queries them one at a time (I don't remember the order offhand). The
autoloader function runs and PHP retests to see if it can resolve the
symbol. If it can, code execution continues. If it can't the next
autoloader is ran and if none are left a Fatal Error is thrown. Autoload
closures get 1 argument - the fully qualified class name. They are expected
to return void.

I believe it would be best to leave the wild and wooly world of package
management alone and just give the engine the ability to allow code in one
area to use a different code even though it has the same label, at least on
the surface.  I think this is possible if the engine handles the symbol
assignment in a different way from the existing include statements.  The
cleanest way to do that would be to have the autoloader return the file
path to require and, optionally, what namespace to prefix onto all
namespaces in the file.

In summary, let the package manager resolve packages and give it better
tools towards that end.

Returning to your example and closing question, how do we know that
AlicesCalendar and BobsDocs are special? Let the package manager tell us
with this hook:

spl_package_register( array[string] $packages):void

To use composer the user has to run `require
"/vendor/composer/autoload.php";` near the beginning of their application.
So inside that file a package aware version of composer can call this to
tell the engine what the package namespaces are - in your example
['AlicesCalendar', 'BobsDocs'].  (Aside, if spl_package_register is called
multiple times the arrays are merged).

Now, PHP executes the application and enters the code of AlicesCalendar, it
will be largely unchanged:

```php
namepace AlicesCalendar;

$logger = new Monolog\Logger('alices-calendar');
$api = new Google\ApiClient();
```

But thanks to the spl_package_register hook the engine knows that when it
sees a namespace that starts with or matches any string in the packages
array that the code is part of a package. This will cause it to sent the
autoload closure a second argument with that package namespace so that it
can determine what to send back.

So next it sees the Monolog\Logger symbol.
Does AlicesCalendar\Monolog\Logger exists? No, so we invoke the autoloader
callback with arguments ('AlicesCalendar\Monolog\Logger',
'AlicesCalendar').  The autologger checks its rules (way, way out of scope
here) and determines that AlicesCalendar is using the latest
Monolog\Logger.  So it responds with
['file/path/to/latest/Monolog/Logger.php', ''], telling the engine what
code to require and that there is no prefix for the namespaces appearing in
that file ( "\" should also work). The engine aliases
AlicesCalendar\Monolog\Logger to \Monolog\Logger so it doesn't have to
pester the autoloader again for this symbol.
The Google\ApiClient goes through the same process. As a result:

```php
namepace AlicesCalendar;

$logger = new Monolog\Logger('alices-calendar');
$api = new Google\ApiClient();

echo $logger::class // \Monolog\Logger
echo $api::class // \Google\ApiClient
```

Now for the complexity - we reach BobsDocs

```php
namespace BobsDocs;

$logger = new Monolog\Logger('bobs-docs')
$api = new Google\ApiClient();
```

Bobs docs needs an older version of Monolog and is configured appropriately
in its composer.json file, so when the engine calls the autoloader with
('BobsDocs\Monolog\Logger', 'BobsDocs') the autoloader returns
['file/path/to/older/Monolog/Logger.php', 'v1']. v1 is prefixed to the
namespace declarations in Monolog\Logger and the file is included. The
engine aliases BobsDocs\Monolog\Logger to \v1\Monolog\Logger.

Keep in mind - namespace prefix is a decision left to the package manager.
I'm sure a PSR will be made to establish best practice, but that's out of
scope here.

The Googl\ApiClient of BobDocs is again, up to the autoloader. Assuming it
too is different (since it's using an older Monolog) we'd get something
like this.

```php
namespace BobsDocs;

$logger = new Monolog\Logger('bobs-docs')
$api = new Google\ApiClient();

echo $logger::class // \v1\Monolog\Logger
echo $api::class //  \v1\Google\ApiClient
```

Now later in the code if we make a new \Monolog\Logger the autoloader won't
be invoked - the symbol was written when AlicesCalendar caused it to be
created indirectly.

This approach keeps package resolution out of the engine entirely, which I
think is consistent with PHP's setup.  We'd just be improving the tools the
package manager / autoloader can leverage. Older code would still work
since the new autoloader behavior is opt in.

Reply via email to