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.