> On Jul 5, 2024, at 1:47 PM, Michael Morris <tendo...@gmail.com> wrote:
> I went to sleep thinking about this post, on import maps in general and how 
> Composer works, specifically when you use a class map instead of the PSR-0 or 
> PSR-4 schemes.  In that mode, Composer does pretty much what I've described.  
> This got me to thinking, could setting an import map be an alternative to 
> setting an autoload function?  Would having the PHP runtime load the file be 
> faster than mucking with some userland code to do the same? And would the 
> engine be able to do anything that can't be done in userland?  I think so.

I very much like this direction of thought.

For context, when I worked with WordPress for about a decade I only ever used 
only Composer for websites but never for my own plugins, and I almost never 
used namespaces nor PSR-4 autoloaders for anything except when a plugin used 
them.  I almost exclusively used naming convensions for "namespacing" and 
classmaps for autoloading. 

Why? Was it because I was a "bad" programmer?  No, it was because when in Rome, 
you do as the Romans do.  And also because when I tried to use Composer and 
PSR-4 I was always fighting when them to do things in the way that worked best 
for WordPress.

Or to use a more modern analogy to PHP and WordPress, even though you may be 
landlocked by the country of Italy, if you are within the borders of Vatican 
City you follow the laws and conventions of Vatican City when they conflict 
with those of Italy.

> So first, I agree that supporting two formats, while convenient, increases 
> the maintenance burden, so let's just go with ini.  As far as the installing 
> function - a better name is this.

Obviously I agree with having only one format, but not sure I concur with the 
use of .ini. However, me debating against `.ini` would be me bikeshedding and 
thus I will demure here.

> spl_autoload_map( string $filepath );

Adding an `spl_autoload_map()` function and feature really resonates with me.  
As I said, I (almost?) always used class maps with WordPress so if I were to 
build another WordPress site with a future PHP that made an 
`spl_autoload_map()` available, I would TOTALLY use it.

<epiphany>

Reading this however caused me to ponder things certain people has said 
recently — and many people have said for years on this list — and I think I am 
recognizing something that I have always known but never put the pieces 
together before.

Many (most?) people on PHP Internals view WordPress coding standards as bad and 
some even view addressing WordPress developers needs as bad for PHP. And in 
general I concur that those people are reasonably justified in their belief 
WordPress' coding standards are not the standards that PHP developer who want 
to do professional level software engineering should aspire. 

And since many (most?) PHP Internals members generally do not experience the 
issues that WordPress developers have they do not recognize that they are 
issues; IOW, "out of sight, out of mind." 

I also think some list members tend to dismiss WordPress developers pains as 
unimportant and/or think that addressing those pains have will harm PHP. 

(BTW, I recently had a dialog off-list with someone who wrote in an email that 
"Wordpress is an exception, but nobody these days treats WordPress as a valid 
example to do anything. It is an ancient piece of legacy code that has no 
bearing on modern situation and it's their problem to deal with." So I am not 
just erecting a straw man here.)

But I think what most may not consciously recognize is that WordPress is a 
different type of web app than an app build using Symfony or Laravel and 
deployed by its developers, or by some other professional developer. 

WordPress differs from the apps many (most?) developers on PHP Internals work 
with in the following way:

WordPress = User-managed app
Most = Developer-managed apps

In a Developer-Managed app developers choose which 3rd party functionality will 
be incorporated into their sites whereas with a User-managed app users choose 
which 3rd party functionality will be incorporated into their site. And that is 
the KEY difference. 

So I am wondering if we can get people on this PHP Internals list who dismiss 
the needs of WordPress developer BECAUSE it is WordPress to recognize that 
User-Managed apps ARE a class of PHP applications have needs that deserve to be 
addressed?  

Two (2) unmet needs of User-Managed apps that "standard" PHP currently does not 
address come to mind:

User-managed apps needs to be able to handle both:

1. User-added add-ons ("plugins" in WordPress, "modules" in Drupal) that have 
conflicting dependencies, and

2. Add-on directory structures that do not follow a PSR-4 directory hierarchy.

As for #2, even if those apps could rearchitect their existing directory 
structure they cannot realistically be expected to do with because of the huge 
BC issues their users would experience. 

And newly created User-managed apps may still find that a PSR-4 directory 
structure is not in the best interest of their project or their users. To 
elaborate, PSR-4 generally assumes that ALL code goes into ONE hierarchy and 
that any and all code that will be autoload gets placed in that hierarchy.

