> On Jul 5, 2024, at 1:47 PM, Michael Morris <[email protected]> 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