Hi!
It's been a few days since I wanted to send this email to internals, but
real life has been a bit chaotic so I apologize if it comes off as if I
didn't research the archives enough. I glossed over the Module conversation
from 10 months ago and the one that recently surfaced and after deeply
thinking about Rowan's and Larry's comments I wanted to throw this idea
into the pit.
Lets preface the conversation with the fact that 1) a module system for PHP
has been discussed for several years and 2) if there was an easy and
perfect solution it would have been long implemented by now. With that in
mind, I think there are mainly two major "camps": the ones that would
support something new similar to Node ESM vs CommonJS and those who won't.
Having dealt with this mess on the NodeJS side, I'm still on the side that
would support it because even though it's been 10 years worth of "mess", it
has greatly empowered progress. But I think PHP is too conservative to
indulge this camp, so I'm going to focus on Rowan's and Larry's position of
"we need something that builds on top of namespace, not replace it".
If we consider how GitHub, Composer and Docker Hub works, we can pin a very
important aspect of "namespaces": {entity}/{project}. Entity may either be
an individual or an organization, but the concept is mostly the same.
Although it can be argued that PHP has nothing to do with that, I think
that could be a "good-enough" foundation considering the complexity of the
subject. Here is what we could do:
<?php declare(strict_types=1);
namespace Acme\ProjectOne
{
public class Foo {} // same as class Foo {}
private class Bar {} // only visible inside Acme\ProjectOne
protected class Baz {} // visible inside Acme
}
namespace Acme\ProjectTwo
{
new \Acme\ProjectOne\Foo; // Work as always
new \Acme\ProjectOne\Bar; // Fatal error: Uncaught Error: Cannot
instantiate private class \Acme\ProjectOne\Bar from \Acme\ProjectTwo
new \Acme\ProjectOne\Baz; // Works
}
namespace Corp\Corp
{
new \Acme\ProjectOne\Foo; // Work as always
new \Acme\ProjectOne\Bar; // Fatal error: Uncaught Error: Cannot
instantiate private class \Acme\ProjectOne\Bar from \Corp\Corp
new \Acme\ProjectOne\Baz; // Fatal error: Uncaught Error: Cannot
instantiate protected class \Acme\ProjectOne\Baz from \Corp\Corp
}
function (\Acme\ProjectOne\Foo $foo) {} // Works as always
function (\Acme\ProjectOne\Bar $bar) {} // Open question: allow or disallow it?
function (\Acme\ProjectOne\Baz $baz) {} // Open question: allow or disallow it?
This would allow public, private and protected classes in a way that I
believe to be useful for the large ecosystem that surrounds Composer. From
my extremely limited understanding of the engine, I think the easy/natural
step would be to allow private/protected classes to be received outside
its namespace because a type declaration does not trigger autoload.
However, an important question is whether this is enough groundwork that
could lead to optimizations that have been discussed when the topic of
module is brought up. For instance, if type-hint outside the module is
disallowed, could that make it easier to pack and optimize an entire module
if we could instruct PHP how to load all symbols of a namespace all at
once? I don't know.
As I'm writing this down I don't know if it could be related or if its
something only making sense inside my head, but I see the above proposal
paired with a potential amendment to PSR-4 (and Composer), to stimulate the
community to pack small related symbols in a single file with an opt-in
approach:
composer.json:
// ...
"autoload": {
"psr-4-with-module": {
"App\\": "app/",
}
},
// ...
<?php declare(strict_types=1);
// app/Foo/Bar.php
namespace App\Foo;
class Bar {}
// app/Foo.module.php
namespace App\Foo;
enum Baz {}
enum Qux {}
new \App\Foo\Bar; // loads app/Foo/Bar.php
\App\Foo\Baz::option; // file app/Foo/Baz.php does not exist, tries
app/Foo.module.php before giving up
\App\Foo\Qux::option; // app/Foo.module.php has been loaded and Qux
has been registered already
Thoughts?
--
Marco Deleu
Hi!
It's been a few days since I wanted to send this email to internals, but
real life has been a bit chaotic so I apologize if it comes off as if I
didn't research the archives enough. I glossed over the Module conversation
from 10 months ago and the one that recently surfaced and after deeply
thinking about Rowan's and Larry's comments I wanted to throw this idea
into the pit.Lets preface the conversation with the fact that 1) a module system for
PHP has been discussed for several years and 2) if there was an easy and
perfect solution it would have been long implemented by now.If we consider how GitHub, Composer and Docker Hub works, we can pin a
very important aspect of "namespaces": {entity}/{project}.
Hi, thank you for starting this thread.
I have a similar thing, an idea that was sitting on my mind that I want to
put in writing, so I will share it here.
First of all, I want to acknowledge the problems with big projects, big
monoliths that can only have one composer.json and many libraries needed,
that would eventually clash and make upgrades harder than they need to be.
Can we develop the modules concept without linking ourselves to namespaces
or any other new entity?
I think it might be possible, if every symbol that is defined is attached
to a module when it is loaded.
And we can use something like:
module(string $name, callable $closure);
The $closure is defined in the current module, but is executed in the new
module.
Anything else follows the logic: the functions and methods are executed in
the module where they are defined and associated with. Any new symbols
defined inherit the current module.
When a module is defined by another parent module, a relation will be
created between them and the parent module would get access to the child
module symbols, but not also the other way around.
There is no need to change anything on the libraries code but only on
composer and autoloading. That is the key point for easy adoption.
Composer-related, each installed package will be loaded in their own module.
What is yet to figure out:
- can we define internal modules, where the child module symbols are
visible only in the parent defining module and not any level up? - once a module defined, can the parent - child relation be
redefined/changed/removed?
I apologize if this changes the solution discussion, but I didn't wanted to
start a new thread on the same topic.
--
Alex
Hi!
This would allow public, private and protected classes in a way that I
believe to be useful for the large ecosystem that surrounds Composer. From
my extremely limited understanding of the engine, I think the easy/natural
step would be to allow private/protected classes to be received outside
its namespace because a type declaration does not trigger autoload.
This has been discussed before - making Namespaces something beyond what
they are - a convenience string replace. That is given
namespace foo;
use otherns\boo;
function bar () {}
bar();
boo();
The engine quietly rewrites as
function foo\bar () {}
foo\bar();
otherns\boo();
The engine doesn't store any notion of namespace directly, it's just a
string replace on the symbol table. And if I recall correctly this is also
the reason that \ is the namespace operator - the original proposal was for
namespaces to have the :: operator, same as static classes, but late in the
implementation it was found that there were disambiguation problems and the
choice was made to use the \ operator rather than solve those problems. I
assume the problems were judged to be intractable, but I can't say for sure.
Also, a namespace block could appear anywhere, so if a stubborn programmer
really wanted to get access to a private or protected block they could put
a namespace block into their own code with the same namespace.
This also doesn't isolate a module with shared dependencies from changes to
those dependencies. This makes composer libraries impossible to use
effectively in the absence of a mechanism for coordinating those conflicts
from the core. And for worse, the WP core team doesn't want to provide
such. And yes, it really isn't the PHP dev team's direct responsibility to
step in and directly fix that problem. Indirectly perhaps? Only if it is
a benefit to everyone who uses the language.
Neither PHP nor JavaScript deal with directories or other file collections.
We have to look to Java and its jar files or golang modules for examples of
this. And no, the PSR standard of mapping namespaces to directories has
nothing to do with how the engine itself sees packages, namespaces, or the
world. In truth it sees none of these because they don't exist in the
runtime in any meaningful way that I'm aware of. Phar perhaps? I know
little of that system beyond the fact is the manner in which composer and a
few other libs are distributed.
I sense, and I could be wrong here, that there is no appetite for moving
beyond the one file at a time model. So the system I proposed last go round
was still a one file solution.
Other ideas I've seen kicked around - file based privacy. In this schema a
file can declare itself private (public is the assumed and Backward
compatible default) and then mark exceptions to this as public. But for
this to truly be useful I fear some file structure awareness would be
needed and again, at the moment, PHP doesn't do that.
A very long time ago I proposed a require into namespace mechanism. The
engine by default attaches all incoming namespaces to the root /. I
suggested (because I naively thought require was a function construct, not
a statement, and was ignorant of the difference at the time) that "target
namespace" could be the second argument of require and if provided all the
symbols established by that file become attached to that namespace. This
could only work if the autoloader correctly dispatched symbols to the
correct namespaces, and I don't have a clue how it could do that.
The need of the plugin community is code that just plugs into the app and
doesn't need to care about what the other applications are doing. This is
currently possible only if the plugin provides all of its own code and if
it does use composer libraries, it resorts to monkey-typing to change the
names of all the symbols to something prefixed with the plugin name to
avoid collisions. This approach is NOT optimal but it is the only one at
present.
However, an important question is whether this is enough groundwork that
could lead to optimizations that have been discussed when the topic of
module is brought up. For instance, if type-hint outside the module is
disallowed, could that make it easier to pack and optimize an entire module
if we could instruct PHP how to load all symbols of a namespace all at
once? I don't know.
Doing that would likely involve giving the engine some notion of directory,
again as Java does with JAR files, and PHP might do in PHAR files but I
know embarrassingly little about them. Could the existing PHAR structure be
used in some way for a starting point on this? I just don't know, not
without research.
If we consider how GitHub, Composer and Docker Hub works, we can pin a very important aspect of "namespaces": {entity}/{project}. Entity may either be an individual or an organization, but the concept is mostly the same. Although it can be argued that PHP has nothing to do with that, I think that could be a "good-enough" foundation considering the complexity of the subject.
While a two-level namespace root for a project is common, it's far from universal. Picking two examples from the first page of "popular packages" on packagist.org, Guzzle's root namespace is one level ("GuzzleHttp") and the Symfony Console component's root namespace is three levels ("Symfony\Component\Console").
So I think any module or visibility tied to namespaces would need a way to declare that prefix explicitly, not have the language assume it's a particular length.
If we just want namespace visibility, we could use Scala's approach, where the private modifier itself can be qualified:
private[\Symfony\Component\Console] class Foo { ... }
private[\Symfony] class Bar { ... }
If we want modules to have more existence - module-wide declares, optimisation, etc - then we need some way of declaring "this namespace prefix is a module" - a "module" keyword, or "declare_module" function, or something. Those are the lines that Larry and Arnaud were exploring along a while ago - see https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md and https://github.com/arnaud-lb/php-src/pull/10
What Michael Morris is talking about is really a completely different concept - it's more like "containers", in the sense of Docker, Kubernetes, etc, where different sections of code can be isolated, and declare classes with conflicting fully-qualified names. I don't think it's what most applications and libraries would want "modules" to be; it's probably best thought of as a completely separate feature.
--
Rowan Tommins
[IMSoP]
On Wed, May 14, 2025 at 4:08 AM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
What Michael Morris is talking about is really a completely different
concept - it's more like "containers", in the sense of Docker, Kubernetes,
etc, where different sections of code can be isolated, and declare classes
with conflicting fully-qualified names. I don't think it's what most
applications and libraries would want "modules" to be; it's probably best
thought of as a completely separate feature.
Well, it's what Go calls "modules". It's confusing because I was being
truthful, not snarky, when I said "Ask 10 programmers for the definition of
module and expect 12 answers." I'm self trained, so I expect to get my
terms wrong from time to time. But I know enough to identify problems and
needs and I've tried to be clear on that.
I'm currently reading up on Phar and seeing exactly how suited it would be
as a foundation for a module system. I've also been reading on how go
approaches things, but go has package management baked into the compiler -
PHP outsources this to userland. I'm going to guess that's largely because
of lack of staff - PHP has no large backers (leeches like Facebook that use
it heavily and could back it yes, but not backers) and Go has Google.
__
What Michael Morris is talking about is really a completely different concept - it's more like "containers", in the sense of Docker, Kubernetes, etc, where different sections of code can be isolated, and declare classes with conflicting fully-qualified names. I don't think it's what most applications and libraries would want "modules" to be; it's probably best thought of as a completely separate feature.
Well, it's what Go calls "modules". It's confusing because I was being truthful, not snarky, when I said "Ask 10 programmers for the definition of module and expect 12 answers." I'm self trained, so I expect to get my terms wrong from time to time. But I know enough to identify problems and needs and I've tried to be clear on that.
I'm currently reading up on Phar and seeing exactly how suited it would be as a foundation for a module system. I've also been reading on how go approaches things, but go has package management baked into the compiler - PHP outsources this to userland. I'm going to guess that's largely because of lack of staff - PHP has no large backers (leeches like Facebook that use it heavily and could back it yes, but not backers) and Go has Google.
Hi Michael,
Since it appears that nested classes probably won't pass by tomorrow (and thus no need to even touch short-syntax classes); I was going to focus on modules next. As I mentioned in that thread, the two are very closely related on a technical level -- it would have only taken 2-3 lines of changes to turn it into namespaces-as-modules and another 10 to turn it into proper modules (minus syntax support). However, I would implement it very differently knowing what I know today and with this as a goal (vs. nested classes). I have zero idea why people voted "no" and the people who expressed their reasons didn't entirely make sense either. So, I suspect it was just down to a poorly worded RFC and/or misunderstanding of how it worked. I'll have to revisit it again later.
Sorry for the vent; that's not what this thread is about.
Modules. First of all, I'd be more than happy to help with the implementation if you're up for some collaboration. Personally, here are my requirements I was going into it with:
- Impossible name collisions. If you want to name something Foo\Bar in your module and I want to name something Foo\Bar in mine; we should be free to do so. Implementing this is straightforward.
- Simple. I don't want to rely on a package manager to create modules or even use them. I really like the simplicity of "require_once" from time to time, and I don't want to see that go away. I have some ideas here, like
require_module my-module.php
- Easy to optimize. A module should be compiled as a complete unit so opcache (or the engine itself) can make full use of the context. There are a lot of optimizations left on the table right now because the engine dynamically compiles one file at a time.
I haven't really even considered syntax too much; but I personally don't want anything new -- or at least, too "out there." I want it to feel like a natural extension to the language rather than something bolted on.
I suspect there will need to be at least two new user-land elements to this:
- a "module loader" that operates similar to the unified class loader Gina proposed.
- importing modules and aliasing them (as needed).
Would you be interested in collaborating further?
— Rob
Well, it's what Go calls "modules". It's confusing because I was being
truthful, not snarky, when I said "Ask 10 programmers for the definition of
module and expect 12 answers." I'm self trained, so I expect to get my
terms wrong from time to time. But I know enough to identify problems and
needs and I've tried to be clear on that.
I don't know much about Go, but at a glance it uses a similar model to JavaScript and Python where classes don't have a universal name, the names are always local. That's not a different kind of module, it's a fundamentally different language design.
If you want to use two different versions of Guzzle in the same application, the first problem you need to solve has nothing to do with require, or autoloading, or Phar files. The first problem you need to solve is that you now have two classes called \GuzzleHttp\Client, and that breaks a bunch of really fundamental assumptions.
For example:
- plugin1 uses Guzzle v5, runs "$client1 = new \GuzzleHttp\Client", and returns it to the main application
- The main application passes $client1 to plugin2
- plugin2 uses Guzzle v4
plugin2 runs "$client2 = new \GuzzleHttp\Client"
$client1 and $client2 are instances of different classes, with the same name! How does "instanceof" behave? What about "get_class"? What if you serialize and unserialize?
I think if you changed the language enough that those questions didn't matter, it would be a language fork on the scale of Python 2 to 3, or even Perl 5 to Raku (originally called "Perl 6"). Every single application and library would have to be rewritten to use the new concept of what a class is. And most of them would get absolutely no benefit, because they want to reference the same version of a class everywhere in the application.
That's why I think "containers" are the more useful comparison - you need some way to put not just plugin1 itself, but all the third-party code it calls, into some kind of sandbox, as though it was running in a separate process. If you can control what classes can go into and out of that sandbox, then in any piece of code, you don't end up with conflicting meanings for the same name - just as a Linux container can't open a network port directly on the host.
Regards,
Rowan Tommins
[IMSoP]
On Wed, May 14, 2025 at 10:57 AM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
I don't know much about Go, but at a glance it uses a similar model to
JavaScript and Python where classes don't have a universal name, the
names are always local. That's not a different kind of module, it's a
fundamentally different language design.
That said, they call them modules. I'm not going to argue with them, what
do I know?
If you want to use two different versions of Guzzle in the same
application, the first problem you need to solve has nothing to do with
require, or autoloading, or Phar files. The first problem you need to solve
is that you now have two classes called \GuzzleHttp\Client, and that breaks
a bunch of really fundamental assumptions.
Your fundamental assumption is that the different versions are loaded onto
the same symbol. Given the problems you outline yourself, why do that?
You see, PHP doesn't have a mechanism for symbol changing at compile time.
Everything loads onto the root. Does it have to be that way? Or can a
file compile onto a namespace, effectively prefixing that namespace.
For example:
- plugin1 uses Guzzle v5, runs "$client1 = new \GuzzleHttp\Client", and
returns it to the main application- The main application passes $client1 to plugin2
- plugin2 uses Guzzle v4
plugin2 runs "$client2 = new \GuzzleHttp\Client"
If plugin 2 wants to use version 4 for whatever reason, why can't it load
it into \Plugin2\GuzzleHttpClient instead of onto the root??
This is what userland monkey-typers like Strauss do. It works, but there
are issues with this solution outlined elsewhere.
That's why I think "containers" are the more useful comparison - you need
some way to put not just plugin1 itself, but all the third-party code it
calls, into some kind of sandbox, as though it was running in a separate
process. If you can control what classes can go into and out of that
sandbox, then in any piece of code, you don't end up with conflicting
meanings for the same name - just as a Linux container can't open a network
port directly on the host.
Container, module, block, package, plugin, domain, division, fraction,
lump, branch, sliver, splinter, constituent or whatever the hell else you
call it, I don't care. What I need is a way to manage package version
conflicts which arise in the real world when plugins get abandoned or when
coordinating having everyone change dependencies at the same time isn't
feasible.
Container, module, block, package, plugin, domain, division, fraction,
lump, branch, sliver, splinter, constituent or whatever the hell else you
call it, I don't care.
I know you think I'm just being pedantic about names, but
what I was trying to get across was the distinction between different features that we could have both of, because they're solving separate problems.
It's basically about where the dividing line is. If you want this hierarchy of dependencies:
+-- Plugin1 -- AcmeSDK v2 -- Guzzle v5
App --+
+-- Plugin2 -- AcmeSDK v1 -- Guzzle v4
The requirement is not to hide Guzzle from Plugin1 - maybe it needs to create an object from Guzzle and pass it into AcmeSDK.
Instead, the requirement is for Plugin1 to hide both AcmeSDK and Guzzle from Plugin2. You don't want 7 different "things" (whatever you want to call them) in that diagram, you want 3 (App, Plugin1-and- recursive-dependencies, Plugin2-and- recursive-dependencies).
The Linux container analogy is something like this:
+-- container { WordPress -- PHP -- Apache }
Host --+
+-- container { MediaWiki -- PHP -- Apache }
The goal of containers isn't to hide WordPress from Apache or vice versa, it's to hide the two copies of Apache and PHP from each other. There are plenty of things hidden inside Apache (the equivalent of "private classes") but that's a completely separate concept.
I wasn't saying the feature had to be called "containers", just that the analogy might be useful.
Rowan Tommins
[IMSoP]
Well, it's what Go calls "modules". It's confusing because I was being
truthful, not snarky, when I said "Ask 10 programmers for the definition of
module and expect 12 answers." I'm self trained, so I expect to get my
terms wrong from time to time. But I know enough to identify problems and
needs and I've tried to be clear on that.I don't know much about Go, but at a glance it uses a similar model to JavaScript and Python where classes don't have a universal name, the names are always local. That's not a different kind of module, it's a fundamentally different language design.
Go has some weird scoping, for sure. Everything is done by convention instead of syntax. In other words, if you want to export a symbol, you capitalize it; otherwise, it is lower-cased and thus private to the module. Then each directory is a module, and even in the same project, you cannot access another lower-cased symbol from another directory -- er, module.
It is strange, and I don't think it translates to PHP. PHP is generally explicit via syntax over convention.
If you want to use two different versions of Guzzle in the same application, the first problem you need to solve has nothing to do with require, or autoloading, or Phar files. The first problem you need to solve is that you now have two classes called \GuzzleHttp\Client, and that breaks a bunch of really fundamental assumptions.
As written, that simply isn't possible in PHP because there is only one class allowed with a given name. Names of classes are global. I don't think this has to be the case, though. Different languages take different approaches to this. For example, JavaScript allows each module to "close over" its dependencies so each module can import its own version of dependencies. Originally, there wasn't even any deduplication, so you'd have 500 copies of left-pad or whatever. Then there is Go, which doesn't allow you to have multiple versions of modules. You get exactly one version, which is similar to how PHP currently works with composer by default. However, with some massaging, you can "prefix" your imports so you get only your own version. I believe many WordPress plugins do this, so each plugin can use their own version of things.
I'm fairly certain we can do a similar thing so that each module gets its own unique 'namespace' in the class table such that two modules can define the same classes. So ModuleA and ModuleB can have Foo\Bar without conflicting with one another. From the user's perspective, we can probably hide that technical detail from them but allow aliasing:
use module ModuleA; // import ModuleA's namespace into our current namespace for this file
use module ModuleB as Baz; // import ModuleB's namespace into our current namespace for this file, but with a prefix
Foo\Bar; // ModuleA\Foo\Bar
Baz\Foo\Bar; // ModuleB\Foo\Bar
I'm just spitballing syntax here, and I'm not suggesting it actually work like this, but I just want to illustrate that I think there are reasonable ways to allow modules to have conflicting names.
For example:
- plugin1 uses Guzzle v5, runs "$client1 = new \GuzzleHttp\Client", and returns it to the main application
- The main application passes $client1 to plugin2
- plugin2 uses Guzzle v4
plugin2 runs "$client2 = new \GuzzleHttp\Client"$client1 and $client2 are instances of different classes, with the same name! How does "instanceof" behave? What about "get_class"? What if you serialize and unserialize?
I'm of the opinion that the "names" of the module classes be distinct so that humans (and deserializers) know it is from a module. Something like [ModuleA]\Foo\Bar.
I think if you changed the language enough that those questions didn't matter, it would be a language fork on the scale of Python 2 to 3, or even Perl 5 to Raku (originally called "Perl 6"). Every single application and library would have to be rewritten to use the new concept of what a class is. And most of them would get absolutely no benefit, because they want to reference the same version of a class everywhere in the application.
I suspect the hard part will be defining the module in the first place. IE, the "package.json" or "go.mod" or whatever it gets called. As composer isn't a part of the PHP project, I don't want to take it for granted, but I also don't want to rely on it. That means each module may have to define its own "loader" or somehow define what PHP files encompass the module. As I mentioned earlier, PHP doesn't usually operate by convention, though the community tends to force it to anyway (PSR-4 autoloading comes to mind immediately); so we'd need something that is explicit but automatable so the community can implement conventions.
That's going to be the hard part.
That's why I think "containers" are the more useful comparison - you need some way to put not just plugin1 itself, but all the third-party code it calls, into some kind of sandbox, as though it was running in a separate process. If you can control what classes can go into and out of that sandbox, then in any piece of code, you don't end up with conflicting meanings for the same name - just as a Linux container can't open a network port directly on the host.
Exactly.
Regards,
Rowan Tommins
[IMSoP]
— Rob
As written, that simply isn't possible in PHP because there is only one class allowed with a given name. Names of classes are global. I don't think this has to be the case, though. Different languages take different approaches to this. For example, JavaScript allows each module to "close over" its dependencies so each module can import its own version of dependencies.
I would say that JavaScript doesn't just allow this, as an added feature, it requires it, as a fundamental design decision:
- In JavaScript, Python, etc, when you declare a function or class, you are creating an anonymous object, and assigning it to a local variable. Code reuse requires you to pass that object around.
- In PHP, Java, C#, etc, when you declare a function or class, you are adding a permanent named item to a global list. Code reuse is about knowing the global names of things.
It's worth noting that JavaScript didn't need to add any features to make NPM, Bower, etc work; everything they do is based on the fact that declarations are objects which can be passed around at will.
That's why I don't think "JavaScript can do it" is relevant, because the way JavaScript does it is impossible in PHP. We're much better off looking at how PHP works, and what problems we're actually trying to solve.
And that in turn is why I was reaching for Linux containers as an alternative analogy, to think about the problem without jumping to the wrong solution.
Rowan Tommins
[IMSoP]
As written, that simply isn't possible in PHP because there is only one class allowed with a given name. Names of classes are global. I don't think this has to be the case, though. Different languages take different approaches to this. For example, JavaScript allows each module to "close over" its dependencies so each module can import its own version of dependencies.
I would say that JavaScript doesn't just allow this, as an added feature, it requires it, as a fundamental design decision:
- In JavaScript, Python, etc, when you declare a function or class, you are creating an anonymous object, and assigning it to a local variable. Code reuse requires you to pass that object around.
- In PHP, Java, C#, etc, when you declare a function or class, you are adding a permanent named item to a global list. Code reuse is about knowing the global names of things.
It's worth noting that JavaScript didn't need to add any features to make NPM, Bower, etc work; everything they do is based on the fact that declarations are objects which can be passed around at will.
That's why I don't think "JavaScript can do it" is relevant, because the way JavaScript does it is impossible in PHP. We're much better off looking at how PHP works, and what problems we're actually trying to solve.
And that in turn is why I was reaching for Linux containers as an alternative analogy, to think about the problem without jumping to the wrong solution.
Rowan Tommins
[IMSoP]
Hey Rowan,
When working on nested classes, I did spend quite a bit of time tinkering with alternative implementations. One of those implementations was having the ability for classes to have their own class tables (both literally and emulated via name mangling) which would have allowed for classes to have private classes that could share names with external classes. This turned out to be an utter disaster of an idea for many reasons. Namely, PHP doesn't really have any native support for shadowing names. Sure, there is aliasing via use statements, but that only works for classes outside the current namespace. As long as we can guarantee that a module acts as a special namespace (under the hood), the only potential for collisions will be in the module itself.
All that is to say that I don't think comparing PHP to JavaScript is appropriate when considering modules. JavaScript doesn't have types, so I can pass you an EpicStringV2 when you're expecting an EpicStringV1, and as long as my EpicStringV2 has the right prototypical behavior and data, it will work just fine. PHP is typed, and fairly strongly typed. There is effectively no way to have multiple versions of the same type running around a codebase and pass type checks. Changing this would be effectively impossible and probably unsound from a type-theory perspective.
— Rob
As written, that simply isn't possible in PHP because there is only one class allowed with a given name. Names of classes are global. I don't think this has to be the case, though. Different languages take different approaches to this. For example, JavaScript allows each module to "close over" its dependencies so each module can import its own version of dependencies.
I would say that JavaScript doesn't just allow this, as an added feature, it requires it, as a fundamental design decision:
- In JavaScript, Python, etc, when you declare a function or class, you are creating an anonymous object, and assigning it to a local variable. Code reuse requires you to pass that object around.
- In PHP, Java, C#, etc, when you declare a function or class, you are adding a permanent named item to a global list. Code reuse is about knowing the global names of things.
It's worth noting that JavaScript didn't need to add any features to make NPM, Bower, etc work; everything they do is based on the fact that declarations are objects which can be passed around at will.
That's why I don't think "JavaScript can do it" is relevant, because the way JavaScript does it is impossible in PHP. We're much better off looking at how PHP works, and what problems we're actually trying to solve.
And that in turn is why I was reaching for Linux containers as an alternative analogy, to think about the problem without jumping to the wrong solution.
Rowan Tommins
[IMSoP]Hey Rowan,
When working on nested classes, I did spend quite a bit of time tinkering with alternative implementations. One of those implementations was having the ability for classes to have their own class tables (both literally and emulated via name mangling) which would have allowed for classes to have private classes that could share names with external classes. This turned out to be an utter disaster of an idea for many reasons. Namely, PHP doesn't really have any native support for shadowing names. Sure, there is aliasing via use statements, but that only works for classes outside the current namespace. As long as we can guarantee that a module acts as a special namespace (under the hood), the only potential for collisions will be in the module itself.
All that is to say that I don't think comparing PHP to JavaScript is appropriate when considering modules. JavaScript doesn't have types, so I can pass you an EpicStringV2 when you're expecting an EpicStringV1, and as long as my EpicStringV2 has the right prototypical behavior and data, it will work just fine. PHP is typed, and fairly strongly typed. There is effectively no way to have multiple versions of the same type running around a codebase and pass type checks. Changing this would be effectively impossible and probably unsound from a type-theory perspective.
— Rob
Haha, I just reread your email and realized we're basically saying the same thing (I think?).
— Rob
The Problem: Interoperability.
That's really it. Scenario
Alice provides whatchamacallit A that depends on other whatchamacallit D to
work.
Bob provides whatchamacallit B that also depends on D.
Charles is using A and B.
D gets updated with a new incompatible API to its prior version.
Alice publishes an update which includes a security fix.
Bob retired.
Charles, who can't program, can't update to Alice's latest code. His site
eventually gets pwned.
That's the problem. Packages with dependencies are not interoperable at
this time. They must be self contained. This is why WordPress doesn't
support Composer at all.
Drupal, Laravel et al bypass this problem by forcing all their
whachamacallits to stay on the same version. This has limited their market
penetration compared to WordPress because, despite being significantly
superior codebases in all respects, they aren't user friendly to someone
who doesn't code at all.
The Solution (10,000 overview)
Composer could be made to allow interoperable packages, but it will need
support at the language level to do so. Specifically, it needs to know who
wants what. It can then make decisions based on that information.
Composer's primary link to the language is the autoload closure it
provides. That closure currently takes one argument - the fully qualified
name of the symbol to be loaded - currently almost always classes as for
various reasons function autoloading isn't a thing. Can it not take a
second argument to modify its behavior? The current behavior is to flat
require the file if it is found in accordance to whichever schema is in
use. Perhaps we don't want that anymore - perhaps we want to return the
file path to use. This allows the engine to make decisions about how
exactly to include the file, including the possibility of monkey typing it
as can be done in userland, though when done in userland this effectively
generates a new package.
(5,000 ft. overview)
Suppose we have a whatchamacallit that declares its namespace as a new root
independent of / . If a file inclusion happens in this namespace, this
namespace prepends everything in the included file. So if I do a file
include in the \MyPlugin namespace and that file declares its namespace as
Twig, it will become \MyPlugin\Twig.
That works, but direct file include is no longer the PHP norm though.
Autoloading is. So we need to tell the Autoloader that we want a file path
returned - do NOT require the file yourself in your namespace. This could
be as simple as a boolean flag of true sent to the autoloader. BUT it isn't
- the autoloader (usually composer) needs to know the identity of this
requestor because by configuration in the package json (the details of
which are wildly out of scope) it might change which file path it returns.
When the engine gets the path it does the include and the prepending
business on the fly that Strauss and similar packages already do
in userland.
(2,500 ft overview)
The above I think would more or less work, but it would lead to massive
code duplication as Whatchamacallit A and B now have their own D's at \A\D
and \B\D (assuming namespaces match whatchamacallit names).
Here's what I think would prevent that:
A asks the autoloader for D. The autoloader returns a file path and the
engine mounts to \D
B asks for D. The autoloader returns a different file path so the engine
mounts to B\D and rewrites the D file with the new namespace the same way
Stauss would have done.
This works except for the problem of who had the older version, A or B? and
what order are A and B going to be asking - cause depending on the
application's architecture this order is not guaranteed.
To solve this the autoloader can tell the engine it is safe to mount the
file on root using an array return of [path, true] and mount on the
whatchamacallit's namespace if [path, false]. So
A asks for D. Autoloader returns [path, false]. Engine maps to \A\D and
monkey types D as needed.
B asks for D. Autoloader returns [path, true]. Engine maps to \D
Non whatchamacallit code at namespace C asks for D. It will get the same
version B is using and the autoloader shouldn't be queried unless C makes
this ask before B.
When C asks the autoloader gets (string RequestedSymbol, null) so it can
either do the require itself or return a string, either will work (and it
has to be this way for backwards compat).
When B asks the autoloader gets ( Requested, 'B' ) and it should return
[path, true]
I hope the above is followable. It's more of a morning brainstorm than a
spec.
The Problem: Interoperability.
That's really it. Scenario
Alice provides whatchamacallit A that depends on other whatchamacallit D to
work.
Bob provides whatchamacallit B that also depends on D.
Charles is using A and B.
D gets updated with a new incompatible API to its prior version.
Alice publishes an update which includes a security fix.
Bob retired.
Charles, who can't program, can't update to Alice's latest code. His site
eventually gets pwned.
Let me correct something here. The whole reason I was bringing in the distinction between "module" and "container" is that B and C are one kind of thing, but D is a different kind of thing.
D is something like Guzzle. There is zero motivation for Guzzle to be rewritten in a way that forces its dependencies to be isolated. It depends on packages like "psr/http-client" whose entire purpose is to define interfaces that multiple packages agree on.
A, meanwhile, isn't a thing at all; it's just any old PHP code - in your example, the whole spaghetti of WordPress core.
B and C are the only "whatchamacallits" - they are, in your example, WordPress plugins. They are the thing you want a boundary around, the black box you want conflicting names to be hidden by.
Suppose we have a whatchamacallit that declares its namespace as a new root
independent of / . If a file inclusion happens in this namespace, this
namespace prepends everything in the included file. So if I do a file
include in the \MyPlugin namespace and that file declares its namespace as
Twig, it will become \MyPlugin\Twig.
What does it mean, exactly, for a file inclusion to "happen in a namespace"? Bear in mind, most of the files we want to load, whether explicitly or via an autoloader, are not requests from A (the WordPress plugin) directly to D (Guzzle); they are references between files inside D, or in further dependencies that A has no idea about at all.
What PHP needs to track, somehow, is that a whole bunch of code is "inside" something, or "coloured by" something, in a way that is completely recursive.
That works, but direct file include is no longer the PHP norm though.
Autoloading is. So we need to tell the Autoloader that we want a file path
returned - do NOT require the file yourself in your namespace.
This for me is a non-starter: the existing packages which you want to make use of have little or no motivation to adapt to this new system.
Again, think about Linux containers: applications don't get a message saying "you're running in a container, please use different file I/O conventions"; they think they are accessing the root filesystem, and the host silently rewrites the access to be somewhere in the middle of a larger tree.
I think the way it would need to work would be some global state inside the compiler, so that regardless of how the code ended up being loaded, an extra transform was included in the compilation pipeline to attempt to rewrite all definitions, and all references to those definitions.
(I say "attempt", because even with all this built into the compiler, PHP's highly dynamic nature means there would be code patterns that the rewriter would not see; the whole thing would come with a bunch of caveats.)
The above I think would more or less work, but it would lead to massive
code duplication as Whatchamacallit A and B now have their own D's at \A\D
and \B\D (assuming namespaces match whatchamacallit names).
I don't think this is a problem that can or should be solved.
Imagine A and B both use the same version of E, but different versions of D; and E references D. If we try to de-duplicate, we load one copy of E, but when called from A it needs to reference \A\D and when called from B it needs to reference \B\D. Clearly, that's not going to work, so we're forced to define a separate \A\E and \B\E.
Note that this is completely different from any de-duplication of files on disk that a package manager might perform. It's a bit like the same C source file being compiled into two different object files with different #defines in effect.
I'm still not convinced that all this complexity actually leaves you better off than building a Composer plugin that automatically applies the rewriting to a whole directory at source code level.
Rowan Tommins
[IMSoP]
On Tue, May 20, 2025 at 6:18 PM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
The Problem: Interoperability.
That's really it. Scenario
Alice provides whatchamacallit A that depends on other whatchamacallit D
to
work.
Bob provides whatchamacallit B that also depends on D.
Charles is using A and B.
D gets updated with a new incompatible API to its prior version.
Alice publishes an update which includes a security fix.
Bob retired.
Charles, who can't program, can't update to Alice's latest code. His site
eventually gets pwned.Let me correct something here. The whole reason I was bringing in the
distinction between "module" and "container" is that B and C are one kind
of thing, but D is a different kind of thing.D is something like Guzzle. There is zero motivation for Guzzle to be
rewritten in a way that forces its dependencies to be isolated. It depends
on packages like "psr/http-client" whose entire purpose is to define
interfaces that multiple packages agree on.A, meanwhile, isn't a thing at all; it's just any old PHP code
I'll stop you there. You are deliberately misrepresenting what I wrote and
even a cursory glance at it makes that clear. You are not trying to be
constructive in any way, you're trolling.
I'll stop you there. You are deliberately misrepresenting what I wrote and
even a cursory glance at it makes that clear. You are not trying to be
constructive in any way, you're trolling.
I'm sorry you got that impression. I can assure you that I am not trolling, and my email was an entirely genuine attempt to engage with the problem that you were trying to describe.
My understanding of the example is that there are two WordPress plugins, which want independent sets of Composer dependencies. There might be 20 different Composer packages used by each plugin, but those packages don't need any special relationship with each other, they just need a special relationship with the WordPress plugin.
So if we can come up with a solution where only the WordPress plugins need to be changed, and you can use whatever dependencies you want without waiting for them to be changed to a new way of working, is that not a good thing?
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.
Rowan Tommins
[IMSoP]
My understanding of the example is that there are two WordPress plugins, which want independent sets of Composer dependencies. There might be 20 different Composer packages used by each plugin, but those packages don't need any special relationship with each other, they just need a special relationship with the WordPress plugin.
Looking closely, I see I did make one honest mistake: in your example, the WordPress plugins are A and B, not B and C. So my sentence should have read "A and B are one kind of thing, but D is a different kind of thing".
Rowan Tommins
[IMSoP]
On Wed, May 21, 2025 at 8:27 AM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
So if we can come up with a solution where only the WordPress plugins need
to be changed, and you can use whatever dependencies you want without
waiting for them to be changed to a new way of working, is that not a good
thing?
Yes, and that's all I think is needed here. I could modify plugin code if I
needed to. The majority of developers capable of doing this have left
WordPress in disgust - I did too, but I needed the work. Since I have to
work with it I'd like to make it more sane. One major step in that
direction is the Timber library which bridges WP to Twig and gets rid of
that god damned loop architecture they think is the bees knees but in
reality is an antipattern and untestable spaghetti nightmare.
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.
Looking closely, I see I did make one honest mistake: in your example, the
WordPress plugins are A and B, not B and C. So my sentence should have read
"A and B are one kind of thing, but D is a different kind of thing".
That's what set me off the most and I over-reacted. To you and to the list
at large, I apologize. I'm just frustrated - I feel like a five year old
trying to explain a problem to a physicist.
Anyone familiar with C++'s friend keyword? It’s not a direct replacement
for modules, but it solves similar problems — allowing trusted classes or
functions to access private/protected members without making them public.
Friend has been brought up before and I believe it was in at least one RFC
before and voted down. That doesn't mean the issue can't be revisited, but
look into the archive and see if my memory is right and if so why was it
voted down before? IIRC it's tied to the fact PHP doesn't have a notion of
namespace level visibility. Classes and functions outside of classes must
be public in the current architecture.
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".
--
Rowan Tommins
[IMSoP]
On Thu, May 22, 2025 at 4:29 PM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
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/apiclientWe 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/apiclientThat 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 BobsDocsAll 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 BobsDocsIn 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:
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:
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
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.
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.
Hi Michael,
I'm going to skip over all the details about the autoloader for now, because I think they're going deep into implementation details, and I want to focus on the same top-level design as my previous email.
Bobs docs needs an older version of Monolog and is configured appropriately
in its composer.json file, so ... 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.
If I'm following correctly, you suggest that we would end up with class names like this:
\v1\Monolog\Logger
\v2\Monolog\Logger
\v5\Google\Client
\v7\Google\Client
It feels like there's a lot of complexity in the package manager here - it's got to keep track of which versions of each package are installed, what they depend on, and decide what prefixes need to be used where. You also suggest that one version of each package is left with no prefix, which adds even more complexity.
The Googl\ApiClient of BobDocs is again, up to the autoloader. Assuming it
too is different (since it's using an older Monolog)
The biggest problem comes when this assumption doesn't hold. I actually chose these particular packages to illustrate this problem, then left it out of my previous message. It happens that the latest version of google/apiclient supports both monolog/monolog 2.9 and 3.0, so it's possible to have:
- AlicesCalendar wants to use google/apiclient 2.18 and monolog/monolog 2.9
- BobsDocs wants to use google/apiclient 2.18 and monolog/monolog 3.0
If the package manager is adding prefixes to individual package versions, we will have one class called \v2_18\Google\Client containing our familiar "new Logger" line. AlicesCalendar will expect that line to create a \v2_9\Monolog\Logger, but BobsDocs will expect it to create a \v3_0\Monolog\Logger. We can't please both of them without creating an extra copy of Google\Client with a different prefix.
So the version of an individual package isn't enough to decide the prefix, we need to know which set of packages it belongs to.
My suggestion uses a much simpler rule to define the prefix: if it's loaded "inside" AlicesCalendar, add the prefix "\AlicesCalendar". All the classes that are "inside" are completely sandboxed from the classes "outside", without needing any interaction with a package manager.
As far as I know, this is how existing userland solutions work, and I haven't yet spotted a reason why it needs to be any more complex than that.
Regards,
Rowan Tommins
[IMSoP]
Hi Michael,
I'm going to skip over all the details about the autoloader for now, because I think they're going deep into implementation details, and I want to focus on the same top-level design as my previous email.
Bobs docs needs an older version of Monolog and is configured appropriately
in its composer.json file, so ... 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.If I'm following correctly, you suggest that we would end up with class names like this:
\v1\Monolog\Logger
\v2\Monolog\Logger
\v5\Google\Client
\v7\Google\ClientIt feels like there's a lot of complexity in the package manager here - it's got to keep track of which versions of each package are installed, what they depend on, and decide what prefixes need to be used where. You also suggest that one version of each package is left with no prefix, which adds even more complexity.
The Googl\ApiClient of BobDocs is again, up to the autoloader. Assuming it
too is different (since it's using an older Monolog)The biggest problem comes when this assumption doesn't hold. I actually chose these particular packages to illustrate this problem, then left it out of my previous message. It happens that the latest version of google/apiclient supports both monolog/monolog 2.9 and 3.0, so it's possible to have:
- AlicesCalendar wants to use google/apiclient 2.18 and monolog/monolog 2.9
- BobsDocs wants to use google/apiclient 2.18 and monolog/monolog 3.0
If the package manager is adding prefixes to individual package versions, we will have one class called \v2_18\Google\Client containing our familiar "new Logger" line. AlicesCalendar will expect that line to create a \v2_9\Monolog\Logger, but BobsDocs will expect it to create a \v3_0\Monolog\Logger. We can't please both of them without creating an extra copy of Google\Client with a different prefix.
So the version of an individual package isn't enough to decide the prefix, we need to know which set of packages it belongs to.
My suggestion uses a much simpler rule to define the prefix: if it's loaded "inside" AlicesCalendar, add the prefix "\AlicesCalendar". All the classes that are "inside" are completely sandboxed from the classes "outside", without needing any interaction with a package manager.
As far as I know, this is how existing userland solutions work, and I haven't yet spotted a reason why it needs to be any more complex than that.
Regards,
Rowan Tommins
[IMSoP]
My only concern is how this would be handled in the class tables. Right now, \AlicesCalendar\Monolog\Logger and \BobsDocs\Monolog\Logger would be considered entirely different types -- as in, not compatible. So if AlicesCalendar returns a type that BobsDocs expects, they won't be able to talk to each other.
So, this means we'd need a couple of different types of dependencies:
- "direct dependencies" that work in a containerized way
- "parent dependencies" that expect a parent to provide the dependency so it can interoperate between packages
I assume that it will be up to a dependency resolver (either composer or something else) will need to figure out which direct dependencies to "hoist" up and provide a compatible version between the two packages.
That then begs the question of whether this complication is needed at all? I can understand why having a 'containerized' package system is useful (in the case of WordPress or plugins in general), but I'm wondering if it is actually needed?
If we look at npm and yarn and how they handle this in the Javascript space, they basically install compatible packages when possible, and only 'contain' them when it would introduce an incompatibility.
I have some ideas here, but I need some time to think on it; but I also want to point out the problem to see if anyone else has any ideas.
— Rob
My only concern is how this would be handled in the class tables. Right now, \AlicesCalendar\Monolog\Logger and \BobsDocs\Monolog\Logger would be considered entirely different types -- as in, not compatible. So if AlicesCalendar returns a type that BobsDocs expects, they won't be able to talk to each other.
Once again, I'd like to use the Linux Container analogy: a process in one container never communicates directly with a process in another container. The process "thinks" it's running as normal, but is actually isolated inside a sandbox. The container then defines the inputs and outputs it wants to open between that sandbox and the host, and something running on the host can wire those up as necessary.
I assume that it will be up to a dependency resolver (either composer or something else) will need to figure out which direct dependencies to "hoist" up and provide a compatible version between the two packages.
I see this as the responsibility of each "container": if AlicesCalendar wants to use an un-sandboxed version of a PSR interface or a framework component, it declares that to the "host" (e.g. WordPress core). The PHP engine then knows to leave that interface name without a prefix. Any other class - whether it's written by Alice or installed by Composer - exists inside the sandbox, and gets a prefix.
Importantly, all of this should happen on the PHP symbol level (classes, interfaces, functions); the sandboxing mechanism doesn't need to know about package managers - just as Docker, Kunernetes, etc, don't know about APT / Yum / whatever Apine calls it.
Rowan Tommins
[IMSoP]
The Problem: Interoperability.
That's really it.
I think this is why Rowan keeps telling you to call or compare this with
"Containers" and not modules. When I opened this thread, my interest was in
bundling multiple files all at once so that the PHP engine can make
assumptions and optimizations about it and expand namespace to also allow
class visibility. To me, and I believe to a vast majority of PHP users,
interoperability is not a problem. We don't need, and, depending on how we
position it, don't want multiple versions of the same package on a single
application.
--
Marco Deleu
On Tue, May 20, 2025 at 11:08 AM Michael Morris tendoaki@gmail.com
wrote:The Problem: Interoperability.
That's really it.
I think this is why Rowan keeps telling you to call or compare this with
"Containers" and not modules.
Which is why I switched to calling them a "whachamacallit" to set the issue
aside and focus on concepts, not terms. He insisted on trolling despite
that.
When I opened this thread, my interest was in bundling multiple files all
at once so that the PHP engine can make assumptions and optimizations about
it and expand namespace to also allow class visibility. To me, and I
believe to a vast majority of PHP users, interoperability is not a problem.
We don't need, and, depending on how we position it, don't want multiple
versions of the same package on a single application.
Not all of us have the pleasure of living in ivory towers like you do. In
the real world code isn't perfect and problems need to be solved.
No one wants multiple versions of the same package in a single application.
How stupid do you think I am?
We don't always get what we want - the need for multiple package versions
does arise in the real world which is why other languages such as golang
and JavaScript can allow it.
Hi!
It's been a few days since I wanted to send this email to internals, but
real life has been a bit chaotic so I apologize if it comes off as if I
didn't research the archives enough. I glossed over the Module conversation
from 10 months ago and the one that recently surfaced and after deeply
thinking about Rowan's and Larry's comments I wanted to throw this idea
into the pit.Lets preface the conversation with the fact that 1) a module system for
PHP has been discussed for several years and 2) if there was an easy and
perfect solution it would have been long implemented by now. With that in
mind, I think there are mainly two major "camps": the ones that would
support something new similar to Node ESM vs CommonJS and those who won't.
Having dealt with this mess on the NodeJS side, I'm still on the side that
would support it because even though it's been 10 years worth of "mess", it
has greatly empowered progress. But I think PHP is too conservative to
indulge this camp, so I'm going to focus on Rowan's and Larry's position of
"we need something that builds on top of namespace, not replace it".If we consider how GitHub, Composer and Docker Hub works, we can pin a
very important aspect of "namespaces": {entity}/{project}. Entity may
either be an individual or an organization, but the concept is mostly the
same. Although it can be argued that PHP has nothing to do with that, I
think that could be a "good-enough" foundation considering the complexity
of the subject. Here is what we could do:<?php declare(strict_types=1); namespace Acme\ProjectOne { public class Foo {} // same as class Foo {} private class Bar {} // only visible inside Acme\ProjectOne protected class Baz {} // visible inside Acme } namespace Acme\ProjectTwo { new \Acme\ProjectOne\Foo; // Work as always new \Acme\ProjectOne\Bar; // Fatal error: Uncaught Error: Cannot instantiate private class \Acme\ProjectOne\Bar from \Acme\ProjectTwo new \Acme\ProjectOne\Baz; // Works } namespace Corp\Corp { new \Acme\ProjectOne\Foo; // Work as always new \Acme\ProjectOne\Bar; // Fatal error: Uncaught Error: Cannot instantiate private class \Acme\ProjectOne\Bar from \Corp\Corp new \Acme\ProjectOne\Baz; // Fatal error: Uncaught Error: Cannot instantiate protected class \Acme\ProjectOne\Baz from \Corp\Corp } function (\Acme\ProjectOne\Foo $foo) {} // Works as always function (\Acme\ProjectOne\Bar $bar) {} // Open question: allow or disallow it? function (\Acme\ProjectOne\Baz $baz) {} // Open question: allow or disallow it?
This would allow public, private and protected classes in a way that I
believe to be useful for the large ecosystem that surrounds Composer. From
my extremely limited understanding of the engine, I think the easy/natural
step would be to allow private/protected classes to be received outside
its namespace because a type declaration does not trigger autoload.
However, an important question is whether this is enough groundwork that
could lead to optimizations that have been discussed when the topic of
module is brought up. For instance, if type-hint outside the module is
disallowed, could that make it easier to pack and optimize an entire module
if we could instruct PHP how to load all symbols of a namespace all at
once? I don't know.As I'm writing this down I don't know if it could be related or if its
something only making sense inside my head, but I see the above proposal
paired with a potential amendment to PSR-4 (and Composer), to stimulate the
community to pack small related symbols in a single file with an opt-in
approach:composer.json:
// ... "autoload": { "psr-4-with-module": { "App\\": "app/", } }, // ...
<?php declare(strict_types=1); // app/Foo/Bar.php namespace App\Foo; class Bar {} // app/Foo.module.php namespace App\Foo; enum Baz {} enum Qux {} new \App\Foo\Bar; // loads app/Foo/Bar.php \App\Foo\Baz::option; // file app/Foo/Baz.php does not exist, tries app/Foo.module.php before giving up \App\Foo\Qux::option; // app/Foo.module.php has been loaded and Qux has been registered already
Thoughts?
--
Marco Deleu
Hey all,
Anyone familiar with C++'s friend keyword? It’s not a direct replacement
for modules, but it solves similar problems — allowing trusted classes or
functions to access private/protected members without making them public.
The idea: allow one class to explicitly grant access to another class or
function. Useful for tightly coupled code that still wants to maintain
encapsulation. Since friend would be a new keyword, it’s safe to add
(currently a parse error).
Examples:
`
class Engine {
private string $status = 'off';
friend class Car;
friend function debugEngine;
}
class Car {
public function start(Engine $e) {
$e->status = 'on'; // allowed
}
}
function debugEngine(Engine $e) {
echo $e->status; // also allowed
}
`
This avoids reflection, awkward internal APIs, or overly permissive
visibility. Could be useful in frameworks, testing tools, or any place
where selective trust is helpful.
Thoughts?
Hammed