But with add-ons it makes a lot more sense to have the entire add-on contained 
in its own add-on directory. This is exactly where PSR-4 breaks down with 
respect to User-managed apps.

Sure, you can have multiple PSR-4 autoloader root directories, but that does 
not scale well to websites with a large number of add-ons as many WordPress 
sites I worked on used. Some had over 100 plugins.  With a hierarchy of 
autoloader maps that Michael Morris is proposing WordPress could collect up all 
the maps and create one map every time a plugin is added, updated or deleted.

</epiphany>

Based on my above labeled epiphany, I think MOST of what you recently proposed 
could address those unmet needs of User-managed apps written in PHP, with a few 
caveats and improvements.  Read on.

>   ; A path fragment can be used, in which case PSR-4 will be used to map the 
> rest of the symbol to the filename.

>   ; Pay attention to the direction of the slash at the tail - if the symbol 
> key has this the value MUST also have this.
>   B/ = './path/to/B/'

It is not clear to me what a trailing slash means, and especially why it is 
needed on the left-hand side? And why slash here when namespaces use backslash?

Also, as someone raised on DOS and then Windows only "converting" in 2009, I 
still get confused in *nix when to use a trailing slash and when to not, so 
this trailing slash worries me, if only for that reason alone.

>   ; A package is declared with a @ and maps the package namespace to its 
> autoload file.
>   ; If the package name here doesn't match what the package calls itself then 
> the symbol
>   ; given here takes precedence, acting as an alias.
>   @C = './path/to/C/autoload.ini'

Using the `@` here feels cryptic, and hard to discover and remember.

I think this would be infinitely easier to follow if packages were just 
included in a `[packages]` section.

Your comments also confuse me a bit.  

Is this saying that your hypothetical app — which you stated this `.ini` file 
is for — needs to use a package named `C` use "definition" is located at 
'./path/to/C/autoload.ini' then it would use this syntax, and that in the app 
its components would be accessed at namespace `\C`?

And I were to have:

@Foo\Bar\Baz = './path/to/Foo/Bar/Baz/autoload.ini'

Then in the app its components would be accessed at namespace `\Foo\Bar\Baz`?

I think if your examples used hypothetical "real-world" symbols it would be 
easier to follow than A, B, C, D, etc.

>   ; An import into a package can be done like so
>   ; Twig will load into \C\Twig and that use will need to be used by any code 
> outside the C package.
>   @C\Twig/ = './path/to/Twig/'
> 
>   ; The same library can be loaded into a different package, but a symbolic 
> link is used internally in the engine to optimize
>   @D\Twig/ = './path/to/Twig/'
> 
>   ; Nothing stops a different package from loading a different version now.
>   @E\Twig/ = './path/to/Twig/Version4/'

Okay, this makes sense.  OTOH, this is the part that of your proposal that is 
incomplete for the needs of User-managed apps IMO.

I think you are implying a necessary "best practice" that whenever any PHP 
library, or package would include code they would need to prefix the namespace 
of package when importing it and then when using it. Given an org named ACME 
that released a library called Widgets then if it were to use Twig it should 
import and use Twig like this (did I understand your intent correctly?):

@ACME\Widgets\Twig/ = './path/to/Twig/'

And in PHP code?:

use \ACME\Widgets\Twig;

I think that would work well for newer libraries and packages authored and used 
by developers of Developer-managed apps. OTOH I do not think it would be 
sufficient for any existing libraries or frameworks, nor for non-professional 
developers scratching their own itch on a User-managed apps and then deciding 
to publish it for others to use (which happens a lot with User-managed apps.)

The problem would be that most (all?) of those would not be namespace-prefixing 
Twig but instead using it directly. I believe you need an ADDITIONAL `replace` 
sectionS that allowed an app/website developer to indicate that namespace A 
should instead be replaced in `use` statements and direct references with `B\A` 
for code that exists in directory(s) `C` but not in directories `C\D` where `C` 
and `D` can be globs.

To illustrate I created a completely hypothetical `.ini` that the WordPress 
plugin admin page could create any time a user would install WordPress and any 
time the user would add/edit/delete plugins or themes (I did try to modifying 
your A/B/C example but couldn't come up with anything that could illustrate the 
use-case):

[default]
root = '/wp-content/'

