> 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