[includes]
UpdraftPlus = './plugins/updraft-plus/index.php'        # Uses Twig v4
Automattic\JetPack = './plugins/jetpack/jetpack.php'    # Uses Twig
Elementor = './plugins/elementor/elementor.php'         # Uses Twig v4

[packages]
Yoast\SEO = './plugins/yoast-seo/autoload.ini'   # Uses Twig v4 as Yoast\Twig
WPForms = './plugins/wp-forms/autoload.ini'      # Uses Twig as WPForms\Twig

[replace]
Twig[UpdraftPlus] = 'Twig_edaf27eb'
Twig[Elementor] = 'Twig_edaf27eb'

In the above example `[packages]` are ones that have gotten religion and have 
delivered a best-practices package where they have namespaced Twig.  We can 
ignore them for now as they follow your best-practices. 

The `[includes]` are ones that have paid no attention to newer best practice 
and/or simply have not been updated by their authors. They were implemented to 
load and use Twig as simply `\Twig`.

The `[replace]` section tells PHP that when `\Twig` is found included or used 
within the `[includes]` files denoted by those namespaces referenced such as 
`UpdraftPlus` it should instead use the namespace of `\Twig_edaf27eb` 
(dynamically generated by WordPress), and the same goes for any includes and 
uses by `Elementor`.

My hypothetical design may not survive a fully-working implementation, but I 
hope it illustrates that we need to:

1.) Handle those who are NOT following best practices, AND 
2.) Alias namespaces when used IN ADDITION TO when imported.

Of course if the code in any of the three plugins included expect the 
namespaces to be exact via reflection then they would break, but I think it 
would be a reasonable breakage as most plugins won't do this and most plugins 
that break could either be updated by their authors or disabled for installs on 
newer PHP by the WordPress plugin repo.

BTW, Go uses `replace` in `go.mod` albeit as a compiled language its use is not 
a one-to-one analogue to the example above. If you are interested in seeing 
them in the wild here is what the use of `replace` looks like for Kubernetes: 
https://github.com/kubernetes/kubernetes/blob/master/go.mod#L227-L258

As an "optimization", WordPress could recognize that Twig and Twig4 are being 
used not only by the includes but also by the packages and could generate this 
optimization instead:

[default]
root = '/wp-content/'

[includes]
UpdraftPlus = './plugins/updraft-plus/index.php'        # Uses Twig v4
Automattic\JetPack = './plugins/jetpack/jetpack.php'    # Uses Twig
Elementor = './plugins/elementor/elementor.php'         # Uses Twig v4

[packages]
Yoast\SEO = './plugins/yoast-seo/autoload.ini'   # Uses Twig v4 as Yoast\Twig
WPForms = './plugins/wp-forms/autoload.ini'      # Uses Twig as WPForms\Twig

[replace]
Twig[UpdraftPlus] = 'Twig_edaf27eb'
Twig[Elementor] = 'Twig_edaf27eb'
Twig[Yoast\SEO] = 'Twig_edaf27eb'

Twig[Automattic\JetPack] = 'Twig_2ba3f91f'
Twig[WPForms] = 'Twig_2ba3f91f'
As a further optimization, WordPress could reach into all the `.ini` files 
recursively and create a SINGLE autoload map, but to do that we would need an 
additional section: `[ignore]`: 
[ignore]
Yoast\SEO = './plugins/yoast-seo/autoload.ini'   
WPForms = './plugins/wp-forms/autoload.ini'      
The above assumes that WordPress already generated everything that was needed 
to no longer need to load the autoload.ini files for either the `Yoast\SEO` or 
`WPForms` namespaces and thus the ignore tells `spl_autoload_map()` to ignore 
any calls to these `autoload.ini` files if called later.  (I would have created 
a complete example but at this point I am too tired for that.)

And lastly, because WordPress would need to generate this and having a web app 
write to a file is a modern security no-no, then `spl_autoload_map()` should 
accept multiple different valid values:

spl_autoload_map( string|array|\PHP\AutoloadMap $map);

1. String would be the `.ini` file path

2. Array would be the format returned by parse_ini_file() for parsing an 
applicable `.ini` file

3. \PHP\AutoloadMap could be a new class containing the required values in 
object format. (Hopefully adding such a class as a third option would not be 
controversial to the list members who criticize those developers still wanting 
to use arrays as hash maps?)

And that is about it for my feedback today.

-Mike

Reply via email to